--- # Common baseline applied to both prod hosts: Docker engine, a non-sudo deploy # service account, SSH hardening, a default-deny firewall, fail2ban, unattended # security upgrades and time sync. Every task is idempotent. - name: Install base packages ansible.builtin.apt: name: - ca-certificates - curl - gnupg - ufw - fail2ban - unattended-upgrades - chrony state: present update_cache: true cache_valid_time: 3600 # --- Docker engine (official repo; trixie is published upstream) --------------- - name: Create apt keyring directory ansible.builtin.file: path: /etc/apt/keyrings state: directory mode: "0755" - name: Install Docker apt GPG key ansible.builtin.get_url: url: https://download.docker.com/linux/debian/gpg dest: /etc/apt/keyrings/docker.asc mode: "0644" - name: Add Docker apt repository ansible.builtin.apt_repository: repo: >- deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable filename: docker state: present - name: Install Docker engine and the compose plugin ansible.builtin.apt: name: - docker-ce - docker-ce-cli - containerd.io - docker-buildx-plugin - docker-compose-plugin state: present update_cache: true - name: Configure the Docker daemon (live-restore + log rotation) ansible.builtin.template: src: daemon.json.j2 dest: /etc/docker/daemon.json mode: "0644" notify: restart docker - name: Enable and start Docker ansible.builtin.service: name: docker enabled: true state: started # --- Deploy service account ---------------------------------------------------- - name: Create the deploy service account ansible.builtin.user: name: "{{ deploy_user }}" groups: docker append: true shell: /bin/bash create_home: true - name: Ensure the deploy .ssh directory ansible.builtin.file: path: "/home/{{ deploy_user }}/.ssh" state: directory owner: "{{ deploy_user }}" group: "{{ deploy_user }}" mode: "0700" - name: Authorize the CI deploy SSH key (exclusive) ansible.builtin.copy: dest: "/home/{{ deploy_user }}/.ssh/authorized_keys" content: "{{ deploy_ci_pubkey }}\n" owner: "{{ deploy_user }}" group: "{{ deploy_user }}" mode: "0600" # --- SSH hardening ------------------------------------------------------------- - name: Harden sshd (key-only auth) ansible.builtin.template: src: sshd-hardening.conf.j2 dest: /etc/ssh/sshd_config.d/10-scrabble-hardening.conf mode: "0644" validate: sshd -t -f %s notify: reload sshd # --- Firewall (default deny incoming) ------------------------------------------ # SSH is allowed before the policy flips so enabling ufw never locks us out. - name: Allow SSH through the firewall community.general.ufw: rule: allow name: OpenSSH - name: Default-deny incoming, allow outgoing community.general.ufw: direction: "{{ item.direction }}" policy: "{{ item.policy }}" loop: - { direction: incoming, policy: deny } - { direction: outgoing, policy: allow } - name: Enable the firewall community.general.ufw: state: enabled # --- fail2ban ------------------------------------------------------------------ - name: Configure the fail2ban sshd jail ansible.builtin.template: src: jail.local.j2 dest: /etc/fail2ban/jail.local mode: "0644" notify: restart fail2ban - name: Enable and start fail2ban ansible.builtin.service: name: fail2ban enabled: true state: started # --- Unattended security upgrades + time sync ---------------------------------- - name: Enable unattended upgrades ansible.builtin.copy: dest: /etc/apt/apt.conf.d/20auto-upgrades mode: "0644" content: | APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; - name: Enable and start chrony ansible.builtin.service: name: chrony enabled: true state: started # --- Deploy directories -------------------------------------------------------- - name: Create the scrabble base directories ansible.builtin.file: path: "{{ scrabble_base_dir }}/{{ item }}" state: directory owner: "{{ deploy_user }}" group: "{{ deploy_user }}" mode: "0750" loop: - "" - config - certs - dumps - images