security: drop all CBC TLS suites (Qualys WEAK -> AEAD-only, still A+); sync ansible nginx templates (ciphers + ywxi CSP); capture host firewall as IaC
This commit is contained in:
parent
113c73b392
commit
695c3e2431
17 changed files with 162 additions and 12 deletions
|
|
@ -86,3 +86,15 @@ backups moved to `/etc/nginx/backups/` (NOT in an include path).
|
||||||
- **Hosted in a SOC 2 Type II compliant data center**
|
- **Hosted in a SOC 2 Type II compliant data center**
|
||||||
TODO: TrustedSite (ex-McAfee SECURE) free tier needs a signup to get the
|
TODO: TrustedSite (ex-McAfee SECURE) free tier needs a signup to get the
|
||||||
daily-scan trustmark image - add later if an image seal is wanted.
|
daily-scan trustmark image - add later if an image seal is wanted.
|
||||||
|
|
||||||
|
### TLS cipher: removed all CBC suites (2026-06-06)
|
||||||
|
Qualys flagged the two remaining CBC suites as WEAK:
|
||||||
|
`TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384` (0xc024) and
|
||||||
|
`TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256` (0xc023). CBC modes carry the historic
|
||||||
|
padding-oracle risk; every modern client supports AEAD, so they were dropped.
|
||||||
|
Final cipher list = AEAD only: GCM (AES-128/256) + CHACHA20-POLY1305 (TLS 1.2)
|
||||||
|
plus TLS 1.3 suites. Verified: CBC no longer negotiates, GCM/TLS1.3 work, site
|
||||||
|
200, **Qualys A+ with WEAK suites: NONE**. The cipher list + the cdn.ywxi.net CSP
|
||||||
|
addition are now in the ansible templates (`infra/ansible/roles/nginx/templates/`)
|
||||||
|
so they don't drift on the next ansible run. Firewall captured as IaC in
|
||||||
|
`infra/firewall/`.
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/analytics.performancewest.net/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/analytics.performancewest.net/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/analytics.performancewest.net/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/analytics.performancewest.net/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/api.performancewest.net/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/api.performancewest.net/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/api.performancewest.net/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/api.performancewest.net/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ shkeeper_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ shkeeper_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ shkeeper_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ shkeeper_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ shkeeper_admin_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ shkeeper_admin_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ shkeeper_admin_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ shkeeper_admin_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ dev_api_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ dev_api_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ dev_api_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ dev_api_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ dev_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ dev_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ dev_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ dev_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ listmonk_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ listmonk_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ listmonk_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ listmonk_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ minio_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ minio_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ minio_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ minio_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
@ -59,7 +59,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ minio_console_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ minio_console_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ minio_console_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ minio_console_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/monitoring.performancewest.net/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/monitoring.performancewest.net/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/monitoring.performancewest.net/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/monitoring.performancewest.net/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/{{ portal_domain }}/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/{{ portal_domain }}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/{{ portal_domain }}/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/{{ portal_domain }}/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com https://cdn.ywxi.net https://analytics.performancewest.net https://embed.tawk.to https://*.tawk.to; style-src 'self' 'unsafe-inline' https://*.tawk.to https://fonts.googleapis.com; img-src 'self' data: https: blob:; font-src 'self' data: https://*.tawk.to https://fonts.gstatic.com; connect-src 'self' https://api.performancewest.net https://api.dev.performancewest.net https://api.stripe.com https://analytics.performancewest.net https://*.tawk.to wss://*.tawk.to; frame-src https://js.stripe.com https://hooks.stripe.com https://cdn.ywxi.net https://*.tawk.to; object-src 'none'; base-uri 'self'; form-action 'self' https://checkout.stripe.com; upgrade-insecure-requests;" always;
|
||||||
|
|
||||||
# Block common attack paths
|
# Block common attack paths
|
||||||
location ~* \.(php|asp|aspx|cgi|pl)$ {
|
location ~* \.(php|asp|aspx|cgi|pl)$ {
|
||||||
return 444;
|
return 444;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ server {
|
||||||
ssl_certificate /etc/letsencrypt/live/performancewest.net/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/performancewest.net/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/performancewest.net/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/performancewest.net/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_session_timeout 10m;
|
ssl_session_timeout 10m;
|
||||||
|
|
|
||||||
39
infra/firewall/README.md
Normal file
39
infra/firewall/README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Host firewall (Infrastructure as Code)
|
||||||
|
|
||||||
|
Canonical copies of the prod app-host firewall (see `docs/vm-security-hardening.md`
|
||||||
|
for the full rationale). These are the source of truth; the live host should
|
||||||
|
match. Installed as a boot-enabled systemd service.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- `pw-firewall.nft` -> `/etc/pw-firewall/pw-firewall.nft`
|
||||||
|
nftables `inet pw_fw` table (input hook, priority -150). Public allow-list
|
||||||
|
`{22, 22022, 80, 443}`, a `trusted_admin` set allow-listing git/forgejo (3022),
|
||||||
|
internal subnets + loopback fully trusted, everything else on `ens18` dropped.
|
||||||
|
- `pw-docker-fw.sh` -> `/usr/local/sbin/pw-docker-fw.sh`
|
||||||
|
Adds DOCKER-USER rules so external (ens18) NEW inbound to Docker-published
|
||||||
|
container ports is dropped (postgres/listmonk/api/forgejo were accidentally
|
||||||
|
on 0.0.0.0); trusted_admin IPs are allow-listed to forgejo first.
|
||||||
|
- `pw-firewall.service` -> `/etc/systemd/system/pw-firewall.service`
|
||||||
|
Applies both at boot (After=docker). Also re-applied on docker restart via
|
||||||
|
`/etc/systemd/system/docker.service.d/pw-firewall.conf` (ExecStartPost).
|
||||||
|
|
||||||
|
## Install / update on the host
|
||||||
|
```
|
||||||
|
sudo install -D -m 0644 pw-firewall.nft /etc/pw-firewall/pw-firewall.nft
|
||||||
|
sudo install -D -m 0755 pw-docker-fw.sh /usr/local/sbin/pw-docker-fw.sh
|
||||||
|
sudo install -D -m 0644 pw-firewall.service /etc/systemd/system/pw-firewall.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now pw-firewall.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add / remove a trusted admin IP (for git push over :3022)
|
||||||
|
```
|
||||||
|
sudo nft add element inet pw_fw trusted_admin { <IP> } # live
|
||||||
|
# then add <IP> to TRUSTED_ADMIN in pw-docker-fw.sh + the set in pw-firewall.nft
|
||||||
|
# and re-run: sudo systemctl restart pw-firewall.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
Roll out with an auto-rollback timer (`setsid sh -c 'sleep 300; nft delete table
|
||||||
|
inet pw_fw; iptables -F DOCKER-USER; ...'`) so a bad rule can't lock you out;
|
||||||
|
cancel it only after verifying SSH + git still work from off-network.
|
||||||
26
infra/firewall/pw-docker-fw.sh
Normal file
26
infra/firewall/pw-docker-fw.sh
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Block external (internet) access to Docker-published container ports.
|
||||||
|
# Host nginx reaches containers over loopback (127.0.0.1), so dropping NEW
|
||||||
|
# inbound from the public uplink (ens18) into the Docker FORWARD path closes
|
||||||
|
# the accidental 0.0.0.0 exposure (postgres 5432, forgejo 3022, listmonk
|
||||||
|
# 9100/9101, api 3001/3002, etc.) without breaking nginx->container or
|
||||||
|
# container->container/internet traffic.
|
||||||
|
set -euo pipefail
|
||||||
|
UPLINK=ens18
|
||||||
|
|
||||||
|
# Trusted admin source IPs allowed to reach the forgejo container (host :3022
|
||||||
|
# DNATs to 172.18.0.2:22, so the post-DNAT dport is 22). Keep in sync with the
|
||||||
|
# nft 'trusted_admin' set in /etc/pw-firewall/pw-firewall.nft.
|
||||||
|
TRUSTED_ADMIN="76.228.206.147"
|
||||||
|
|
||||||
|
# Rebuild DOCKER-USER deterministically.
|
||||||
|
iptables -F DOCKER-USER 2>/dev/null || true
|
||||||
|
iptables -A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
|
||||||
|
# Allow trusted admins to git/forgejo (post-DNAT dport 22) before the drop.
|
||||||
|
for ip in $TRUSTED_ADMIN; do
|
||||||
|
iptables -A DOCKER-USER -i "$UPLINK" -s "$ip" -p tcp --dport 22 -j RETURN
|
||||||
|
done
|
||||||
|
iptables -A DOCKER-USER -i "$UPLINK" -m conntrack --ctstate NEW,INVALID -j DROP
|
||||||
|
iptables -A DOCKER-USER -j RETURN
|
||||||
|
echo "DOCKER-USER rules:"
|
||||||
|
iptables -L DOCKER-USER -n -v --line-numbers
|
||||||
55
infra/firewall/pw-firewall.nft
Normal file
55
infra/firewall/pw-firewall.nft
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/sbin/nft -f
|
||||||
|
# Performance West host firewall.
|
||||||
|
# Goal: expose ONLY public-facing ports to the internet; keep all container /
|
||||||
|
# k3s / loopback traffic fully open so nothing internal breaks.
|
||||||
|
#
|
||||||
|
# Dedicated table (pw_fw) with a high-priority (-150) input hook so it is
|
||||||
|
# evaluated BEFORE kube-router's ACCEPT chain in the standard 'filter' table.
|
||||||
|
# fail2ban (f2b-table) stays untouched.
|
||||||
|
|
||||||
|
table inet pw_fw
|
||||||
|
delete table inet pw_fw
|
||||||
|
|
||||||
|
table inet pw_fw {
|
||||||
|
set pub_tcp {
|
||||||
|
type inet_service
|
||||||
|
# Public-facing ports only.
|
||||||
|
elements = { 22, 22022, 80, 443 }
|
||||||
|
# NB: 22 kept too in case provider/console uses it; SSH is on 22022.
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trusted admin source IPs allowed to reach git/forgejo (3022) and other
|
||||||
|
# non-public admin ports. Update with: nft add element inet pw_fw trusted_admin { <ip> }
|
||||||
|
set trusted_admin {
|
||||||
|
type ipv4_addr
|
||||||
|
flags interval
|
||||||
|
elements = { 76.228.206.147 }
|
||||||
|
}
|
||||||
|
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority -150; policy accept;
|
||||||
|
|
||||||
|
# 1) Always allow loopback + already-established flows.
|
||||||
|
iif "lo" accept
|
||||||
|
ct state established,related accept
|
||||||
|
ct state invalid drop
|
||||||
|
|
||||||
|
# 2) Fully trust internal networks (docker bridges, flannel pod/service,
|
||||||
|
# cni0). Containers and k3s must keep talking unrestricted.
|
||||||
|
ip saddr { 127.0.0.0/8, 172.16.0.0/12, 10.42.0.0/16, 10.43.0.0/16 } accept
|
||||||
|
iifname { "docker0", "br-*", "cni0", "flannel.1", "kube-*", "veth*", "cilium*" } accept
|
||||||
|
|
||||||
|
# 3) Allow ICMP (ping / path MTU) - safe and useful for diagnostics.
|
||||||
|
ip protocol icmp accept
|
||||||
|
ip6 nexthdr icmpv6 accept
|
||||||
|
|
||||||
|
# 4) Trusted-admin allow-list for git/forgejo SSH (3022).
|
||||||
|
ip saddr @trusted_admin tcp dport 3022 accept
|
||||||
|
|
||||||
|
# 5) Public allow-list (new inbound from the internet).
|
||||||
|
tcp dport @pub_tcp accept
|
||||||
|
|
||||||
|
# 6) Everything else arriving on the public uplink is dropped.
|
||||||
|
iifname "ens18" drop
|
||||||
|
}
|
||||||
|
}
|
||||||
15
infra/firewall/pw-firewall.service
Normal file
15
infra/firewall/pw-firewall.service
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Performance West host firewall (nft input + DOCKER-USER egress-only)
|
||||||
|
After=docker.service nftables.service network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=/usr/sbin/nft -f /etc/pw-firewall/pw-firewall.nft
|
||||||
|
ExecStart=/usr/local/sbin/pw-docker-fw.sh
|
||||||
|
ExecReload=/usr/sbin/nft -f /etc/pw-firewall/pw-firewall.nft
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Add table
Add a link
Reference in a new issue