Initial commit: BBB VPS prep (Ubuntu 22.04, nftables, Docker)

This commit is contained in:
Daniel Allaire 2026-02-08 15:50:57 -05:00
commit c59c5e9ef1
18 changed files with 680 additions and 0 deletions

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
*.retry
*.log
.DS_Store
.vscode/
.idea/
.ansible/
.cache/
.venv/
__pycache__/
group_vars/*secret*.yml
**/vault.yml
.env

57
Makefile Normal file
View file

@ -0,0 +1,57 @@
SHELL := /bin/bash
INVENTORY ?= inventory.ini
PLAYBOOK ?= site.yml
EXTRA_VARS ?=
ANSIBLE ?= ansible-playbook
.PHONY: help ping check run dry-run diff vars tree clean
help:
@echo "Targets:"
@echo " make ping - ping hosts (Ansible)"
@echo " make check - syntax-check playbook"
@echo " make run - apply playbook"
@echo " make dry-run - check mode with diff (no changes)"
@echo " make diff - show diff of changes (implies --check)"
@echo " make vars - show vars for a host (requires host=...)"
@echo " make tree - show repository tree"
@echo ""
@echo "Examples:"
@echo " make run EXTRA_VARS='-e ssh_allow_cidrs_v4=[\"1.2.3.4/32\"]'"
@echo " make vars host=bbb"
ping:
ansible -i $(INVENTORY) vps -m ping
check:
$(ANSIBLE) -i $(INVENTORY) $(PLAYBOOK) --syntax-check
run:
$(ANSIBLE) -i $(INVENTORY) $(PLAYBOOK) --become $(EXTRA_VARS)
dry-run:
$(ANSIBLE) -i $(INVENTORY) $(PLAYBOOK) --become --check --diff $(EXTRA_VARS)
diff:
$(ANSIBLE) -i $(INVENTORY) $(PLAYBOOK) --become --check --diff $(EXTRA_VARS)
vars:
@if [[ -z "$(host)" ]]; then echo "ERROR: host is required, e.g. make vars host=bbb"; exit 2; fi
ansible -i $(INVENTORY) $(host) -m debug -a "var=hostvars[inventory_hostname]" | sed -n '1,200p'
tree:
@python3 - <<'PY'
import os
for root, dirs, files in os.walk('.'):
dirs[:] = [d for d in dirs if d not in {'.git','__pycache__','.venv','.cache'}]
level = root.count(os.sep)
indent = ' ' * level
print(f"{indent}{os.path.basename(root)}/")
subindent = ' ' * (level + 1)
for f in sorted(files):
print(f"{subindent}{f}")
PY
clean:
@find . -name "*.retry" -delete

36
README.md Normal file
View file

@ -0,0 +1,36 @@
# bbb-vps-prep (Ubuntu 22.04) — nftables + hardening + Docker (BBB 3.x ready)
Ce dépôt prépare un VPS **Ubuntu 22.04** pour BigBlueButton (BBB 3.x) avec :
- SSH durci (clés seulement, root interdit, allowlist utilisateurs)
- **nftables** comme firewall (pas UFW, pas iptables en gestion humaine)
- Docker CE (repo officiel) requis par BBB 3.x
- compatibilité Docker : on **nefface jamais** le ruleset global, on applique une **table nftables dédiée** (`inet hostfilter`)
- fail2ban avec action nftables
- journald persistant + rotation
- sysctl hardening (inclut ip_forward pour Docker)
- swapfile (utile avec 12 Go RAM)
## Mise en route
1) Éditer `inventory.ini` (IP du VPS)
2) Éditer `group_vars/all.yml` (clé publique, CIDR SSH, FQDN)
3) Exécuter :
```bash
make check
make run
```
Mode dry-run :
```bash
make dry-run
```
## Ports ouverts (par défaut)
- SSH: 22/TCP (restreignable par CIDR)
- BBB: 80/TCP, 443/TCP, 1638432768/UDP
- TURN (cohab): 3478/TCP+UDP, 5349/TCP, 3276965535/UDP
## Notes
- Docker gardera ses règles nécessaires au bridge/NAT; ce dépôt ajoute une couche “policy” via nftables.
- TURN sur 443 “exclusif” nest pas compatible cohabitation 1 IP avec nginx/BBB (on utilise 5349/TLS).

41
group_vars/all.yml Normal file
View file

@ -0,0 +1,41 @@
# --- Identity ---
fqdn: "bbb.chezlepro.ca"
# --- Admin user + keys ---
admin_user: "ansible"
admin_pubkeys:
- "ssh-ed25519 AAAA...REMPLACE... ton_key_ed25519 ..."
# --- SSH hardening ---
ssh_port: 22
ssh_allow_cidrs_v4: ["0.0.0.0/0"] # RECO: remplace par ton IP fixe (ex: "1.2.3.4/32")
ssh_allow_cidrs_v6: ["::/0"] # RECO: remplace par ton IP /128 si applicable
ssh_allow_users: ["ansible"]
disable_password_auth: true
permit_root_login: "no"
# --- BBB ports ---
bbb_udp_min: 16384
bbb_udp_max: 32768
# --- TURN cohabitation (1 IP) ---
turn_enabled: true
turn_listen_port: 3478 # TCP/UDP
turn_tls_port: 5349 # TURN over TLS (standard). 443 est conflictuel sur 1 IP avec nginx/BBB
turn_relay_udp_min: 32769
turn_relay_udp_max: 65535
# --- Hardening toggles ---
enable_unattended_upgrades: true
enable_fail2ban: true
journald_persistent: true
# --- Swap (utile avec 12G RAM) ---
manage_swapfile: true
swapfile_path: /swapfile
swapfile_size_mb: 8192
# --- Docker (BBB 3.x requires latest docker) ---
docker_install: true
docker_add_admin_to_group: false # IMPORTANT: groupe docker = root-equivalent

2
inventory.ini Normal file
View file

@ -0,0 +1,2 @@
[vps]
bbb ansible_host=YOUR.VPS.IP ansible_user=ansible

View file

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
NFT=/usr/sbin/nft
RULES=/etc/nftables/hostfilter.nft
# Delete only our table if present; do NOT flush whole ruleset (Docker owns its rules)
$NFT delete table inet hostfilter 2>/dev/null || true
$NFT -f "$RULES"

View file

@ -0,0 +1,31 @@
---
- name: reload sshd
ansible.builtin.service:
name: ssh
state: reloaded
- name: restart fail2ban
ansible.builtin.service:
name: fail2ban
state: restarted
- name: restart journald
ansible.builtin.service:
name: systemd-journald
state: restarted
- name: reload sysctl
ansible.builtin.command: sysctl --system
changed_when: false
- name: restart docker
ansible.builtin.systemd:
name: docker
state: restarted
daemon_reload: true
- name: restart hostfilter firewall
ansible.builtin.systemd:
name: hostfilter-nft
state: restarted
daemon_reload: true

View file

