YAML · 5071 bytes Raw Blame History
1 ---
2 # SPDX-License-Identifier: AGPL-3.0-or-later
3 #
4 # Base hardening: apt baseline, ufw, fail2ban-lite, system users.
5
6 - name: Apt — install baseline packages
7 apt:
8 name:
9 - curl
10 - wget
11 - ca-certificates
12 - ufw
13 - fail2ban
14 - sudo
15 - git
16 - jq
17 - rsync
18 - rclone # cross-region Spaces sync (S37)
19 - tzdata
20 - unattended-upgrades
21 - python3-psycopg2 # community.postgresql modules need this
22 state: present
23 update_cache: yes
24
25 - name: Timezone — pin to UTC for log + cron sanity
26 community.general.timezone:
27 name: UTC
28
29 - name: Unattended security upgrades — enable
30 copy:
31 dest: /etc/apt/apt.conf.d/20auto-upgrades
32 content: |
33 APT::Periodic::Update-Package-Lists "1";
34 APT::Periodic::Unattended-Upgrade "1";
35 mode: "0644"
36
37 - name: UFW — defaults
38 ufw:
39 direction: incoming
40 policy: deny
41 - name: UFW — allow ssh
42 ufw: { rule: allow, port: "22", proto: tcp }
43 - name: UFW — allow http (Caddy redirect to https)
44 ufw: { rule: allow, port: "80", proto: tcp }
45 - name: UFW — allow https
46 ufw: { rule: allow, port: "443", proto: tcp }
47 - name: UFW — allow wireguard
48 ufw: { rule: allow, port: "51820", proto: udp }
49 - name: UFW — enable
50 ufw: { state: enabled }
51
52 - name: System users — create shithub (web/worker)
53 user:
54 name: "{{ shithub_user }}"
55 system: yes
56 home: "/var/lib/shithub"
57 shell: /usr/sbin/nologin
58 create_home: yes
59
60 - name: System users — create shithub-ssh (AKC)
61 user:
62 name: shithub-ssh
63 system: yes
64 home: /var/lib/shithub-ssh
65 shell: /usr/sbin/nologin
66 create_home: yes
67
68 # The `git` user is the SSH login target sshd matches against in the
69 # sshd_config Match-User-git block. Three subtleties learned during
70 # first-time enable:
71 #
72 # 1. Shell can't be nologin (sshd rejects) — git-shell is the right
73 # choice for defense-in-depth.
74 # 2. `useradd --system` defaults to a LOCKED password (`!` in
75 # shadow); sshd refuses any auth (including pubkey) for locked
76 # accounts. `passwd -d` clears the password to NP (no password)
77 # which sshd accepts. With `PasswordAuthentication no` globally,
78 # no-password is fine — pubkey is the only path.
79 # 3. ssh-shell (the AKC's forced command) needs to read
80 # /etc/shithub/web.env for SHITHUB_DATABASE_URL. Adding `git` to
81 # the `shithub` group + chmod g+r on web.env grants exactly that.
82 - name: Ensure git-shell is installed (provided by `git` package, already a dep)
83 command: which git-shell
84 register: git_shell
85 changed_when: false
86
87 - name: System users — create git (SSH login target)
88 user:
89 name: git
90 system: yes
91 home: /var/lib/git
92 shell: "{{ git_shell.stdout }}"
93 create_home: yes
94 groups: ["{{ shithub_group }}"]
95 append: yes
96
97 - name: System users — unlock git (sshd refuses locked accounts even for pubkey)
98 command: passwd -d git
99 register: passwd_d
100 changed_when: passwd_d.stdout is search('password changed')
101
102 - name: Data root — create + own
103 file:
104 path: "{{ shithub_data_root }}"
105 state: directory
106 owner: "{{ shithub_user }}"
107 group: "{{ shithub_group }}"
108 mode: "0755"
109
110 # Caddy access log carries the real client IP (Caddy terminates TLS
111 # and shithubd only ever sees 127.0.0.1 over the loopback proxy).
112 # This filter scans Caddy's JSON-formatted access log for failed
113 # auth attempts. Field order in Caddy's JSON output is stable in
114 # practice; the regex is deliberately substring-anchored so the
115 # `<HOST>` group always lands on the request.remote_ip value.
116 - name: fail2ban — shithubd auth filter (Caddy access log)
117 copy:
118 dest: /etc/fail2ban/filter.d/shithubd-auth.conf
119 content: |
120 [Definition]
121 failregex = ^.*"remote_ip":"<HOST>".*?"method":"POST".*?"uri":"/(login|signup|password/reset|password/forgot|2fa(/[^"]*)?|settings/security/2fa/[^"]*)".*?"status":(401|403|429)\b
122 ignoreregex =
123 mode: "0644"
124 notify: restart fail2ban
125
126 - name: fail2ban — jail config (sshd + shithubd-auth)
127 copy:
128 dest: /etc/fail2ban/jail.d/shithub.local
129 content: |
130 [sshd]
131 enabled = true
132 maxretry = 5
133 findtime = 600
134 bantime = 3600
135
136 # Bans clients that hammer the auth surface past the app-level
137 # throttle. The per-user throttle in internal/auth/throttle stops
138 # password guessing; this jail stops the network noise BEFORE
139 # the request reaches the proxy, freeing connection slots.
140 # Slightly more lenient than sshd because legitimate users may
141 # legitimately fail the 2FA challenge once or twice.
142 [shithubd-auth]
143 enabled = true
144 filter = shithubd-auth
145 logpath = /var/log/caddy/access.log
146 maxretry = 8
147 findtime = 600
148 bantime = 3600
149 backend = auto
150 mode: "0644"
151 notify: restart fail2ban
152
153 # AIDE file-integrity monitoring lives in its own task file so the
154 # nightly-check + baseline-init noise doesn't clutter the main flow.
155 - name: AIDE file-integrity monitoring
156 import_tasks: aide.yml
157