Skip to content

fix(baseline): keep node connectivity under DevSec CIS hardening - #24

Merged
thiras merged 1 commit into
mainfrom
harden-node-connectivity-sysctl-overrides
Jul 11, 2026
Merged

thiras merged 1 commit into
mainfrom
harden-node-connectivity-sysctl-overrides

Conversation

@thiras

@thiras thiras commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Context

The shared baseline role applies devsec.hardening.os_hardening with no overrides. Investigation found three of its CIS defaults degrade or endanger a deCDN node — the firewall path itself (nftables default-deny + udp/4433 QUIC hole, loopback metrics/admin) is coherent, but hardening can silently break node reachability.

Problems

  1. IPv6 RA/autoconf disabled. os_hardening zeroes the whole RA path — accept_ra, autoconf, and accept_ra_pinfo / accept_ra_defrtr. On SLAAC-addressed hosts (common on VPS) the node loses its IPv6 address and default route on the next RA cycle / reboot. Re-enabling accept_ra/autoconf alone is insufficient — without accept_ra_pinfo no prefix is learned (no address) and without accept_ra_defrtr no default route is installed.
  2. Strict reverse-path filtering (rp_filter=1) silently drops inbound QUIC on multi-homed / policy-routed hosts.
  3. ufw foot-gun. os_hardening templates a DROP-policy /etc/default/ufw (inert, but invites an operator to enable ufw and flush the nftables ruleset).

Fix

Feed os_hardening a sysctl_overwrite (it merges last, so it wins only for the relaxed keys — everything else keeps the hardened value) via two opt-in knobs with node-safe defaults, plus disable its ufw template:

  • baseline_preserve_ipv6_autoconf (default true) — re-enables the full RA path SLAAC needs, incl. accept_ra_pinfo/accept_ra_defrtr on default so a rebooted host reacquires address + route. Set false for static-IPv6 hosts.
  • baseline_rp_filter_loose (default false) — optionally sets rp_filter=2 for multi-homed nodes.
  • ufw_manage_defaults: false — stops the misleading /etc/default/ufw write.

An is boolean assert on both knobs fails loud instead of letting a mistyped/quoted value silently coerce to false (matching the existing guards in roles/decdn_node/tasks/main.yml).

Review

Findings 1 (IPv6 incompleteness) and 2 (boolean guard) were surfaced by a multi-agent PR review and are folded into this branch. The ufw wording and a container-VPS (lxc/openvz) no-op caveat were tightened in the docs.

Verification

  • ansible-playbook playbooks/site.yml --syntax-check → OK
  • ansible-lint roles/baseline playbooks/site.yml → 0 failures (production profile)
  • combine expression runtime-checked across knob states (default = 9 keys incl. pinfo=1/defrtr=1; both-off = {} safe no-op); boolean assert verified
  • Operator step (not CI-testable — os_hardening skips sysctl in containers): on a rebooted SLAAC VPS, confirm the global IPv6 address (ip -6 addr) and default route (ip -6 route) survive, and ss -lun | grep 4433 stays reachable.

🤖 Generated with Claude Code

devsec.hardening.os_hardening applies CIS network sysctls (and a ufw
config template) that can sever a deCDN node's connectivity. baseline
applied it with no overrides, so three defaults degraded/endangered a
node:

- IPv6 RA/autoconf disabled (accept_ra/autoconf=0, plus accept_ra_pinfo/
  accept_ra_defrtr=0): SLAAC-addressed hosts (common on VPS) lose their
  IPv6 address + default route on the next RA cycle / reboot.
- Strict reverse-path filtering (rp_filter=1) silently drops inbound
  QUIC on multi-homed / policy-routed hosts.
- os_hardening templates a DROP-policy /etc/default/ufw, inviting an
  operator to enable ufw and flush the nftables ruleset.

Feed os_hardening a sysctl_overwrite (merged last, so it wins only for
the relaxed keys) via two opt-in knobs with node-safe defaults, and
disable its ufw template:

- baseline_preserve_ipv6_autoconf (default true) re-enables the full RA
  path SLAAC needs, incl. accept_ra_pinfo/accept_ra_defrtr on `default`
  so a rebooted host reacquires its address + route.
- baseline_rp_filter_loose (default false) optionally sets rp_filter=2.
- ufw_manage_defaults: false stops the misleading /etc/default/ufw write.

An is-boolean assert on both knobs fails loud instead of letting a
mistyped value silently coerce to false (matching the decdn_node
guards). Documented in ansible/README.md and roles/baseline/README.md,
incl. the container-VPS (lxc/openvz) no-op caveat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 11, 2026 21:34
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thiras, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a1c2bbd4-5151-4ee6-a919-3c8c6db68887

📥 Commits

Reviewing files that changed from the base of the PR and between 8fcbb5e and 85405de.