@ -0,0 +1,289 @@
---
- name: Assert Ubuntu 22.04
ansible.builtin.assert:
that:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version is version("22.04", "==")
fail_msg: "Ce playbook cible Ubuntu 22.04 LTS uniquement."
- name: Set hostname to FQDN
ansible.builtin.hostname:
name: "{{ fqdn }}"
- name: Install base packages
ansible.builtin.apt:
update_cache: true
name:
- ca-certificates
- curl
- gnupg
- sudo
- vim
- jq
- unzip
- chrony
- rsyslog
- openssh-server
- fail2ban
- unattended-upgrades
- apt-listchanges
- logrotate
- apparmor
- apparmor-utils
- nftables
- locales
- lsb-release
state: present
# BBB 3.x install guide expects en_US.UTF-8
- name: Ensure en_US.UTF-8 locale exists
ansible.builtin.command: locale-gen en_US.UTF-8
changed_when: false
- name: Set default locale
ansible.builtin.copy:
dest: /etc/default/locale
owner: root
group: root
mode: "0644"
content: |
LANG="en_US.UTF-8"
- name: Ensure admin user exists
ansible.builtin.user:
name: "{{ admin_user }}"
groups: sudo
append: true
shell: /bin/bash
create_home: true
state: present
- name: Install admin authorized keys
ansible.builtin.authorized_key:
user: "{{ admin_user }}"
key: "{{ item }}"
state: present
loop: "{{ admin_pubkeys }}"
- name: Harden sudoers for admin (NOPASSWD)
ansible.builtin.copy:
dest: "/etc/sudoers.d/90-{{ admin_user }}"
content: "{{ admin_user }} ALL=(ALL) NOPASSWD:ALL\n"
owner: root
group: root
mode: "0440"
# --- SSH hardening ---
- name: Template sshd_config
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: "0600"
notify: reload sshd
- name: Validate sshd configuration
ansible.builtin.command: sshd -t
changed_when: false
# --- journald persistence ---
- name: Configure journald
ansible.builtin.template:
src: journald.conf.j2
dest: /etc/systemd/journald.conf
owner: root
group: root
mode: "0644"
notify: restart journald
- name: Ensure journald persistent directory exists
ansible.builtin.file:
path: /var/log/journal
state: directory
owner: root
group: systemd-journal
mode: "2755"
when: journald_persistent | bool
# --- sysctl hardening (incl. ip_forward for Docker) ---
- name: Apply sysctl hardening
ansible.builtin.template:
src: sysctl-hardening.conf.j2
dest: /etc/sysctl.d/99-hardening.conf
owner: root
group: root
mode: "0644"
notify: reload sysctl
# --- Unattended upgrades ---
- name: Configure 20auto-upgrades
ansible.builtin.template:
src: 20auto-upgrades.j2
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: "0644"
when: enable_unattended_upgrades | bool
- name: Configure 50unattended-upgrades
ansible.builtin.template:
src: 50unattended-upgrades.j2
dest: /etc/apt/apt.conf.d/50unattended-upgrades
owner: root
group: root
mode: "0644"
when: enable_unattended_upgrades | bool
# --- fail2ban (nftables action) ---
- name: Configure fail2ban jail.local
ansible.builtin.template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: "0644"
when: enable_fail2ban | bool
notify: restart fail2ban
- name: Ensure fail2ban enabled
ansible.builtin.service:
name: fail2ban
enabled: true
state: started
when: enable_fail2ban | bool
# --- Docker CE (repo officiel) ---
- name: Map architecture for Docker repo
ansible.builtin.set_fact:
docker_arch: >-
{{
{'x86_64':'amd64','aarch64':'arm64','armv7l':'armhf'}.get(ansible_architecture, 'amd64')
}}
when: docker_install | bool
- name: Add Docker GPG key (keyring)
ansible.builtin.shell: |
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
args:
creates: /etc/apt/keyrings/docker.gpg
when: docker_install | bool
- name: Add Docker apt repo
ansible.builtin.apt_repository:
repo: "deb [arch={{ docker_arch }} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
filename: docker
state: present
when: docker_install | bool
- name: Install Docker CE packages
ansible.builtin.apt:
update_cache: true
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
when: docker_install | bool
- name: Configure Docker daemon.json
ansible.builtin.template:
src: docker-daemon.json.j2
dest: /etc/docker/daemon.json
owner: root
group: root
mode: "0644"
when: docker_install | bool
notify: restart docker
- name: Enable and start Docker
ansible.builtin.service:
name: docker
enabled: true
state: started
when: docker_install | bool
- name: Optionally add admin user to docker group (NOT recommended)
ansible.builtin.user:
name: "{{ admin_user }}"
groups: docker
append: true
when: docker_install | bool and docker_add_admin_to_group | bool
# --- Host firewall (nftables policy layer; Docker keeps its own rules) ---
- name: Install apply script for nftables hostfilter
ansible.builtin.copy:
src: apply-hostfilter-nft.sh
dest: /usr/local/sbin/apply-hostfilter-nft
owner: root
group: root
mode: "0755"
- name: Deploy nftables rules (hostfilter table)
ansible.builtin.template:
src: hostfilter.nft.j2
dest: /etc/nftables/hostfilter.nft
owner: root
group: root
mode: "0644"
notify: restart hostfilter firewall
- name: Deploy systemd unit for hostfilter firewall
ansible.builtin.template:
src: hostfilter-nft.service.j2
dest: /etc/systemd/system/hostfilter-nft.service
owner: root
group: root
mode: "0644"
notify: restart hostfilter firewall
- name: Enable and start hostfilter firewall
ansible.builtin.systemd:
name: hostfilter-nft
enabled: true
state: started
daemon_reload: true
# Ensure fqdn -> public IPv4 mapping
- name: Ensure /etc/hosts has fqdn -> public IPv4 mapping
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '^\s*{{ ansible_default_ipv4.address | regex_escape() }}\s+{{ fqdn | regex_escape() }}\s*$'
line: "{{ ansible_default_ipv4.address }} {{ fqdn }}"
state: present
# --- Swap (useful with 12G RAM) ---
- name: Create swapfile
ansible.builtin.command: "fallocate -l {{ swapfile_size_mb }}M {{ swapfile_path }}"
args:
creates: "{{ swapfile_path }}"
when: manage_swapfile | bool
- name: Set swapfile permissions
ansible.builtin.file:
path: "{{ swapfile_path }}"
owner: root
group: root
mode: "0600"
when: manage_swapfile | bool
- name: Make swap
ansible.builtin.command: "mkswap {{ swapfile_path }}"
when: manage_swapfile | bool
changed_when: false
- name: Enable swap
ansible.builtin.command: "swapon {{ swapfile_path }}"
when: manage_swapfile | bool
failed_when: false
- name: Persist swap in fstab
ansible.builtin.lineinfile:
path: /etc/fstab
line: "{{ swapfile_path }} none swap sw 0 0"
state: present
when: manage_swapfile | bool

View file

@ -0,0 +1,5 @@
// Managed by Ansible
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";

View file

@ -0,0 +1,10 @@
// Managed by Ansible
Unattended-Upgrade::Origins-Pattern {
"origin=Ubuntu,codename=${distro_codename}-security";
"origin=Ubuntu,codename=${distro_codename}-updates";
};
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::SyslogEnable "true";

View file

@ -0,0 +1,7 @@
{
"ip-forward-no-drop": true,
"log-driver": "local",
"log-opts": { "max-size": "50m", "max-file": "3" },
"live-restore": true,
"userland-proxy": false
}

View file

@ -0,0 +1,12 @@
[Unit]
Description=Host firewall (nftables) - hostfilter table
Wants=network-online.target
After=network-online.target docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/apply-hostfilter-nft
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,81 @@
#!/usr/sbin/nft -f
define EXT_IF = "{{ ansible_default_ipv4.interface }}"
table inet hostfilter {
set ssh_allow_v4 {
type ipv4_addr
flags interval
elements = { {% for c in ssh_allow_cidrs_v4 %} {{ c }}{% if not loop.last %}, {% endif %}{% endfor %} }
}
set ssh_allow_v6 {
type ipv6_addr
flags interval
elements = { {% for c in ssh_allow_cidrs_v6 %} {{ c }}{% if not loop.last %}, {% endif %}{% endfor %} }
}
chain input {
type filter hook input priority 0; policy drop;
ct state invalid drop
ct state { established, related } accept
iif "lo" accept
# Docker internal -> host (avoid breaking container->host access)
iifname { "docker0", "br-*" } accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# SSH
tcp dport {{ ssh_port }} ip saddr @ssh_allow_v4 accept
tcp dport {{ ssh_port }} ip6 saddr @ssh_allow_v6 accept
# BBB
tcp dport { 80, 443 } accept
udp dport {{ bbb_udp_min }}-{{ bbb_udp_max }} accept
{% if turn_enabled | bool %}
# TURN cohabitation (no 443)
udp dport {{ turn_listen_port }} accept
tcp dport {{ turn_listen_port }} accept
tcp dport {{ turn_tls_port }} accept
udp dport {{ turn_relay_udp_min }}-{{ turn_relay_udp_max }} accept
{% endif %}
limit rate 10/second burst 20 packets counter log prefix "nft-drop-in: " flags all drop
}
chain forward {
# Run early; Docker keeps its chains, we enforce policy safely
type filter hook forward priority -100; policy drop;
ct state invalid drop
ct state { established, related } accept
# Allow Docker internal forwarding (critical)
iifname { "docker0", "br-*" } oifname { "docker0", "br-*" } accept
# Allow containers to reach the Internet
iifname { "docker0", "br-*" } oifname $EXT_IF accept
# Allow Internet -> published container ports we expect (guardrail)
iifname $EXT_IF oifname { "docker0", "br-*" } tcp dport { 80, 443 } accept
iifname $EXT_IF oifname { "docker0", "br-*" } udp dport {{ bbb_udp_min }}-{{ bbb_udp_max }} accept
{% if turn_enabled | bool %}
iifname $EXT_IF oifname { "docker0", "br-*" } udp dport {{ turn_listen_port }} accept
iifname $EXT_IF oifname { "docker0", "br-*" } tcp dport { {{ turn_listen_port }}, {{ turn_tls_port }} } accept
iifname $EXT_IF oifname { "docker0", "br-*" } udp dport {{ turn_relay_udp_min }}-{{ turn_relay_udp_max }} accept
{% endif %}
limit rate 10/second burst 20 packets counter log prefix "nft-drop-fwd: " flags all drop
}
chain output {
type filter hook output priority 0; policy accept;
}
}

