--- # SPDX-License-Identifier: AGPL-3.0-or-later # # Base hardening: apt baseline, ufw, fail2ban-lite, system users. - name: Apt — install baseline packages apt: name: - curl - wget - ca-certificates - ufw - fail2ban - sudo - git - jq - rsync - rclone # cross-region Spaces sync (S37) - tzdata - unattended-upgrades - python3-psycopg2 # community.postgresql modules need this state: present update_cache: yes - name: Timezone — pin to UTC for log + cron sanity community.general.timezone: name: UTC - name: Unattended security upgrades — enable copy: dest: /etc/apt/apt.conf.d/20auto-upgrades content: | APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; mode: "0644" - name: UFW — defaults ufw: direction: incoming policy: deny - name: UFW — allow ssh ufw: { rule: allow, port: "22", proto: tcp } - name: UFW — allow http (Caddy redirect to https) ufw: { rule: allow, port: "80", proto: tcp } - name: UFW — allow https ufw: { rule: allow, port: "443", proto: tcp } - name: UFW — allow wireguard ufw: { rule: allow, port: "51820", proto: udp } - name: UFW — enable ufw: { state: enabled } - name: System users — create shithub (web/worker) user: name: "{{ shithub_user }}" system: yes home: "/var/lib/shithub" shell: /usr/sbin/nologin create_home: yes - name: System users — create shithub-ssh (AKC) user: name: shithub-ssh system: yes home: /var/lib/shithub-ssh shell: /usr/sbin/nologin create_home: yes # The `git` user is the SSH login target sshd matches against in the # sshd_config Match-User-git block. Three subtleties learned during # first-time enable: # # 1. Shell can't be nologin (sshd rejects) — git-shell is the right # choice for defense-in-depth. # 2. `useradd --system` defaults to a LOCKED password (`!` in # shadow); sshd refuses any auth (including pubkey) for locked # accounts. `passwd -d` clears the password to NP (no password) # which sshd accepts. With `PasswordAuthentication no` globally, # no-password is fine — pubkey is the only path. # 3. ssh-shell (the AKC's forced command) needs to read # /etc/shithub/web.env for SHITHUB_DATABASE_URL. Adding `git` to # the `shithub` group + chmod g+r on web.env grants exactly that. - name: Ensure git-shell is installed (provided by `git` package, already a dep) command: which git-shell register: git_shell changed_when: false - name: System users — create git (SSH login target) user: name: git system: yes home: /var/lib/git shell: "{{ git_shell.stdout }}" create_home: yes groups: ["{{ shithub_group }}"] append: yes - name: System users — unlock git (sshd refuses locked accounts even for pubkey) command: passwd -d git register: passwd_d changed_when: passwd_d.stdout is search('password changed') - name: Data root — create + own file: path: "{{ shithub_data_root }}" state: directory owner: "{{ shithub_user }}" group: "{{ shithub_group }}" mode: "0755" # Caddy access log carries the real client IP (Caddy terminates TLS # and shithubd only ever sees 127.0.0.1 over the loopback proxy). # This filter scans Caddy's JSON-formatted access log for failed # auth attempts. Field order in Caddy's JSON output is stable in # practice; the regex is deliberately substring-anchored so the # `` group always lands on the request.remote_ip value. - name: fail2ban — shithubd auth filter (Caddy access log) copy: dest: /etc/fail2ban/filter.d/shithubd-auth.conf content: | [Definition] failregex = ^.*"remote_ip":"".*?"method":"POST".*?"uri":"/(login|signup|password/reset|password/forgot|2fa(/[^"]*)?|settings/security/2fa/[^"]*)".*?"status":(401|403|429)\b ignoreregex = mode: "0644" notify: restart fail2ban - name: fail2ban — jail config (sshd + shithubd-auth) copy: dest: /etc/fail2ban/jail.d/shithub.local content: | [sshd] enabled = true maxretry = 5 findtime = 600 bantime = 3600 # Bans clients that hammer the auth surface past the app-level # throttle. The per-user throttle in internal/auth/throttle stops # password guessing; this jail stops the network noise BEFORE # the request reaches the proxy, freeing connection slots. # Slightly more lenient than sshd because legitimate users may # legitimately fail the 2FA challenge once or twice. [shithubd-auth] enabled = true filter = shithubd-auth logpath = /var/log/caddy/access.log maxretry = 8 findtime = 600 bantime = 3600 backend = auto mode: "0644" notify: restart fail2ban # AIDE file-integrity monitoring lives in its own task file so the # nightly-check + baseline-init noise doesn't clutter the main flow. - name: AIDE file-integrity monitoring import_tasks: aide.yml