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: []
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
base__mesh_enabled: true
|
||||
base__mesh_manage: false
|
||||
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:
|
||||
lan: 10.30.0.0/24
|
||||
srv: 10.20.0.0/24
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ platforms:
|
|||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
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:
|
||||
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
|
||||
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)
|
||||
- name: Check whether netbird got installed
|
||||
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
|
||||
ansible.builtin.apt:
|
||||
name: openssh-server
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@ PasswordAuthentication {{ base__ssh_password_authentication }}
|
|||
PermitRootLogin {{ base__ssh_permit_root_login }}
|
||||
PubkeyAuthentication yes
|
||||
KbdInteractiveAuthentication no
|
||||
{% if base__ssh_listen_mesh_only | bool %}
|
||||
ListenAddress {{ base__ssh_listen_addr_resolved }}
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue