-
Notifications
You must be signed in to change notification settings - Fork 0
378 lines (344 loc) · 24.1 KB
/
Copy pathe2e.yml
File metadata and controls
378 lines (344 loc) · 24.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
name: e2e
# The real proof: boot a disposable Debian "server" (systemd container), run
# harden.sh inside it for real, run it again to prove idempotence, then verify
# every promise from the outside over SSH. Linting checks the script parses;
# this checks it actually hardens.
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
harden-and-verify:
name: Harden a disposable Debian node + verify
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Boot the test node
run: ./test/node.sh up && ./test/node.sh wait
# CIS 6.1 offenders planted BEFORE hardening: a world-writable file, an
# orphan owned by a UID that doesn't exist, and a 777 dir without the
# sticky bit. The file-permissions sweeps must fix all three — verify's
# checks flip from red to green because of these, not by vacuum.
- name: Plant file-permission offenders for the first pass to fix
run: |
docker exec db-harden-node bash -c "
install -d -m 777 /usr/local/share/dh-ww-dir &&
touch /usr/local/share/dh-ww-file && chmod 666 /usr/local/share/dh-ww-file &&
touch /usr/local/share/dh-orphan && chown 12345:12345 /usr/local/share/dh-orphan"
# CIS 5.4.2 / 6.2.9 offender: a service account with a REAL shell and a
# usable password — exactly what a packaged daemon leaves behind, and a
# valid su / SSH target. The system-accounts step must take both away,
# so verify's checks flip from red to green because of this account.
- name: Plant a system account with a login shell for step 25 to lock
run: |
docker exec db-harden-node bash -c "
useradd --system --shell /bin/bash --create-home dhsvc &&
echo 'dhsvc:Plant3d!Svc#2026' | chpasswd"
# CIS 4.2.3 offenders: a world-readable log file, and rsyslog.conf's own
# FileCreateMode drifted to 0644 (one careless edit away on any box).
# The log-permissions step must sweep the file to 0640 AND re-impose
# 0640 on newly created logs via its drop-in — verify deletes syslog and
# proves the reborn file's mode, so this flips red to green for real.
- name: Plant log-permission offenders for step 26 to fix
run: |
docker exec db-harden-node bash -c "
touch /var/log/dh-app.log && chmod 666 /var/log/dh-app.log &&
sed -i 's/^\\\$FileCreateMode 0640/\\\$FileCreateMode 0644/' /etc/rsyslog.conf"
# CIS 4.4 offender: a logrotate snippet that re-creates its log 0666.
# Stock Debian already offends on its own (bare global create, dpkg and
# alternatives at 644) — this one is the evergreen guarantee should the
# distro ever clean up: the logrotate-perms step must tighten it to
# 0640 while keeping the owner/group arguments untouched.
- name: Plant a logrotate offender for step 27 to tighten
run: |
docker exec db-harden-node bash -c "
touch /var/log/dh-rotated.log &&
printf '/var/log/dh-rotated.log {\n weekly\n rotate 2\n create 0666 root adm\n}\n' > /etc/logrotate.d/dh-rotated"
# CIS 6.2 offender: an interactive user whose home is 755 — the default
# many distros still ship — carrying the two legacy dotfiles that grant
# access with no password (.netrc holds cleartext logins, .forward
# reroutes mail). The home-permissions step must tighten the mode AND
# remove both relics — verify's checks flip from red to green for real.
- name: Plant a loose interactive home for step 29 to tighten
run: |
docker exec db-harden-node bash -c "
useradd --create-home --shell /bin/bash dhhome &&
chmod 755 /home/dhhome &&
printf 'machine example.com login dh password hunter2\n' > /home/dhhome/.netrc &&
printf 'dh@elsewhere.example\n' > /home/dhhome/.forward &&
chown dhhome:dhhome /home/dhhome/.netrc /home/dhhome/.forward"
# Step 30 offender: /proc mounted WITHOUT hidepid (the stock state) and
# ptrace_scope forced back to 0. The second half matters — this WSL /
# runner kernel already ships ptrace_scope=1, so without planting the 0
# the check would pass without the step doing anything (tautological).
- name: Plant open process visibility for step 30 to close
run: |
docker exec db-harden-node bash -c "
mount -o remount,hidepid=0 /proc &&
sysctl -qw kernel.yama.ptrace_scope=0 &&
findmnt -no OPTIONS /proc &&
sysctl -n kernel.yama.ptrace_scope"
# Step 31 offenders: the guess-cost knobs pinned to their WEAK values —
# an explicit cost factor 5 (what stock yescrypt uses implicitly) and
# FAIL_DELAY 0. The stock file carries NEITHER key, so planting them
# forces the step through its reconcile path (sed, not append) and the
# login.defs checks flip red→green because real drift got rewritten.
- name: Plant weak guess-cost values for step 31 to reconcile
run: |
docker exec db-harden-node bash -c "
printf 'YESCRYPT_COST_FACTOR\t5\nFAIL_DELAY\t0\n' >> /etc/login.defs &&
grep -E '^(YESCRYPT_COST_FACTOR|FAIL_DELAY)' /etc/login.defs"
# Step 32 offenders, planted in ALL THREE sources that set root's PATH
# on Debian (login.defs, /etc/profile — which OVERWRITES login.defs for
# login shells — and /etc/crontab). Two kinds on purpose, because the
# step treats them differently and that distinction IS the promise:
# - fixable: /opt/dh-path-loose (exists, world-writable) and a
# loosened /usr/local/bin — both must come back TIGHTENED and still
# in the PATH. Deleting a directory the admin put there would break
# locally installed binaries; the step fixes what it can.
# - unfixable: an empty entry (= the current directory), a bare '.',
# and /opt/dh-path-gone which does not exist — these must vanish.
- name: Plant PATH traps for step 32 to sanitize
run: |
docker exec db-harden-node bash -c "
install -d -m 777 /opt/dh-path-loose &&
chmod 777 /usr/local/bin &&
sed -i 's|^ENV_SUPATH.*|ENV_SUPATH\tPATH=/opt/dh-path-loose:/opt/dh-path-gone:/usr/local/sbin::/usr/local/bin:.:/usr/sbin:/usr/bin:/sbin:/bin|' /etc/login.defs &&
sed -i '5s|PATH=\"[^\"]*\"|PATH=\"/opt/dh-path-loose:/opt/dh-path-gone:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:\"|' /etc/profile &&
sed -i 's|^PATH=.*|PATH=/opt/dh-path-gone:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin|' /etc/crontab &&
grep -E '^ENV_SUPATH' /etc/login.defs && sed -n 5p /etc/profile"
# Step 34 offenders: a LAX pwhistory profile (remember=3, and no
# enforce_for_root — measured: without it a reuse performed by root
# prints the warning and goes through anyway) plus a loosened opasswd.
# The stock system carries no profile at all, so planting one forces
# the step through its overwrite path, and 0644 on a file of password
# hashes is exactly the drift the chmod must undo.
- name: Plant a lax password-history profile for step 34 to overwrite
run: |
docker exec db-harden-node bash -c "
printf 'Name: Password history (lax)\nDefault: yes\nPriority: 512\nPassword-Type: Primary\nPassword:\n\trequisite\t\t\tpam_pwhistory.so remember=3 use_authtok\n' > /usr/share/pam-configs/hardening-pwhistory &&
chmod 644 /etc/security/opasswd &&
stat -c '%a' /etc/security/opasswd && cat /usr/share/pam-configs/hardening-pwhistory"
# Step 35 offender: an explicit weak-crypto line in the MAIN sshd
# config — the "legacy client compat" line every fleet has somewhere.
# Debian includes sshd_config.d at the TOP of sshd_config and sshd
# honours the FIRST occurrence of a keyword, so the step's drop-in
# must beat this line: planting it proves the PRECEDENCE, not just
# the file contents (and stock offers hmac-sha1 anyway — this keeps
# the check meaningful the day upstream retires it).
- name: Plant a weak-crypto line for step 35 to override
run: |
docker exec db-harden-node bash -c "
printf 'MACs hmac-sha2-256,hmac-sha1\nKexAlgorithms ecdh-sha2-nistp256,curve25519-sha256\n' >> /etc/ssh/sshd_config &&
sshd -t && tail -2 /etc/ssh/sshd_config"
# Step 36 offenders: a legacy client and a legacy superserver apt can
# still install on Debian 13 — and `telnet` is the transitional trap
# (a dummy package whose payload is inetutils-telnet via
# update-alternatives: purging the dummy alone leaves /usr/bin/telnet
# working). The purge must take the whole pair AND xinetd away.
- name: Plant legacy protocol packages for step 36 to purge
run: |
docker exec db-harden-node bash -c "
apt-get install -y --no-install-recommends telnet xinetd >/dev/null &&
command -v telnet && dpkg-query -W telnet inetutils-telnet xinetd"
# Step 37 offenders: modern kernels ship fs.protected_* already ON
# (measured: 1/1/1/2 in a fresh privileged container), so WITHOUT
# planting the step would confirm a default and prove nothing. Write
# all four to 0 via /proc (sysctl isn't installed pre-harden) — the
# container is privileged with host cgroups, so the write takes; the
# step must tighten them back to 1/1/1/2.
- name: Plant weak fs.protected_* values for step 37 to tighten
run: |
docker exec db-harden-node bash -c '
for f in symlinks hardlinks fifos regular; do echo 0 > /proc/sys/fs/protected_$f; done
grep -H . /proc/sys/fs/protected_symlinks /proc/sys/fs/protected_regular'
# Step 39 offenders: the runner ships full ASLR and a soft-disabled
# unprivileged BPF, so plant the classic weak values (a debugging
# session's randomize_va_space=0, BPF fully open, perf at the
# everything-goes -1) via /proc — sysctl isn't installed pre-harden.
# kexec_load_disabled is a natural offender (ships 0) and is one-way,
# so there is nothing to plant. The two greps log the smoking gun:
# with ASLR off, two fresh processes print the SAME stack base.
- name: Plant exploit-mitigation offenders for step 39 to fix
run: |
docker exec db-harden-node bash -c '
echo 0 > /proc/sys/kernel/randomize_va_space
echo 0 > /proc/sys/kernel/unprivileged_bpf_disabled 2>/dev/null \
|| echo "unprivileged_bpf_disabled already latched at 1 (one-way) - nothing to plant"
echo -1 > /proc/sys/kernel/perf_event_paranoid
if [ -e /proc/sys/net/core/bpf_jit_harden ]; then echo 0 > /proc/sys/net/core/bpf_jit_harden; fi
echo "ASLR planted off — two fresh processes, same stack base:"
grep -m1 "\[stack\]" /proc/self/maps | cut -d- -f1
grep -m1 "\[stack\]" /proc/self/maps | cut -d- -f1
grep -H . /proc/sys/kernel/randomize_va_space /proc/sys/kernel/unprivileged_bpf_disabled /proc/sys/kernel/perf_event_paranoid'
# Step 38 offenders — the logins that hide in the account DATA, all
# three measured to work on a stock node before the step was written:
# an EMPTY password authenticates with a bare Enter (pam_unix ships
# nullok), a hash sitting in world-readable /etc/passwd authenticates
# from there too, and legacy NIS '+' entries are the splice-the-map-in
# trigger the moment nsswitch ever flips to `compat`.
- name: Plant account-database offenders for step 38 to clean
run: |
docker exec db-harden-node bash -c '
useradd --create-home --shell /bin/bash dhnopw && passwd -d dhnopw &&
useradd --create-home --shell /bin/bash dhlegacy &&
echo "dhlegacy:Sh4dow-Migr8-OK!9" | chpasswd &&
h=$(grep "^dhlegacy:" /etc/shadow | cut -d: -f2) &&
sed -i "s|^dhlegacy:x:|dhlegacy:$h:|" /etc/passwd &&
sed -i "s|^dhlegacy:[^:]*:|dhlegacy:*:|" /etc/shadow &&
printf "+::0:0:::\n" >> /etc/passwd &&
printf "+::::::::\n" >> /etc/shadow &&
printf "+:::\n" >> /etc/group &&
grep -c "^+" /etc/passwd /etc/shadow /etc/group &&
grep "^dhlegacy:" /etc/passwd | cut -c1-24'
# Step 42 offender — the broken-mirror workaround that outlives the
# mirror: both apt trust gates loosened in an apt.conf.d file. Measured
# on a stock node: with these two lines a repository with no Release
# file is fetched behind a one-line warning and its packages install
# with "Authentication warning overridden". The step's 99- pin must beat
# it (apt.conf.d is read in order, last setting wins) and verify reads
# the EFFECTIVE config — this plant is what makes that check a proof
# instead of a restatement of the defaults.
# Step 44 offenders — the network daemons a server runs because a
# package pulled them in. The node image ships none of them, so install
# the three (cups brings cups-daemon, the actual cupsd, with it — the
# measured trap: purging `cups` alone leaves it behind). Listeners are
# logged before the step so verify's "nothing on 5353/631/111" means
# something.
- name: Plant attack-surface services for step 44 to purge
run: |
docker exec db-harden-node bash -c '
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq --no-install-recommends avahi-daemon cups rpcbind >/dev/null 2>&1
systemctl start avahi-daemon rpcbind cups 2>/dev/null || true
dpkg -l avahi-daemon cups cups-daemon rpcbind | awk "/^ii/{print \$2}" | tr "\n" " "; echo
ss -lntu | grep -E ":(5353|631|111)[[:space:]]" || echo "(no listener yet - units may need a moment)"'
# Step 45 offenders: a stock kernel ships every one of these interfaces
# open — plant the open values via /proc (io_uring allowed, SysRq at
# Debian's 438, tty line disciplines autoloading, unprivileged user
# namespaces on where the kernel carries the knob). The perl line logs
# the smoking gun: io_uring_setup(2) hands anyone a ring fd.
- name: Plant open kernel interfaces for step 45 to close
run: |
docker exec -i db-harden-node bash -s <<'SH'
if [ -e /proc/sys/kernel/io_uring_disabled ]; then echo 0 > /proc/sys/kernel/io_uring_disabled; else echo "no io_uring_disabled knob on this kernel (< 6.6)"; fi
echo 438 > /proc/sys/kernel/sysrq
echo 1 > /proc/sys/dev/tty/ldisc_autoload
if [ -e /proc/sys/kernel/unprivileged_userns_clone ]; then echo 1 > /proc/sys/kernel/unprivileged_userns_clone; else echo "no unprivileged_userns_clone knob on this kernel (upstream build)"; fi
echo "io_uring open - io_uring_setup(2) hands anyone a ring:"
perl -e '$p = "\0" x 120; $r = syscall(425, 8, $p); print "io_uring_setup -> $r", ($r < 0 ? " ($!)" : " (a ring fd)"), "\n"'
grep -H . /proc/sys/kernel/io_uring_disabled /proc/sys/kernel/sysrq /proc/sys/dev/tty/ldisc_autoload /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null || true
SH
# Step 46 has natural offenders: the passwd and login packages ship
# chfn/chsh/gpasswd/newgrp setuid root and expiry setgid shadow. Nothing
# to plant - log the shipped modes so the flip is visible.
- name: Log the shipped setuid/setgid modes step 46 will strip
run: |
docker exec db-harden-node stat -c "%a %U:%G %n" /usr/bin/chfn /usr/bin/chsh /usr/bin/gpasswd /usr/bin/newgrp /usr/bin/expiry
# Step 47 has a natural offender: every session ships unlimited. Log it
# (a login session through su -l, the same pam_limits stack as sshd).
- name: Log the shipped per-session process limit step 47 will cap
run: |
docker exec db-harden-node bash -c "echo \"nproc as shipped (root): soft=\$(ulimit -Su) hard=\$(ulimit -Hu); pam_limits in sshd stack: \$(grep -cE '^session\s+required\s+pam_limits' /etc/pam.d/sshd)\""
# Step 48 has a natural offender: systemd ships ctrl-alt-del.target as an
# alias of reboot.target. Nothing to plant - log the shipped state.
- name: Log the shipped Ctrl+Alt+Del state step 48 will close
run: |
docker exec db-harden-node bash -c "echo \"ctrl-alt-del.target as shipped: \$(systemctl is-enabled ctrl-alt-del.target 2>&1)\""
# Step 49 has a natural offender too: ufw is not even installed yet, so
# outbound is open by definition - and step 5 will then set it open on
# purpose (`default allow outgoing`). Log what the box ships with.
- name: Log the shipped outbound policy step 49 will close
run: |
docker exec db-harden-node bash -c "echo \"outbound policy as shipped: \$(grep -h DEFAULT_OUTPUT_POLICY /etc/default/ufw 2>/dev/null || echo 'ufw not installed - nothing filters outbound')\""
# Step 50 has a natural offender: Debian ships pam_unix with nullok in
# common-auth. Nothing to plant - log the shipped line.
- name: Log the shipped pam_unix line step 50 will strip nullok from
run: |
docker exec db-harden-node bash -c "echo \"pam_unix as shipped: \$(grep pam_unix /etc/pam.d/common-auth)\""
# Step 51: the natural offender is the ECDSA nistp256 host key Debian
# generates and serves. Plant a stray `HostKey` line for it in the MAIN
# config too: HostKey lines accumulate (first-wins does not apply), so
# naming ed25519/RSA in a drop-in does NOT stop that key from being
# served (measured) - only the HostKeyAlgorithms pin does. Record the
# fingerprints, so verify can prove nothing was regenerated.
- name: Plant a stray ECDSA HostKey line and record the host keys for step 51
run: |
docker exec db-harden-node bash -c '
echo "HostKey /etc/ssh/ssh_host_ecdsa_key" >> /etc/ssh/sshd_config &&
for f in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$f"; done' | tee test/.ssh_ci/hostkeys-before.txt
echo "served as shipped: $(ssh-keyscan -p 2222 127.0.0.1 2>/dev/null | cut -d" " -f2 | sort -u | tr "\n" " ")"
- name: Plant loosened apt trust gates for step 42 to override
run: |
docker exec db-harden-node bash -c '
printf "Acquire::AllowInsecureRepositories \"true\";\nAPT::Get::AllowUnauthenticated \"true\";\n" > /etc/apt/apt.conf.d/90-weak-mirror-workaround &&
apt-config dump | grep -E "AllowInsecureRepositories|AllowUnauthenticated"'
# Step 52: Debian ships sudo's credential cache per tty, 15 minutes - so
# the offender is planted: a convenience drop-in that shares the window
# with every process of the user (timestamp_type=global) and never
# closes it (timestamp_timeout=-1). Measured: with it, a no-tty job and
# a second pty of the user both run sudo without a password.
- name: Plant a global, never-expiring sudo credential cache for step 52
run: |
docker exec db-harden-node bash -c '
printf "Defaults timestamp_type=global\nDefaults timestamp_timeout=-1\n" > /etc/sudoers.d/40-ci-convenience &&
chmod 440 /etc/sudoers.d/40-ci-convenience && visudo -cf /etc/sudoers.d/40-ci-convenience >/dev/null &&
sudo -V | grep -E "^(Authentication timestamp timeout|Type of authentication timestamp record):"'
- name: "First pass: run harden.sh inside the node (as root)"
run: |
docker exec db-harden-node bash /root/harden.sh \
--admin-user opsadmin \
--pubkey "$(cat test/.ssh_ci/id_ci.pub)" -y
# The audit used to be run by hand, so test/AUDIT.md quoted whatever the
# last local run said (171 while step 52 had added a check). Now the CI
# measures it - after ONE pass, before the second: AUDIT.md explains why
# one (a second pass hides the drift a real server shows after its first
# nightly upgrade). No FAIL is allowed, and the documented score must be
# the measured one, so the number in the docs can't go stale again.
- name: "Audit after one pass (no FAIL, and AUDIT.md must quote this score)"
run: |
out=$(./test/audit.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
echo "$out"
measured=$(grep -E '^ Score: ' <<<"$out")
documented=$(grep -E '^ Score: ' test/AUDIT.md | tail -1)
if grep -qE '^ FAIL ' <<<"$out"; then
echo "::error::the audit has FAIL items on a freshly hardened node"; exit 1
fi
if [ "$measured" != "$documented" ]; then
echo "::error::test/AUDIT.md quotes '$documented' but the audit measured '$measured'"; exit 1
fi
echo "AUDIT.md agrees with the measurement: $measured"
- name: "Second pass: idempotence (config files must not change)"
run: |
files="/etc/ssh/sshd_config.d/99-hardening.conf /etc/fail2ban/jail.local /etc/apt/apt.conf.d/20auto-upgrades /etc/sysctl.d/99-hardening.conf /etc/login.defs /etc/default/useradd /etc/fstab /etc/issue /etc/issue.net /etc/motd /etc/ssh/sshd_config.d/98-banner.conf /etc/sudoers.d/99-hardening-sudo /etc/ssh/sshd_config.d/97-hardening-policies.conf /etc/security/limits.d/99-hardening-coredumps.conf /etc/systemd/coredump.conf.d/99-hardening.conf /etc/profile.d/99-hardening-umask.sh /etc/profile.d/99-hardening-tmout.sh /etc/cron.allow /etc/security/pwquality.conf /etc/aide/hardening.conf /etc/systemd/system/aide-check.timer /etc/default/rkhunter /etc/systemd/system/rkhunter-check.timer /etc/modprobe.d/99-hardening-blacklist.conf /etc/security/faillock.conf /etc/pam.d/su /etc/passwd /etc/rsyslog.d/99-hardening.conf /etc/logrotate.conf /etc/logrotate.d/dpkg /etc/logrotate.d/dh-rotated /etc/audit/rules.d/hardening.rules /etc/audit/auditd.conf /etc/sysctl.d/99-hardening-process.conf /usr/share/pam-configs/hardening-faildelay /etc/pam.d/common-auth /etc/profile /etc/crontab /usr/share/pam-configs/hardening-pwhistory /etc/pam.d/common-password /etc/security/opasswd /etc/ssh/sshd_config.d/95-hardening-crypto.conf /etc/sysctl.d/99-hardening-fs.conf /etc/shadow /etc/group /etc/sysctl.d/99-hardening-exploit.conf /etc/systemd/timesyncd.conf.d/99-hardening.conf /etc/apt/apt.conf.d/99-hardening-apt-trust /etc/apt/apt.conf.d/99-hardening-logperms /etc/sysctl.d/99-hardening-kernel-surface.conf /var/lib/dpkg/statoverride /etc/security/limits.d/99-hardening-nproc.conf /etc/systemd/system/ctrl-alt-del.target /etc/systemd/system.conf.d/99-hardening-ctrlaltdel.conf /etc/default/ufw /etc/ufw/user.rules /etc/ufw/user6.rules /etc/ssh/sshd_config.d/96-hardening-hostkeys.conf /etc/sudoers.d/99-hardening-timestamp"
before=$(docker exec db-harden-node sha256sum $files)
docker exec db-harden-node bash /root/harden.sh \
--admin-user opsadmin \
--pubkey "$(cat test/.ssh_ci/id_ci.pub)" -y
after=$(docker exec db-harden-node sha256sum $files)
if [ "$before" != "$after" ]; then
echo "::error::harden.sh is not idempotent — config changed on the second run"
diff <(echo "$before") <(echo "$after") || true
exit 1
fi
echo "Idempotent: config unchanged on the second run."
- name: Verify the hardening from the outside
run: ./test/verify.sh
- name: Tear the node down
if: always()
run: ./test/node.sh down
# Independent of the node above: hardens fresh throwaway containers with
# different flag sets and asserts each behaves (lockout guard, custom
# port, extra ports, skipping a step).
# Same guard for the scenario count test/SCENARIOS.md quotes (it said
# 108 while the suite ran 112).
- name: Scenario tests (flag behaviour)
run: |
out=$(./test/scenarios.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g') || { echo "$out"; exit 1; }
echo "$out"
n=$(grep -oE '^ [0-9]+/[0-9]+ scenario checks passed' <<<"$out" | grep -oE '[0-9]+/[0-9]+')
grep -qF "**Result: $n checks pass.**" test/SCENARIOS.md \
|| { echo "::error::test/SCENARIOS.md does not quote the measured $n"; exit 1; }