railiance-infra/ansible/roles/base/tasks/main.yml

252 lines
7.4 KiB
YAML
Raw Normal View History

---
- name: Require the executable baseline contract
tags: [base, baseline]
ansible.builtin.assert:
that:
- baseline_required_packages is defined
- baseline_ssh_directives is defined
- baseline_user is defined
- baseline_security is defined
- baseline_firewall is defined
- ufw_manage == baseline_firewall.managed
fail_msg: >-
Resolve a baseline_profile from spec/server-baseline.yaml through the
dynamic inventory before running this role.
- name: Ensure base packages
tags: [base, packages]
ansible.builtin.package:
name: "{{ baseline_required_packages }}"
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: |
{% for directive in baseline_ssh_directives | dict2items %}
{{ directive.key }} {{ directive.value }}
{% endfor %}
notify: Restart sshd
- name: Ensure baseline operator user exists
tags: [base, user]
ansible.builtin.user:
name: "{{ baseline_user.name }}"
state: present
shell: "{{ baseline_user.shell }}"
create_home: true
- name: Ensure declared passwordless sudo posture
tags: [base, user, sudo]
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ baseline_user.name }}"
owner: root
group: root
mode: '0440'
content: "{{ baseline_user.name }} ALL=(ALL) {{ baseline_user.sudo }}:ALL\n"
validate: /usr/sbin/visudo -cf %s
- 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.
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
- name: Allow k3s API from approved operator sources only
tags: [base, firewall, ufw]
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
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
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
- 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
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
delete: true
when: ufw_manage | bool
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
- name: Revoke k3s API access for retired operator sources
tags: [base, firewall, ufw]
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
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
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
- name: Warn when no operator source is allowed to reach the k3s API
tags: [base, firewall, ufw]
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
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 declared fail2ban jails
tags: [base, fail2ban]
ansible.builtin.copy:
dest: "/etc/fail2ban/jail.d/{{ item }}.conf"
owner: root
group: root
mode: '0644'
content: |
[{{ item }}]
enabled = true
port = {{ 'ssh' if item == 'sshd' else item }}
filter = {{ item }}
maxretry = 5
bantime = 3600
findtime = 600
loop: "{{ baseline_security.fail2ban_jails }}"
notify: Restart fail2ban
- name: Set declared HISTCONTROL
tags: [base, histcontrol]
ansible.builtin.copy:
dest: /etc/profile.d/histcontrol.sh
owner: root
group: root
mode: '0644'
content: |
export HISTCONTROL={{ baseline_security.histcontrol }}
- name: Set timezone
tags: [base, timezone]
community.general.timezone:
name: "{{ timezone | default('UTC') }}"