View file

@ -0,0 +1,12 @@
# Managed by Ansible
[DEFAULT]
backend = systemd
banaction = nftables-multiport
findtime = 10m
bantime = 1h
maxretry = 5
[sshd]
enabled = true
port = {{ ssh_port }}

View file

@ -0,0 +1,9 @@
# Managed by Ansible
[Journal]
Storage={{ "persistent" if journald_persistent else "auto" }}
Compress=yes
SystemMaxUse=500M
RuntimeMaxUse=200M
MaxRetentionSec=1month
RateLimitIntervalSec=30s
RateLimitBurst=10000

View file

@ -0,0 +1,30 @@
# Managed by Ansible
Port {{ ssh_port }}
Protocol 2
PermitRootLogin {{ permit_root_login }}
PasswordAuthentication {{ "no" if disable_password_auth else "yes" }}
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
PubkeyAuthentication yes
AuthenticationMethods publickey
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
PermitUserEnvironment no
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 4
LogLevel VERBOSE
{% if ssh_allow_users is defined and ssh_allow_users|length > 0 %}
AllowUsers {% for u in ssh_allow_users %}{{ u }} {% endfor %}
{% endif %}

View file

@ -0,0 +1,30 @@
# Managed by Ansible
# Network hardening + Docker needs forwarding
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.randomize_va_space = 2
vm.swappiness = 10

7
site.yml Normal file
View file

@ -0,0 +1,7 @@
---
- name: Prepare Ubuntu 22.04 VPS for BBB (nftables + hardening + Docker)
hosts: vps
become: true
gather_facts: true
roles:
- bbb_vps_prep