Map SSH certificate principal agt-custodian to a dedicated custodian-agt account on Railiance01 only, converged by ssh_ca_host (tag restricted_agent_account): locked password, home 0700, no supplementary groups, no sudoers entry, no authorized_keys, and an sshd Match drop-in (05-restricted-agents.conf) with publickey only and all forwarding, tunnel and user RC off. A separate goss spec asserts the posture, including that the account cannot read the k3s kubeconfig and the sshd -T effective config. Offline tests (optional real-sshd container check). Existing tegwick mappings unchanged; their retirement is RAIL-HO-WP-0014-T03. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 605711@bnt-lap001 Assistant-Session: 1f5e2256-181f-49ed-b7b1-a1c34bf37a61
231 lines
7.5 KiB
YAML
231 lines
7.5 KiB
YAML
---
|
|
- name: Require SSH CA public key path
|
|
ansible.builtin.assert:
|
|
that:
|
|
- ssh_ca_pubkey_path is defined
|
|
- ssh_ca_pubkey_path | length > 0
|
|
fail_msg: >-
|
|
Set ssh_ca_pubkey_path to the OpenBao SSH CA public key file
|
|
(from railiance-platform openbao-configure-ssh).
|
|
|
|
- name: Stat SSH CA public key source
|
|
ansible.builtin.stat:
|
|
path: "{{ ssh_ca_pubkey_path }}"
|
|
delegate_to: localhost
|
|
become: false
|
|
register: ssh_ca_pubkey_stat
|
|
|
|
- name: Fail when SSH CA public key is missing
|
|
ansible.builtin.fail:
|
|
msg: "SSH CA public key not found on controller: {{ ssh_ca_pubkey_path }}"
|
|
when: not ssh_ca_pubkey_stat.stat.exists
|
|
|
|
- name: Ensure SSH CA directory exists
|
|
ansible.builtin.file:
|
|
path: /etc/ssh/ca
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
- name: Install SSH user CA public key
|
|
ansible.builtin.copy:
|
|
src: "{{ ssh_ca_pubkey_path }}"
|
|
dest: /etc/ssh/ca/ca_user.pub
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Restart sshd
|
|
|
|
- name: Configure SSH certificate trust
|
|
ansible.builtin.copy:
|
|
dest: /etc/ssh/sshd_config.d/20-ssh-ca.conf
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
content: |
|
|
TrustedUserCAKeys /etc/ssh/ca/ca_user.pub
|
|
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
|
|
notify: Restart sshd
|
|
|
|
- name: Ensure auth_principals directory exists
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.file:
|
|
path: /etc/ssh/auth_principals
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
- name: Resolve principals for this host
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.set_fact:
|
|
ssh_ca_host_principals: >-
|
|
{{
|
|
(ssh_principals[inventory_hostname].users
|
|
if ssh_principals is defined
|
|
and inventory_hostname in ssh_principals
|
|
else {})
|
|
}}
|
|
|
|
- name: Resolve restricted agent accounts for this host
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.set_fact:
|
|
ssh_ca_host_restricted_accounts: >-
|
|
{{
|
|
(ssh_principals[inventory_hostname].restricted_accounts | default([])
|
|
if ssh_principals is defined
|
|
and inventory_hostname in ssh_principals
|
|
else [])
|
|
}}
|
|
|
|
- name: Require restricted accounts to be certificate-mapped and distinct from operators
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.assert:
|
|
that:
|
|
- item in ssh_ca_host_principals
|
|
- ssh_ca_host_principals[item] | length > 0
|
|
- item not in ['root', 'tegwick', (baseline_user.name | default('tegwick'))]
|
|
fail_msg: >-
|
|
Restricted account {{ item }} must have principals under
|
|
ssh_principals.{{ inventory_hostname }}.users and must not be root or
|
|
the baseline operator user.
|
|
loop: "{{ ssh_ca_host_restricted_accounts }}"
|
|
|
|
- name: Ensure restricted agent account primary group
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.group:
|
|
name: "{{ item }}"
|
|
state: present
|
|
loop: "{{ ssh_ca_host_restricted_accounts }}"
|
|
|
|
- name: Ensure restricted agent account (locked password, no supplementary groups)
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.user:
|
|
name: "{{ item }}"
|
|
group: "{{ item }}"
|
|
groups: []
|
|
append: false
|
|
password: "!"
|
|
update_password: always
|
|
shell: "{{ ssh_ca_host_restricted_agent_shell }}"
|
|
home: "/home/{{ item }}"
|
|
create_home: true
|
|
state: present
|
|
loop: "{{ ssh_ca_host_restricted_accounts }}"
|
|
|
|
- name: Restrict restricted agent home to the account (0700)
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.file:
|
|
path: "/home/{{ item }}"
|
|
state: directory
|
|
owner: "{{ item }}"
|
|
group: "{{ item }}"
|
|
mode: "0700"
|
|
loop: "{{ ssh_ca_host_restricted_accounts }}"
|
|
# On a first check-mode run the account does not exist yet, so chown lookup fails.
|
|
ignore_errors: "{{ ansible_check_mode }}"
|
|
|
|
- name: Remove any authorized_keys for restricted agent accounts
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.file:
|
|
path: "/home/{{ item.0 }}/.ssh/{{ item.1 }}"
|
|
state: absent
|
|
loop: "{{ ssh_ca_host_restricted_accounts | product(['authorized_keys', 'authorized_keys2']) | list }}"
|
|
|
|
- name: Ensure no sudoers drop-in exists for restricted agent accounts
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.file:
|
|
path: "/etc/sudoers.d/{{ item }}"
|
|
state: absent
|
|
loop: "{{ ssh_ca_host_restricted_accounts }}"
|
|
|
|
- name: Configure sshd Match blocks for restricted agent accounts
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.template:
|
|
src: restricted-agents-sshd.conf.j2
|
|
dest: /etc/ssh/sshd_config.d/05-restricted-agents.conf
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
validate: /usr/sbin/sshd -t -f %s
|
|
when: ssh_ca_host_restricted_accounts | length > 0
|
|
notify: Restart sshd
|
|
|
|
- name: Deploy auth_principals files per user
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.copy:
|
|
dest: "/etc/ssh/auth_principals/{{ item.key }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
content: "{{ item.value | join('\n') }}\n"
|
|
loop: "{{ ssh_ca_host_principals | dict2items }}"
|
|
when: ssh_ca_host_principals | length > 0
|
|
notify: Restart sshd
|
|
|
|
- name: Warn when no principals configured for host
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
No principals in ssh_principals.yaml for {{ inventory_hostname }} —
|
|
CA trust installed; add users under hosts.<name>.users to enable cert login.
|
|
when: ssh_ca_host_principals | length == 0
|
|
|
|
- name: Validate the complete sshd configuration before any restart
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.command:
|
|
cmd: /usr/sbin/sshd -t
|
|
changed_when: false
|
|
when: ssh_ca_host_restricted_accounts | length > 0
|
|
|
|
- name: Ensure Goss config directory exists
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.file:
|
|
path: /etc/goss
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
when: ssh_ca_host_restricted_accounts | length > 0
|
|
|
|
- name: Install restricted agent Goss assertions
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.template:
|
|
src: "{{ playbook_dir }}/../../goss/restricted-agent.yaml.j2"
|
|
dest: /etc/goss/restricted-agent.yaml
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
when: ssh_ca_host_restricted_accounts | length > 0
|
|
|
|
- name: Apply sshd changes before asserting the effective configuration
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.meta: flush_handlers
|
|
|
|
- name: Stat Goss binary for restricted agent assertions
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.stat:
|
|
path: /usr/local/bin/goss
|
|
register: ssh_ca_host_goss_bin
|
|
when: ssh_ca_host_restricted_accounts | length > 0
|
|
|
|
- name: Assert restricted agent account posture (Goss)
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.command:
|
|
cmd: /usr/local/bin/goss -g /etc/goss/restricted-agent.yaml validate --format tap
|
|
register: ssh_ca_host_restricted_goss
|
|
changed_when: false
|
|
failed_when: false
|
|
when:
|
|
- ssh_ca_host_restricted_accounts | length > 0
|
|
- not ansible_check_mode
|
|
- ssh_ca_host_goss_bin.stat.exists | default(false)
|
|
|
|
- name: Fail closed when restricted agent assertions do not pass
|
|
tags: [ssh_ca, restricted_agent_account]
|
|
ansible.builtin.assert:
|
|
that:
|
|
- ssh_ca_host_restricted_goss.rc == 0
|
|
fail_msg: "{{ ssh_ca_host_restricted_goss.stdout_lines | select('match', '^not ok') | list }}"
|
|
success_msg: "restricted agent account assertions passed"
|
|
when: ssh_ca_host_restricted_goss is not skipped and ssh_ca_host_restricted_goss.rc is defined
|