RAIL-HO-WP-0014: explicit allowed supplementary groups for restricted agents (gexec-review)
The account task set groups: [] with append: false, so the next run would have removed custodian-agt from gated-executor's exchange group. Allowed groups are now an explicit per-account inventory list, refused if forbidden, and asserted as the exact set by goss; container test covers an unlisted group. 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
This commit is contained in:
parent
6c3a4164b7
commit
37ed8b012b
5 changed files with 56 additions and 6 deletions
|
|
@ -28,3 +28,9 @@ ssh_principals:
|
|||
# forwarding/tunnel/user RC). Each must also appear under users above.
|
||||
restricted_accounts:
|
||||
- custodian-agt
|
||||
# The only supplementary groups a restricted account may hold. gexec-review
|
||||
# lets the agent exchange proposals/approvals with gated-executor
|
||||
# (GEXEC-WP-0001); it carries no privilege. Asserted as the exact set.
|
||||
restricted_account_groups:
|
||||
custodian-agt:
|
||||
- gexec-review
|
||||
|
|
|
|||
|
|
@ -79,6 +79,33 @@
|
|||
else [])
|
||||
}}
|
||||
|
||||
- name: Resolve allowed supplementary groups of restricted agent accounts
|
||||
tags: [ssh_ca, restricted_agent_account]
|
||||
ansible.builtin.set_fact:
|
||||
ssh_ca_host_restricted_groups: >-
|
||||
{{
|
||||
(ssh_principals[inventory_hostname].restricted_account_groups | default({})
|
||||
if ssh_principals is defined
|
||||
and inventory_hostname in ssh_principals
|
||||
else {})
|
||||
}}
|
||||
|
||||
- name: Refuse forbidden groups in the allowed supplementary group list
|
||||
tags: [ssh_ca, restricted_agent_account]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (ssh_ca_host_restricted_groups.get(item, []) | intersect(ssh_ca_host_restricted_agent_forbidden_groups)) | length == 0
|
||||
fail_msg: "Restricted account {{ item }} lists a forbidden group."
|
||||
loop: "{{ ssh_ca_host_restricted_accounts }}"
|
||||
|
||||
- name: Ensure allowed supplementary groups exist
|
||||
tags: [ssh_ca, restricted_agent_account]
|
||||
ansible.builtin.group:
|
||||
name: "{{ item }}"
|
||||
system: true
|
||||
state: present
|
||||
loop: "{{ ssh_ca_host_restricted_groups.values() | flatten | unique }}"
|
||||
|
||||
- name: Require restricted accounts to be certificate-mapped and distinct from operators
|
||||
tags: [ssh_ca, restricted_agent_account]
|
||||
ansible.builtin.assert:
|
||||
|
|
@ -99,12 +126,12 @@
|
|||
state: present
|
||||
loop: "{{ ssh_ca_host_restricted_accounts }}"
|
||||
|
||||
- name: Ensure restricted agent account (locked password, no supplementary groups)
|
||||
- name: Ensure restricted agent account (locked password, only allowed supplementary groups)
|
||||
tags: [ssh_ca, restricted_agent_account]
|
||||
ansible.builtin.user:
|
||||
name: "{{ item }}"
|
||||
group: "{{ item }}"
|
||||
groups: []
|
||||
groups: "{{ ssh_ca_host_restricted_groups.get(item, []) }}"
|
||||
append: false
|
||||
password: "!"
|
||||
update_password: always
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ user:
|
|||
# goss treats this as containment; "id -nG" below asserts the exact set.
|
||||
groups:
|
||||
- {{ account }}
|
||||
{% for group in ssh_ca_host_restricted_groups.get(account, []) %}
|
||||
- {{ group }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
file:
|
||||
|
|
@ -65,7 +68,7 @@ command:
|
|||
"id -nG {{ account }}":
|
||||
exit-status: 0
|
||||
stdout:
|
||||
- "/^{{ account }}$/"
|
||||
- "/^{{ ([account] + ssh_ca_host_restricted_groups.get(account, [])) | join(' ') }}$/"
|
||||
{% for group in ssh_ca_host_restricted_agent_forbidden_groups %}
|
||||
- "!/\\b{{ group }}\\b/"
|
||||
{% endfor %}
|
||||
|
|
|
|||
4
tests/fixtures/restricted_agent_container.sh
vendored
4
tests/fixtures/restricted_agent_container.sh
vendored
|
|
@ -23,6 +23,9 @@ echo 'tegwick ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/tegwick
|
|||
groupadd custodian-agt
|
||||
useradd -m -g custodian-agt -s /bin/bash -d /home/custodian-agt custodian-agt
|
||||
usermod -p '!' custodian-agt
|
||||
# Mirror ssh_principals restricted_account_groups (gated-executor exchange group).
|
||||
groupadd --system gexec-review
|
||||
usermod -aG gexec-review custodian-agt
|
||||
chmod 0700 /home/custodian-agt
|
||||
echo agt-custodian > /etc/ssh/auth_principals/custodian-agt
|
||||
echo 'apiVersion: v1' > /etc/rancher/k3s/k3s.yaml
|
||||
|
|
@ -40,6 +43,7 @@ expect_fail() {
|
|||
}
|
||||
chmod 0644 /etc/rancher/k3s/k3s.yaml; expect_fail "k3s.yaml world-readable"; chmod 0600 /etc/rancher/k3s/k3s.yaml
|
||||
usermod -aG sudo custodian-agt; expect_fail "sudo group"; gpasswd -d custodian-agt sudo >/dev/null
|
||||
groupadd unlisted-extra; usermod -aG unlisted-extra custodian-agt; expect_fail "unlisted supplementary group"; gpasswd -d custodian-agt unlisted-extra >/dev/null
|
||||
echo 'custodian-agt ALL=(ALL) ALL' > /etc/sudoers.d/custodian-agt; expect_fail "sudoers entry"; rm /etc/sudoers.d/custodian-agt
|
||||
sed -i 's/X11Forwarding no/X11Forwarding yes/; s/DisableForwarding yes/DisableForwarding no/' /etc/ssh/sshd_config.d/05-restricted-agents.conf
|
||||
expect_fail "forwarding re-enabled"
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ def railiance01_context() -> dict:
|
|||
ctx = dict(_load(ROLE / "defaults" / "main.yml"))
|
||||
ctx["ssh_ca_host_restricted_accounts"] = host["restricted_accounts"]
|
||||
ctx["ssh_ca_host_principals"] = host["users"]
|
||||
ctx["ssh_ca_host_restricted_groups"] = host.get("restricted_account_groups", {})
|
||||
return ctx
|
||||
|
||||
|
||||
|
|
@ -101,15 +102,22 @@ class RestrictedAgentRoleTests(unittest.TestCase):
|
|||
self.assertEqual(len(matches), 1, name)
|
||||
return matches[0]
|
||||
|
||||
def test_account_has_locked_password_and_no_supplementary_groups(self) -> None:
|
||||
user = self._task("Ensure restricted agent account (locked password, no supplementary groups)")[
|
||||
def test_account_has_locked_password_and_only_allowed_groups(self) -> None:
|
||||
user = self._task("Ensure restricted agent account (locked password, only allowed supplementary groups)")[
|
||||
"ansible.builtin.user"
|
||||
]
|
||||
self.assertEqual(user["groups"], [])
|
||||
# Groups come only from the explicit per-account inventory list.
|
||||
self.assertEqual(user["groups"], "{{ ssh_ca_host_restricted_groups.get(item, []) }}")
|
||||
self.assertIs(user["append"], False)
|
||||
self.assertEqual(user["password"], "!")
|
||||
self.assertEqual(user["update_password"], "always")
|
||||
|
||||
def test_allowed_groups_are_explicit_and_not_forbidden(self) -> None:
|
||||
groups = railiance01_context()["ssh_ca_host_restricted_groups"]
|
||||
self.assertEqual(groups, {ACCOUNT: ["gexec-review"]})
|
||||
for allowed in groups[ACCOUNT]:
|
||||
self.assertNotIn(allowed, REQUIRED_FORBIDDEN_GROUPS)
|
||||
|
||||
def test_home_0700_no_sudoers_no_authorized_keys(self) -> None:
|
||||
home = self._task("Restrict restricted agent home to the account (0700)")["ansible.builtin.file"]
|
||||
self.assertEqual(home["mode"], "0700")
|
||||
|
|
@ -157,6 +165,8 @@ class RestrictedAgentRenderTests(unittest.TestCase):
|
|||
commands = spec["command"]
|
||||
self.assertIn(f"sudo -n -l -U {ACCOUNT}", commands)
|
||||
idg = commands[f"id -nG {ACCOUNT}"]["stdout"]
|
||||
# Exact set: the account's own group plus gexec-review, nothing else.
|
||||
self.assertIn(f"/^{ACCOUNT} gexec-review$/", idg)
|
||||
for group in REQUIRED_FORBIDDEN_GROUPS:
|
||||
self.assertIn(f"!/\\b{group}\\b/", idg)
|
||||
self.assertTrue(any("k3s.yaml" in c and "runuser" in c for c in commands))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue