Close RAIL-HO-WP-0009 declared-state gaps; leave live 6443 prune gated
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Make the k3s API tunnel-only (ADR-005), stop declaring Flannel VXLAN
open to Anywhere, tag the base role so firewall can be scoped, and
schedule the Goss declared-vs-live check. CoulombCore sets ufw_manage
false so a converge cannot enable UFW there. T02 still needs operator
approval for make converge-firewall HOST=Railiance01.
This commit is contained in:
codex 2026-08-15 15:41:59 +02:00
parent 434121be99
commit 4d9e77c968
29 changed files with 793 additions and 99 deletions

View file

@ -1,5 +1,6 @@
---
- name: Ensure base packages
tags: [base, packages]
ansible.builtin.package:
name:
- apt-transport-https
@ -15,6 +16,7 @@
update_cache: true
- name: Harden SSH
tags: [base, ssh]
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/10-hardening.conf
owner: root
@ -24,13 +26,10 @@
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted
notify: Restart sshd
- name: Ensure .ssh directory exists for ops_bridge_user
tags: [base, ssh]
ansible.builtin.file:
path: "/home/{{ ops_bridge_user | default('tegwick') }}/.ssh"
state: directory
@ -39,6 +38,7 @@
mode: '0700'
- name: Inject ops-bridge public key into authorized_keys
tags: [base, ssh]
ansible.posix.authorized_key:
user: "{{ ops_bridge_user | default('tegwick') }}"
key: "{{ ops_bridge_pubkey }}"
@ -47,27 +47,46 @@
when: ops_bridge_pubkey is defined and ops_bridge_pubkey | length > 0
- name: Configure UFW default incoming policy
tags: [base, firewall, ufw]
ansible.builtin.ufw:
state: enabled
policy: deny
direction: incoming
when: ufw_manage | bool
- name: Allow UFW routing (required for k3s flannel pod networking)
- name: Allow UFW routing when VXLAN peers are declared
tags: [base, firewall, ufw]
ansible.builtin.ufw:
policy: allow
direction: routed
when: ufw_manage | bool and (flannel_vxlan_allowed_sources | length > 0)
- name: Allow SSH in UFW
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
name: OpenSSH
when: ufw_manage | bool
# k3s API access is source-restricted. See roles/base/defaults/main.yml for why
# this is declared rather than hand-applied. Order matters below: grants are
# added BEFORE the blanket rule is removed, so convergence never opens a window
# in which the operator cannot reach the API.
- name: Allow declared extra UFW ports
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
port: "{{ item.port }}"
proto: "{{ item.proto | default('tcp') }}"
comment: "{{ item.comment | default('extra-allow') }}"
loop: "{{ ufw_extra_allowed }}"
loop_control:
label: "{{ item.port }}/{{ item.proto | default('tcp') }}"
when: ufw_manage | bool
# k3s API access is source-restricted and empty by default (tunnel-only).
# See roles/base/defaults/main.yml and docs/adr/ADR-005-k3s-api-tunnel-only.md.
# Order matters: remaining grants (if any) are added BEFORE the blanket rule
# is removed, so a non-empty allowlist never opens a window without API access.
- name: Allow k3s API from approved operator sources only
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
port: '6443'
@ -77,15 +96,19 @@
loop: "{{ k3s_api_allowed_sources }}"
loop_control:
label: "{{ item.address }}"
when: ufw_manage | bool
- name: Remove blanket k3s API rule if present (must not be world-reachable)
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
port: '6443'
proto: tcp
delete: true
when: ufw_manage | bool
- name: Revoke k3s API access for retired operator sources
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
port: '6443'
@ -95,29 +118,49 @@
loop: "{{ k3s_api_revoked_sources }}"
loop_control:
label: "{{ item.address }}"
when: ufw_manage | bool
- name: Warn when no operator source is allowed to reach the k3s API
tags: [base, firewall, ufw]
ansible.builtin.debug:
msg: >-
k3s_api_allowed_sources is empty, so 6443/tcp is closed to all external
sources on this host. This is the safe default, not an error. SSH is
unaffected and the host remains recoverable. Set the allowlist in
inventory/group_vars/all.yaml to restore API access.
when: k3s_api_allowed_sources | length == 0
sources on this host. Reach the API over the ops-bridge tunnel
(k3s-api-railiance01 on 16444, k3s-api-coulombcore on 16443). SSH is
unaffected and the host remains recoverable.
when: ufw_manage | bool and (k3s_api_allowed_sources | length == 0)
- name: Allow Flannel VXLAN in UFW
- name: Allow Flannel VXLAN from declared cluster peers only
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
port: '8472'
proto: udp
from_ip: "{{ item.address }}"
comment: "{{ item.comment | default('flannel-vxlan-peer') }}"
loop: "{{ flannel_vxlan_allowed_sources }}"
loop_control:
label: "{{ item.address }}"
when: ufw_manage | bool
- name: Remove blanket Flannel VXLAN rule if present (must not be world-reachable)
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
port: '8472'
proto: udp
delete: true
when: ufw_manage | bool
- name: Enable fail2ban
tags: [base, fail2ban]
ansible.builtin.service:
name: fail2ban
state: started
enabled: true
- name: Configure fail2ban SSH jail
tags: [base, fail2ban]
ansible.builtin.copy:
dest: /etc/fail2ban/jail.d/sshd.conf
owner: root
@ -134,6 +177,7 @@
notify: Restart fail2ban
- name: Set HISTCONTROL to ignorespace
tags: [base, histcontrol]
ansible.builtin.copy:
dest: /etc/profile.d/histcontrol.sh
owner: root
@ -143,5 +187,6 @@
export HISTCONTROL=ignorespace
- name: Set timezone
tags: [base, timezone]
community.general.timezone:
name: "{{ timezone | default('UTC') }}"