--- - 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') }}"