feat(base): opt-in sshd ListenAddress on the mesh IP (fail-closed)
base__ssh_listen_mesh_only binds sshd to the live wt0 IP only, with
ip_nonlocal_bind to beat the post-boot bind race and a fail-closed assert so an
unresolved address never silently listens on all interfaces. Molecule covers
the render + sysctl. Mesh-hardening 1/3 (ADR-016/021).
Environmental checkpoint applied: the molecule-debian13 container image lacks
procps (no sysctl binary). Added molecule/default/prepare.yml to install procps
and sysctls: {net.ipv4.ip_nonlocal_bind: "0"} to molecule.yml platform so the
ansible.posix.sysctl task can write and read back the value hermetically.
Sysctl file format is net.ipv4.ip_nonlocal_bind=1 (no spaces); verify.yml
grep pattern updated to match ansible.posix.sysctl's actual output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dfa363cecd
commit
39d2ad38ca
7 changed files with 68 additions and 0 deletions
|
|
@ -21,6 +21,14 @@ base__fail2ban_findtime: 10m
|
||||||
# base__ssh_authorised_keys lives in group_vars/all/vars.yml (per-person control keys).
|
# base__ssh_authorised_keys lives in group_vars/all/vars.yml (per-person control keys).
|
||||||
base__ssh_authorised_keys: []
|
base__ssh_authorised_keys: []
|
||||||
|
|
||||||
|
# SSH listen-on-mesh (mesh-hardening 1/3, ADR-016/021). Opt-in: when true, sshd binds
|
||||||
|
# ListenAddress to this host's mesh IP only (not the WAN). The IP comes from the live wt0
|
||||||
|
# fact (ansible_facts.wt0.ipv4.address); base__ssh_listen_addr overrides it. ip_nonlocal_bind
|
||||||
|
# lets sshd bind the mesh IP before wt0 exists at boot. Fails closed: the play asserts a
|
||||||
|
# non-empty address rather than silently listening on all interfaces.
|
||||||
|
base__ssh_listen_mesh_only: false
|
||||||
|
base__ssh_listen_addr: ""
|
||||||
|
|
||||||
# NetBird mesh agent enrollment (ADR-016). Opt-in: default off so applying `base` to a
|
# NetBird mesh agent enrollment (ADR-016). Opt-in: default off so applying `base` to a
|
||||||
# host not on the mesh is a no-op for this concern. The live actions (apt install over
|
# host not on the mesh is a no-op for this concern. The live actions (apt install over
|
||||||
# the network, `netbird up` against the coordinator) are additionally gated by
|
# the network, `netbird up` against the coordinator) are additionally gated by
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
base__mesh_enabled: true
|
base__mesh_enabled: true
|
||||||
base__mesh_manage: false
|
base__mesh_manage: false
|
||||||
base__mesh_setup_key: "dummy-molecule-key"
|
base__mesh_setup_key: "dummy-molecule-key"
|
||||||
|
base__ssh_listen_mesh_only: true
|
||||||
|
base__ssh_listen_addr: "100.99.0.1" # fixture mesh IP (no wt0 in the container)
|
||||||
firewall_zones:
|
firewall_zones:
|
||||||
lan: 10.30.0.0/24
|
lan: 10.30.0.0/24
|
||||||
srv: 10.20.0.0/24
|
srv: 10.20.0.0/24
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ platforms:
|
||||||
volumes:
|
volumes:
|
||||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||||
command: /lib/systemd/systemd
|
command: /lib/systemd/systemd
|
||||||
|
# Pre-create the namespaced sysctl so ansible.posix.sysctl can set it (mesh-hardening 1/3).
|
||||||
|
# The container image lacks procps so the sysctl binary is absent; we also install it in
|
||||||
|
# prepare.yml. This entry ensures the value exists in the container's netns at startup.
|
||||||
|
sysctls:
|
||||||
|
net.ipv4.ip_nonlocal_bind: "0"
|
||||||
|
|
||||||
provisioner:
|
provisioner:
|
||||||
name: ansible
|
name: ansible
|
||||||
|
|
|
||||||
11
roles/base/molecule/default/prepare.yml
Normal file
11
roles/base/molecule/default/prepare.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
- name: Prepare
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Install procps so ansible.posix.sysctl can find the sysctl binary
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: procps
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
@ -58,6 +58,18 @@
|
||||||
ansible.builtin.command: grep -q '^\[sshd\]' /etc/fail2ban/jail.d/sshd.local
|
ansible.builtin.command: grep -q '^\[sshd\]' /etc/fail2ban/jail.d/sshd.local
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
|
- name: ListenAddress bound to the fixture mesh IP (mesh-only mode)
|
||||||
|
ansible.builtin.command: grep -q '^ListenAddress 100.99.0.1$' /etc/ssh/sshd_config.d/10-boma.conf
|
||||||
|
changed_when: false
|
||||||
|
- name: Sysctl drop-in for ip_nonlocal_bind is present
|
||||||
|
ansible.builtin.command: grep -q '^net.ipv4.ip_nonlocal_bind=1' /etc/sysctl.d/60-boma-nonlocal-bind.conf
|
||||||
|
changed_when: false
|
||||||
|
- name: Kernel ip_nonlocal_bind is live in this netns
|
||||||
|
ansible.builtin.command: sysctl -n net.ipv4.ip_nonlocal_bind
|
||||||
|
register: _nonlocal
|
||||||
|
changed_when: false
|
||||||
|
failed_when: _nonlocal.stdout | trim != '1'
|
||||||
|
|
||||||
# mesh concern: enabled but manage=false must be a clean no-op (no install/enrol)
|
# mesh concern: enabled but manage=false must be a clean no-op (no install/enrol)
|
||||||
- name: Check whether netbird got installed
|
- name: Check whether netbird got installed
|
||||||
ansible.builtin.command: which netbird
|
ansible.builtin.command: which netbird
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,31 @@
|
||||||
---
|
---
|
||||||
|
- name: Resolve the sshd mesh listen address (override, else live wt0 fact)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
base__ssh_listen_addr_resolved: >-
|
||||||
|
{{ base__ssh_listen_addr
|
||||||
|
or ansible_facts.get('wt0', {}).get('ipv4', {}).get('address', '') }}
|
||||||
|
when: base__ssh_listen_mesh_only | bool
|
||||||
|
|
||||||
|
- name: Fail closed — refuse to render sshd without a known mesh address
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- base__ssh_listen_addr_resolved | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
base__ssh_listen_mesh_only is true but no mesh address resolved (set
|
||||||
|
base__ssh_listen_addr or ensure wt0 is up so its fact is gathered). Refusing to
|
||||||
|
render sshd ListenAddress empty (which would listen on ALL interfaces).
|
||||||
|
when: base__ssh_listen_mesh_only | bool
|
||||||
|
|
||||||
|
- name: Allow sshd to bind the mesh IP before wt0 exists at boot
|
||||||
|
ansible.posix.sysctl:
|
||||||
|
name: net.ipv4.ip_nonlocal_bind
|
||||||
|
value: "1"
|
||||||
|
sysctl_set: true
|
||||||
|
state: present
|
||||||
|
reload: true
|
||||||
|
sysctl_file: /etc/sysctl.d/60-boma-nonlocal-bind.conf
|
||||||
|
when: base__ssh_listen_mesh_only | bool
|
||||||
|
|
||||||
- name: Ensure openssh-server is installed
|
- name: Ensure openssh-server is installed
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
name: openssh-server
|
name: openssh-server
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,6 @@ PasswordAuthentication {{ base__ssh_password_authentication }}
|
||||||
PermitRootLogin {{ base__ssh_permit_root_login }}
|
PermitRootLogin {{ base__ssh_permit_root_login }}
|
||||||
PubkeyAuthentication yes
|
PubkeyAuthentication yes
|
||||||
KbdInteractiveAuthentication no
|
KbdInteractiveAuthentication no
|
||||||
|
{% if base__ssh_listen_mesh_only | bool %}
|
||||||
|
ListenAddress {{ base__ssh_listen_addr_resolved }}
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue