railiance-infra/ansible/roles/base/tasks/main.yml
codex c84fe7a3de
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Make the k3s API firewall allowlist declarative
The live host restricted 6443/tcp to specific operator addresses, added by hand,
while this role still declared the port open to Anywhere with no source
restriction. The declared config was weaker than reality: running the base role
would have REMOVED the restriction and exposed the Kubernetes API to the
internet. Security was tightened on the host and never fed back into the source
of truth.

Found 2026-08-11 while diagnosing lost cluster access, which turned out to be an
ISP lease rotation (89.244.90.246 -> .236) against a hand-maintained allowlist.

Changes:
- defaults: k3s_api_allowed_sources (empty = 6443 closed to all external
  sources, the safe failure; SSH unaffected so the host stays recoverable) and
  k3s_api_revoked_sources, so rotated addresses are pruned rather than left as
  standing grants to whoever the ISP reassigns them to
- tasks: grant approved sources, then remove any blanket rule, then revoke
  retired ones. Order matters - grants are added before the blanket rule is
  deleted so convergence never opens a window with no API access
- group_vars/all.yaml: the current operator address, plus the two stale grants
  (.246 rotated, 85.132.220.102 historic) marked for revocation
- docs/verification.md: state that 6443 is source-restricted rather than
  listing it as a plainly allowed port

Not yet converged against the live host - the role change is committed but
running it is a production action needing operator approval.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:56:28 +02:00

147 lines
3.8 KiB
YAML

---
- name: Ensure base packages
ansible.builtin.package:
name:
- apt-transport-https
- ca-certificates
- curl
- git
- vim
- ufw
- fail2ban
- python3
- python3-venv
state: present
update_cache: true
- name: Harden SSH
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/10-hardening.conf
owner: root
group: root
mode: '0644'
content: |
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted
- name: Ensure .ssh directory exists for ops_bridge_user
ansible.builtin.file:
path: "/home/{{ ops_bridge_user | default('tegwick') }}/.ssh"
state: directory
owner: "{{ ops_bridge_user | default('tegwick') }}"
group: "{{ ops_bridge_user | default('tegwick') }}"
mode: '0700'
- name: Inject ops-bridge public key into authorized_keys
ansible.posix.authorized_key:
user: "{{ ops_bridge_user | default('tegwick') }}"
key: "{{ ops_bridge_pubkey }}"
comment: "ops-bridge@{{ inventory_hostname }}"
state: present
when: ops_bridge_pubkey is defined and ops_bridge_pubkey | length > 0
- name: Configure UFW default incoming policy
ansible.builtin.ufw:
state: enabled
policy: deny
direction: incoming
- name: Allow UFW routing (required for k3s flannel pod networking)
ansible.builtin.ufw:
policy: allow
direction: routed
- name: Allow SSH in UFW
ansible.builtin.ufw:
rule: allow
name: OpenSSH
# 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 k3s API from approved operator sources only
ansible.builtin.ufw:
rule: allow
port: '6443'
proto: tcp
from_ip: "{{ item.address }}"
comment: "{{ item.comment | default('k3s-api-operator') }}"
loop: "{{ k3s_api_allowed_sources }}"
loop_control:
label: "{{ item.address }}"
- name: Remove blanket k3s API rule if present (must not be world-reachable)
ansible.builtin.ufw:
rule: allow
port: '6443'
proto: tcp
delete: true
- name: Revoke k3s API access for retired operator sources
ansible.builtin.ufw:
rule: allow
port: '6443'
proto: tcp
from_ip: "{{ item.address }}"
delete: true
loop: "{{ k3s_api_revoked_sources }}"
loop_control:
label: "{{ item.address }}"
- name: Warn when no operator source is allowed to reach the k3s API
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
- name: Allow Flannel VXLAN in UFW
ansible.builtin.ufw:
rule: allow
port: '8472'
proto: udp
- name: Enable fail2ban
ansible.builtin.service:
name: fail2ban
state: started
enabled: true
- name: Configure fail2ban SSH jail
ansible.builtin.copy:
dest: /etc/fail2ban/jail.d/sshd.conf
owner: root
group: root
mode: '0644'
content: |
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 5
bantime = 3600
findtime = 600
notify: Restart fail2ban
- name: Set HISTCONTROL to ignorespace
ansible.builtin.copy:
dest: /etc/profile.d/histcontrol.sh
owner: root
group: root
mode: '0644'
content: |
export HISTCONTROL=ignorespace
- name: Set timezone
community.general.timezone:
name: "{{ timezone | default('UTC') }}"