c8601c0115
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
Two prod-deploy hardening measures (owner-requested): - Swap file (Ansible common role, swap_size=1G, vm.swappiness=10). The per-container memory caps enforce that one service can't eat all RAM, but they overcommit the 1.9 GiB main host (~2.8 GiB of caps), so a simultaneous spike could hit the kernel OOM-killer (and it might pick postgres). A small swap absorbs the overshoot. Idempotent, builtin-only (no ansible.posix). - Edge maintenance page. prod-deploy.sh raises a flag around the rolling swap / migration window that the caddy edge serves a static 503 "технические работы" page from (deploy/caddy/maintenance.html), for the user-facing routes only — /_gm (Grafana) stays reachable. Cleared on any exit (success, health failure + rollback, or error) by a shell trap so it can never stick on. The 503 carries Retry-After + an X-Scrabble-Maintenance marker so a follow-up SPA overlay can tell a planned window apart from a transient error (the static page only catches a fresh load; an in-session user needs the app-side overlay). Not zero-downtime — the single stateful backend still blips — but the window is graceful instead of raw 502s. Verified: caddy validate; the gate 503s every non-/_gm path incl. the Connect/gRPC edge and serves the page + markers; /_gm bypasses; toggling needs no reload (per-request stat). Ansible --syntax-check + compose config (base+prod) pass.
219 lines
6.0 KiB
YAML
219 lines
6.0 KiB
YAML
---
|
|
# 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
|
|
|
|
# --- Swap file (OOM cushion) ---------------------------------------------------
|
|
# The prod main host is tight (1.9 GiB) and the per-container memory caps
|
|
# (docker-compose.prod.yml) sum to more than RAM, so a simultaneous spike could hit
|
|
# the kernel OOM-killer (which might pick postgres). A small swap absorbs the
|
|
# overshoot. Idempotent; skipped when swap_size == "0". Builtin-only (no ansible.posix).
|
|
|
|
- name: Check whether the swap file is already active
|
|
ansible.builtin.command: swapon --show=NAME --noheadings
|
|
register: swap_active
|
|
changed_when: false
|
|
when: swap_size != "0"
|
|
|
|
- name: Allocate the swap file
|
|
ansible.builtin.command:
|
|
cmd: "fallocate -l {{ swap_size }} /swapfile"
|
|
creates: /swapfile
|
|
when: swap_size != "0" and '/swapfile' not in swap_active.stdout
|
|
|
|
- name: Secure the swap file (0600)
|
|
ansible.builtin.file:
|
|
path: /swapfile
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
when: swap_size != "0"
|
|
|
|
- name: Format and enable the swap file
|
|
ansible.builtin.shell: "mkswap /swapfile && swapon /swapfile"
|
|
when: swap_size != "0" and '/swapfile' not in swap_active.stdout
|
|
|
|
- name: Persist the swap file in /etc/fstab
|
|
ansible.builtin.lineinfile:
|
|
path: /etc/fstab
|
|
line: "/swapfile none swap sw 0 0"
|
|
regexp: '^/swapfile\s'
|
|
state: present
|
|
when: swap_size != "0"
|
|
|
|
- name: Keep swap a cushion, not a hot path (low swappiness)
|
|
ansible.builtin.copy:
|
|
dest: /etc/sysctl.d/60-scrabble-swap.conf
|
|
mode: "0644"
|
|
content: "vm.swappiness = {{ swap_swappiness }}\n"
|
|
register: swappiness_conf
|
|
when: swap_size != "0"
|
|
|
|
- name: Apply swappiness now
|
|
ansible.builtin.command: sysctl --system
|
|
changed_when: false
|
|
when: swap_size != "0" and swappiness_conf is changed
|
|
|
|
# --- 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
|