Skip to content

Commit 9ddbc09

Browse files
Close P3 findings for Molecule matrix, CI lint, and systemd sandboxing.
Run Debian 12/13 Molecule in CI, lint molecule playbooks, migrate the health watchdog to a sandboxed systemd timer, and harden monitoring/deploy units. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 582711e commit 9ddbc09

16 files changed

Lines changed: 280 additions & 48 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,29 @@ jobs:
3838
# Satisfy profile assert for syntax-check (runtime still needs -e @profiles/…)
3939
printf '\nharden_profile: lab\n' >> group_vars/all/vars.yml
4040
41-
- name: Yamllint playbooks and roles
42-
run: yamllint -d "{extends: relaxed, rules: {line-length: {max: 160}}}" playbooks roles group_vars inventories profiles
41+
- name: Yamllint playbooks, roles, and Molecule
42+
run: yamllint -d "{extends: relaxed, rules: {line-length: {max: 160}}}" playbooks roles group_vars inventories profiles molecule
4343

4444
- name: Syntax-check playbooks
4545
run: |
4646
ansible-playbook --syntax-check playbooks/01-bootstrap.yml
4747
ansible-playbook --syntax-check playbooks/02-harden.yml
4848
ansible-playbook --syntax-check playbooks/03-audit.yml
4949
50-
- name: Ansible-lint
51-
run: ansible-lint -c .ansible-lint --nocolor playbooks roles
50+
- name: Ansible-lint (playbooks, roles, Molecule)
51+
run: ansible-lint -c .ansible-lint --nocolor playbooks roles molecule
5252

5353
molecule:
54-
name: Molecule smoke (Docker)
54+
name: Molecule (${{ matrix.distro }})
5555
runs-on: ubuntu-latest
56+
strategy:
57+
fail-fast: false
58+
matrix:
59+
include:
60+
- distro: debian13
61+
image: geerlingguy/docker-debian13-ansible@sha256:66663f77a975012e9da0ff3896d402a412db97c1db20b1cf703f3a6dd169ccf1
62+
- distro: debian12
63+
image: geerlingguy/docker-debian12-ansible@sha256:b4344c8d4985d92b6bad512dd8f230a08851aa75bc68da1c315a4a296bab6b67
5664
defaults:
5765
run:
5866
working-directory: ansible
@@ -73,4 +81,6 @@ jobs:
7381
run: ansible-galaxy collection install -r requirements.yml -p ./collections
7482

7583
- name: Molecule test
84+
env:
85+
MOLECULE_IMAGE: ${{ matrix.image }}
7686
run: molecule test

ansible/.ansible-lint

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ skip_list:
44
- yaml[line-length]
55
- name[casing]
66
- experimental
7+
# Kit-wide harden_* vars (not role-prefixed)
8+
- var-naming[no-role-prefix]
9+
# lineinfile create: on /etc drop-ins; mode set by package defaults
10+
- risky-file-permissions
11+
# Molecule verify intentionally uses systemctl/curl for exact smoke asserts
12+
- command-instead-of-module
713
exclude_paths:
814
- collections/
915
- profiles/
10-
- molecule/

ansible/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,11 @@ Optional smoke test (Docker required; also runs in CI):
9696
cd ansible
9797
pip install 'molecule' 'molecule-plugins[docker]'
9898
molecule test
99+
# Optional: Debian 12 image (CI runs both)
100+
MOLECULE_IMAGE=geerlingguy/docker-debian12-ansible@sha256:b4344c8d4985d92b6bad512dd8f230a08851aa75bc68da1c315a4a296bab6b67 molecule test
99101
```
100102

101-
Default scenario also enables **node_exporter + health_watchdog + Monit**, proves **disable uninstalls** them, exercises **webhook deploy** (healthz + bad HMAC → 401 + non-push → 202), then re-enables for verify. GHA runner stays off in CI (needs a real registration token).
103+
Default scenario also enables **node_exporter + health_watchdog + Monit**, proves **disable uninstalls** them, exercises **webhook deploy** (healthz + bad HMAC → 401 + non-push → 202), then re-enables for verify. GHA runner stays off in CI (needs a real registration token). CI matrix runs the same scenario on **Debian 12 and 13**; ansible-lint/yamllint also cover `molecule/`.
102104

103105
## Directory map
104106

ansible/group_vars/all/vars.yml.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ harden_node_exporter_allow_from: []
8686

8787
harden_enable_health_watchdog: false
8888
harden_health_disk_threshold_pct: 90
89-
harden_health_watchdog_cron: "*/5 * * * *"
89+
# systemd OnCalendar (replaces the old cron schedule)
90+
harden_health_watchdog_oncalendar: "*:0/5"
9091
harden_health_watchdog_mail: true
9192
harden_health_probe_urls: []
9293
harden_health_probe_timeout: 10

ansible/molecule/default/converge.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,16 @@
197197
path: /usr/local/sbin/nowo-health-watchdog
198198
register: wd
199199

200+
- name: Watchdog timer must be absent
201+
ansible.builtin.stat:
202+
path: /etc/systemd/system/nowo-health-watchdog.timer
203+
register: wd_timer
204+
200205
- name: Require watchdog gone
201206
ansible.builtin.assert:
202207
that:
203208
- not wd.stat.exists
209+
- not wd_timer.stat.exists
204210

205211
- name: Monit must be removed
206212
ansible.builtin.command: dpkg -s monit

ansible/molecule/default/molecule.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
---
2-
# Debian 13 Docker smoke: identity, SSH, MFA enforce, UFW/IDS, SMTP sink, Lynis --quick.
2+
# Debian Docker smoke (12 + 13 via CI matrix / MOLECULE_IMAGE).
33
# Requires: pip install molecule molecule-plugins[docker] ansible-core
44
# Run from ansible/: molecule test
5+
# Override image: MOLECULE_IMAGE=geerlingguy/docker-debian12-ansible@sha256:… molecule test
56
dependency:
67
name: galaxy
78
options:
89
requirements-file: requirements.yml
910
driver:
1011
name: docker
1112
platforms:
12-
- name: debian13-harden
13+
- name: harden-target
1314
# Pin digest for supply-chain stability (bump intentionally when refreshing)
14-
image: geerlingguy/docker-debian13-ansible@sha256:66663f77a975012e9da0ff3896d402a412db97c1db20b1cf703f3a6dd169ccf1
15+
image: ${MOLECULE_IMAGE:-geerlingguy/docker-debian13-ansible@sha256:66663f77a975012e9da0ff3896d402a412db97c1db20b1cf703f3a6dd169ccf1}
1516
pre_build_image: true
1617
command: ""
1718
volumes:
@@ -24,7 +25,7 @@ provisioner:
2425
hosts:
2526
all:
2627
hosts:
27-
debian13-harden:
28+
harden-target:
2829
ansible_user: root
2930
playbooks:
3031
converge: converge.yml

ansible/molecule/default/verify.yml

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,21 +176,39 @@
176176
path: /usr/local/sbin/nowo-health-watchdog
177177
register: watchdog
178178

179-
- name: Require watchdog script and cron
179+
- name: Health watchdog timer present
180+
ansible.builtin.stat:
181+
path: /etc/systemd/system/nowo-health-watchdog.timer
182+
register: watchdog_timer
183+
184+
- name: Require watchdog script and timer
180185
ansible.builtin.assert:
181186
that:
182187
- watchdog.stat.exists
183188
- watchdog.stat.mode == '0755'
189+
- watchdog_timer.stat.exists
190+
191+
- name: Health watchdog timer is active
192+
ansible.builtin.command: systemctl is-active nowo-health-watchdog.timer
193+
changed_when: false
184194

185-
- name: Health watchdog cron present
195+
- name: Legacy watchdog cron must be absent
186196
ansible.builtin.stat:
187197
path: /etc/cron.d/nowo-health-watchdog
188198
register: watchdog_cron
189199

190-
- name: Require watchdog cron
200+
- name: Require no legacy cron
191201
ansible.builtin.assert:
192202
that:
193-
- watchdog_cron.stat.exists
203+
- not watchdog_cron.stat.exists
204+
205+
- name: node_exporter systemd hardening drop-in present
206+
ansible.builtin.command: grep -F 'ProtectSystem=strict' /etc/systemd/system/prometheus-node-exporter.service.d/nowo-hardening.conf
207+
changed_when: false
208+
209+
- name: Monit systemd hardening drop-in present
210+
ansible.builtin.command: grep -F 'ProtectSystem=full' /etc/systemd/system/monit.service.d/nowo-hardening.conf
211+
changed_when: false
194212

195213
- name: Monit package present
196214
ansible.builtin.command: dpkg -s monit
@@ -218,6 +236,14 @@
218236
ansible.builtin.command: grep -E '^User=webhook-deploy' /etc/systemd/system/nowo-webhook-deploy.service
219237
changed_when: false
220238

239+
- name: Webhook unit has ProtectSystem=strict
240+
ansible.builtin.command: grep -E '^ProtectSystem=strict' /etc/systemd/system/nowo-webhook-deploy.service
241+
changed_when: false
242+
243+
- name: Webhook unit has MemoryDenyWriteExecute
244+
ansible.builtin.command: grep -E '^MemoryDenyWriteExecute=true' /etc/systemd/system/nowo-webhook-deploy.service
245+
changed_when: false
246+
221247
- name: Webhook healthz responds
222248
ansible.builtin.command: curl -fsS --max-time 5 http://127.0.0.1:9000/hooks/deploy/healthz
223249
register: wh_health

ansible/roles/deploy_agents/tasks/gha_runner.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@
175175
KillMode=process
176176
KillSignal=SIGTERM
177177
TimeoutStopSec=5min
178+
# Conservative sandbox — relax if jobs need Docker/privileged helpers
179+
PrivateTmp=true
180+
ProtectHome=true
181+
ProtectSystem=full
182+
ProtectKernelTunables=true
183+
ProtectKernelModules=true
184+
ProtectControlGroups=true
185+
NoNewPrivileges=true
186+
RestrictSUIDSGID=true
187+
LockPersonality=true
188+
SystemCallArchitectures=native
189+
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
190+
UMask=027
178191
179192
[Install]
180193
WantedBy=multi-user.target

ansible/roles/deploy_agents/tasks/webhook_deploy.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,18 @@
279279
PrivateTmp=true
280280
ProtectHome=true
281281
ProtectSystem=strict
282+
ProtectKernelTunables=true
283+
ProtectKernelModules=true
284+
ProtectControlGroups=true
282285
ReadWritePaths={{ harden_webhook_deploy_repo_path }}
283286
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
287+
RestrictNamespaces=true
288+
RestrictSUIDSGID=true
289+
MemoryDenyWriteExecute=true
284290
CapabilityBoundingSet=
285291
LockPersonality=true
292+
SystemCallArchitectures=native
293+
UMask=027
286294
287295
[Install]
288296
WantedBy=multi-user.target

ansible/roles/monitoring_stack/handlers/main.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
name: monit
1010
state: reloaded
1111

12+
- name: Reload systemd daemon
13+
ansible.builtin.systemd:
14+
daemon_reload: true
15+
16+
- name: Reload systemd and enable health watchdog timer
17+
ansible.builtin.systemd:
18+
name: nowo-health-watchdog.timer
19+
state: started
20+
enabled: true
21+
daemon_reload: true
22+
1223
- name: Reload ufw
1324
ansible.builtin.shell: |
1425
set -euo pipefail

0 commit comments

Comments
 (0)