railiance-infra/ansible/roles/base/tasks/main.yml
codex 61c6082ea8
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Gate reef web ports on exposure grants
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
2026-08-22 12:34:25 +02:00

227 lines
6.4 KiB
YAML

---
- name: Ensure base packages
tags: [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
tags: [base, 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
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
owner: "{{ ops_bridge_user | default('tegwick') }}"
group: "{{ ops_bridge_user | default('tegwick') }}"
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 }}"
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
tags: [base, firewall, ufw]
ansible.builtin.ufw:
state: enabled
policy: deny
direction: incoming
when: ufw_manage | bool
- 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
- name: Reject web ports in the generic UFW exception list
tags: [base, firewall, ufw]
ansible.builtin.assert:
that:
- (ufw_extra_allowed | selectattr('port', 'defined') | map(attribute='port') | map('string') | intersect(['80', '443'])) | length == 0
fail_msg: >-
Ports 80/443 must use public_web_ports plus an ADR-0008 reef declaration;
they cannot be smuggled through ufw_extra_allowed.
when: ufw_manage | bool
- name: Validate public web ports against the reef declaration
tags: [base, firewall, ufw]
become: false
delegate_to: localhost
ansible.builtin.command:
argv:
- python3
- "{{ role_path }}/../../../scripts/validate-reef-exposure.py"
- --reef-declaration
- "{{ reef_declaration_path }}"
- --ports
- "{{ public_web_ports | join(',') }}"
changed_when: false
when: ufw_manage | bool and (public_web_ports | length > 0)
- name: Allow grant-backed public web ports
tags: [base, firewall, ufw]
ansible.builtin.ufw:
rule: allow
port: "{{ item | string }}"
proto: tcp
comment: "adr-0008-reef-public-web"
loop: "{{ public_web_ports }}"
when: ufw_manage | bool
- 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'
proto: tcp
from_ip: "{{ item.address }}"
comment: "{{ item.comment | default('k3s-api-operator') }}"
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'
proto: tcp
from_ip: "{{ item.address }}"
delete: true
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. 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 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
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
tags: [base, histcontrol]
ansible.builtin.copy:
dest: /etc/profile.d/histcontrol.sh
owner: root
group: root
mode: '0644'
content: |
export HISTCONTROL=ignorespace
- name: Set timezone
tags: [base, timezone]
community.general.timezone:
name: "{{ timezone | default('UTC') }}"