📒 Files selected for processing (4)
  • ansible/README.md
  • ansible/roles/baseline/README.md
  • ansible/roles/baseline/defaults/main.yml
  • ansible/roles/baseline/tasks/main.yml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch harden-node-connectivity-sysctl-overrides

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces sysctl overrides for DevSec os_hardening in the Ansible baseline role to prevent security hardening from disrupting node connectivity. Specifically, it adds configuration options to preserve IPv6 RA/autoconf (SLAAC) and to optionally loosen reverse-path filtering for multi-homed hosts, alongside boolean validation for these settings. It also disables the default UFW management since nftables is used as the firewall. The review feedback highlights an issue where setting router_solicitations to -1 disables Router Solicitations, potentially causing significant delays in acquiring IPv6 addresses; it is recommended to change this value to the kernel default of 3.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +288 to +289
net.ipv6.conf.all.router_solicitations: -1
net.ipv6.conf.default.router_solicitations: -1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Setting router_solicitations to -1 disables sending Router Solicitations in the Linux kernel. In Linux, any value less than or equal to 0 for router_solicitations is treated as disabled, meaning the host will not actively solicit Router Advertisements (RAs) on boot or interface up. This can cause significant delays (up to 10 minutes) in acquiring a SLAAC IPv6 address and default route, as the host must wait for the next unsolicited periodic RA from the router. To ensure prompt IPv6 autoconfiguration, this should be set to the Linux kernel default of 3.

      net.ipv6.conf.all.router_solicitations: 3
      net.ipv6.conf.default.router_solicitations: 3

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified on a stock (non-hardened) host: net.ipv6.conf.{all,default}.router_solicitations defaults to -1, not 3. Per the kernel ip-sysctl semantics, -1 = unlimited Router Solicitations (keep sending until a router answers) and 0 = send none. os_hardening sets it to 0 — that 0 is the "wait for the next unsolicited RA" delay described here; this override restores -1, the kernel default and the most robust value for prompt SLAAC acquisition. 3 would send only 3 RS then stop, so -1 is strictly better here — keeping it. (RFC 4861 MAX_RTR_SOLICITATIONS=3 is a protocol constant, but Linux's sysctl default is -1.)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts the Ansible baseline role’s DevSec os_hardening integration to preserve deCDN node connectivity under CIS hardening by applying targeted sysctl overrides (IPv6 RA/SLAAC and optional loose rp_filter) and disabling os_hardening’s ufw defaults template.

Changes:

  • Add boolean validation and computed sysctl_overwrite inputs to devsec.hardening.os_hardening to keep IPv6 SLAAC and (optionally) multi-homed QUIC reachability working.
  • Disable os_hardening’s ufw defaults management to avoid writing a misleading /etc/default/ufw.
  • Document the new baseline knobs and the rationale in the baseline role README and top-level ansible/README.md.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
ansible/roles/baseline/tasks/main.yml Adds boolean guards and assembles/passes sysctl_overwrite, plus disables ufw defaults in os_hardening.
ansible/roles/baseline/README.md Documents connectivity-preserving hardening overrides and new role variables.
ansible/roles/baseline/defaults/main.yml Introduces defaulted knobs for IPv6 RA/autoconf preservation and optional loose rp_filter.
ansible/README.md Updates security model documentation to reflect the hardening overrides and ufw template disablement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +254 to +257
# The `| bool` filter is non-strict: an unparseable value (typo, quoted string, trailing
# space) coerces to false with no error — which would silently DROP the connectivity
# override and let CIS hardening sever IPv6/QUIC. Fail loud instead, matching the
# `is boolean` guards in roles/decdn_node/tasks/main.yml.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ansible's | bool is not Jinja truthiness. {{ x | bool }} calls boolean(x, strict=False), which returns True/False only for the recognized token sets and False for everything else. Verified against this repo's Ansible: 'flase'|bool, 'yes please'|bool, 'true '|bool, ' no '|bool all evaluate to False. So an unrecognized/typo'd value drops toward false → the override is skipped and CIS hardening is applied → the "silently DROP the connectivity override" failure the comment describes. Native Jinja {% if 'garbage' %} is truthy, but | bool is not that. Keeping the comment as written.

Comment on lines +263 to +267
fail_msg: >-
baseline_preserve_ipv6_autoconf and baseline_rp_filter_loose must be real
booleans (true/false), not quoted strings — each gates a `| bool` conditional
that decides whether the node-connectivity sysctl overrides apply. A mistyped
value silently coerces to false and lets CIS hardening sever IPv6/QUIC.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same basis as the comment above: | bool returns False for unrecognized strings in this repo's Ansible ('flase'|bool → False, etc.), so "silently coerces to false" is accurate for the | bool filter (not Jinja truthiness). The assert itself uses is boolean, which is stricter still — it rejects even recognized strings like 'true'/'yes', forcing a real bool. Leaving the fail_msg as-is.

@thiras
thiras merged commit 22e8edd into main Jul 11, 2026
10 checks passed
@thiras
thiras deleted the harden-node-connectivity-sysctl-overrides branch July 11, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants