Close RAIL-HO-WP-0009 declared-state gaps; leave live 6443 prune gated
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:
parent
434121be99
commit
4d9e77c968
29 changed files with 793 additions and 99 deletions
|
|
@ -1,6 +1,12 @@
|
|||
---
|
||||
# Base role defaults.
|
||||
|
||||
# When false, this role will not enable or rewrite UFW. Use that for hosts
|
||||
# whose live packet filter is not UFW (CoulombCore: iptables INPUT DROP with
|
||||
# a Plesk-era accept list). Enabling UFW there is an availability decision,
|
||||
# not a side effect of an unrelated converge.
|
||||
ufw_manage: true
|
||||
|
||||
# Source addresses permitted to reach the k3s API (6443/tcp).
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
|
|
@ -10,21 +16,45 @@
|
|||
# config WEAKER than reality: re-running this role would have removed the
|
||||
# restriction and exposed the Kubernetes API to the internet. Found 2026-08-11.
|
||||
#
|
||||
# The allowlist is therefore declared here and converged, not hand-edited.
|
||||
# Operator addresses rotate (dynamic ISP leases). An allowlist is a treadmill:
|
||||
# each rotation is either an outage or a stale grant to whoever the ISP
|
||||
# reassigns the address to. RAIL-HO-WP-0009-T04 therefore keeps this list
|
||||
# empty. Reach the API over the ops-bridge SSH tunnel
|
||||
# (`k3s-api-railiance01`, local port 16444; `k3s-api-coulombcore`, 16443).
|
||||
# See docs/adr/ADR-005-k3s-api-tunnel-only.md.
|
||||
#
|
||||
# Deliberately empty by default. A host that sets no sources gets NO public
|
||||
# access to 6443 — which is the safe failure. SSH (22) is unaffected, so a host
|
||||
# converged with an empty list is always recoverable.
|
||||
#
|
||||
# Set the real values in inventory/group_vars/all.yaml. Each entry:
|
||||
# Each entry, if any:
|
||||
# - address: "203.0.113.10"
|
||||
# comment: "k3s-api-operator-workstation"
|
||||
k3s_api_allowed_sources: []
|
||||
|
||||
# Source addresses whose k3s API access must be REMOVED on convergence.
|
||||
#
|
||||
# Operator addresses rotate (dynamic ISP leases). Without this, every rotation
|
||||
# leaves a standing grant to an address the ISP has since reassigned to someone
|
||||
# else. Move an address here when it stops being yours; convergence then prunes
|
||||
# it rather than leaving it to accumulate.
|
||||
# Move an address here when it stops being yours (or when the public
|
||||
# allowlist is retired); convergence then prunes it.
|
||||
k3s_api_revoked_sources: []
|
||||
|
||||
# Source addresses permitted to send Flannel VXLAN (8472/udp).
|
||||
#
|
||||
# Empty by default. A single-node cluster does not need a public VXLAN
|
||||
# grant; adding an unrestricted 8472/udp allow would expose the pod network
|
||||
# to injection. Set this to the other nodes' addresses only when the cluster
|
||||
# becomes multi-node (RAIL-BS-WP-0007 / ThreePhoenix HA).
|
||||
#
|
||||
# Each entry:
|
||||
# - address: "203.0.113.20"
|
||||
# comment: "flannel-vxlan-peer"
|
||||
flannel_vxlan_allowed_sources: []
|
||||
|
||||
# Extra UFW allows that are not k3s. Used for provider agents that must stay
|
||||
# reachable (HostEurope Nydus on 2224/tcp). Empty by default so a Hetzner
|
||||
# host does not inherit a HostEurope-only hole.
|
||||
#
|
||||
# Each entry:
|
||||
# - port: "2224"
|
||||
# proto: tcp
|
||||
# comment: "nydus-ex-api dashboard agent"
|
||||
ufw_extra_allowed: []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
---
|
||||
- name: Restart sshd
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: Restart fail2ban
|
||||
ansible.builtin.service:
|
||||
name: fail2ban
|
||||
|
|
|
|||
|
|
@ -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') }}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue