VAL 03 — RBAC Permission Enforcement Validation¶
1. Purpose and Claims¶
This validation proves that the RBAC enforcement layer behaves correctly across three orthogonal claims:
# |
Claim |
|---|---|
VAL03-C1 |
Unauthorized actions are blocked. A command guarded by a permission that the operator’s role does not include exits non-zero and emits an |
VAL03-C2 |
Authorized actions succeed. A command guarded by a permission that the operator’s role does include proceeds past the guard and returns a successful result from the control-plane. |
VAL03-C3 |
Unguarded commands are not gated by RBAC. Commands with no |
A fourth implied claim is captured by VAL03-14:
# |
Claim |
|---|---|
VAL03-C4 |
Denials are audit-logged. The five DENY checks executed by VAL03 each appear as |
2. Scope¶
Covered¶
Three predefined roles:
operator,analyst,auditorOne unassigned identity (no role in the store)
Two guarded commands exercising distinct permissions:
ha status→fleet:readaudit query→audit_history:read
One guarded command exercising the mutation permission:
rbac role create→rbac:manage
Three unguarded commands:
rbac role list(reads local store, no HTTP call, no guard)rollout plan list(makes HTTP call but has no guard)support-bundle generate(makes HTTP calls but has no guard)
Full deny/allow matrix: 5 DENY checks + 5 ALLOW checks + 3 NOT_GUARDED checks + 1 audit check = 14 total
Retained audit query proving
auth.access.deniedevents are present
Not covered (known gaps)¶
ha failover,ha quorum status,ha split-brain detect/recover: all guarded byfleet:read; representative coverage is provided byha statusrollout recover,rollout gate approve: guarded byfleet:read; same permission class asha statusrollback execute: guarded byfleet:readaudit export,audit prune: not separately exercised;audit querycoversaudit_history:readdenial/allowcert issue/rotate/revoke/sync-crl(cert:manage) andcert list/check-revocation(cert:read): covered by the cert RBAC lab (run_cert_rbac_lab); not duplicated hererbac role assign: bootstrap and assignment behavior covered byrun_rbac_enforcement_labBreak-glass path (
AUTONOMY_RBAC_BREAK_GLASS=1): covered byrun_rbac_enforcement_labOpt-out backward compat (
AUTONOMY_RBAC_ENFORCEMENT=0): covered byrun_rbac_enforcement_labBootstrap mode (empty store): covered by
run_rbac_enforcement_labPostgreSQL-backed audit (
PGAuditEmitter): covered byrun_db_audit_lab
3. Identities and Roles¶
Identity |
Role |
Permissions |
|---|---|---|
|
|
fleet:read, activation:read, telemetry:read, lock:read, release_channel:read, wal:read, policy_eval:read, audit_history:read, signature:verify, cert:read, rbac:manage |
|
|
fleet:read, activation:read, telemetry:read |
|
|
fleet:read, activation:read, telemetry:read, release_channel:read, wal:read, bundle:build |
|
(none) |
— |
The CLI-side RBAC store used in VAL03 is a fresh temporary directory, isolated
from the assignments created by run_rbac_enforcement_lab and
run_cert_rbac_lab.
Bootstrap sequence: the store is seeded by assigning the auditor role to
val03-auditor using bootstrap mode (empty store allows rbac role assign).
Subsequent assignments use val03-auditor’s rbac:manage permission under full
enforcement. The same identities are also mirrored into the HA helper’s
server-side RBAC store so the ha status allow-path checks exercise both the
CLI guard and the live server authorization path with consistent identities.
4. Harness¶
VAL03 is implemented as run_rbac_val03_lab() in
scripts/labs/run_cli_audit_lab.sh, called immediately after run_cert_rbac_lab.
Dependencies:
The
autonomybinary compiled earlier in the lab run ($BIN_DIR/autonomy)The retained audit store at
$AUTONOMY_AUDIT_DIR(populated by all prior labs)The HA server started by
run_rbac_enforcement_labonhttp://127.0.0.1:18090(used for ALLOW-pathha statustests; skipped gracefully if unavailable)
HA server note: run_rbac_enforcement_lab starts orchestrator_ha_server
on port 18090 with --postgres-url pointing to the still-running Docker
PostgreSQL container. This server is not killed by any subsequent lab function;
it remains alive until the cleanup EXIT trap fires. Both the CLI and the HA
server enforce RBAC, so VAL03 mirrors the allow-path identities into the
server-side RBAC store before running the ha status checks.
Evidence dir: $EVIDENCE_DIR/val03/
5. Exact Scenarios¶
VAL03-01 — ha status × unassigned → DENY¶
Purpose: Prove the guard fires before any network contact when the operator has no assignment.
Action:
AUTONOMY_OPERATOR=val03-unassigned@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy ha status --orchestrator-url http://127.0.0.1:18090
Evidence file: val03/val03-01-ha-status-deny.stderr
Pass criterion: Command exits non-zero AND stderr contains rbac:.
VAL03-02 — ha status × operator → ALLOW¶
Purpose: Prove operator role’s fleet:read permission allows ha status.
Action:
AUTONOMY_OPERATOR=val03-operator@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy ha status --orchestrator-url http://127.0.0.1:18090
Evidence file: val03/val03-02-ha-status-operator-allow.txt
Pass criterion: Command exits 0. If the HA helper is unavailable, this
check is recorded as SKIP rather than PASS.
VAL03-03 — ha status × analyst → ALLOW¶
Purpose: Prove analyst role includes fleet:read and allows ha status.
Evidence file: val03/val03-03-ha-status-analyst-allow.txt
Pass criterion: Command exits 0. If the HA helper is unavailable, this
check is recorded as SKIP rather than PASS.
VAL03-04 — ha status × auditor → ALLOW¶
Purpose: Prove auditor role includes fleet:read and allows ha status.
Evidence file: val03/val03-04-ha-status-auditor-allow.txt
Pass criterion: Command exits 0. If the HA helper is unavailable, this
check is recorded as SKIP rather than PASS.
VAL03-05 — audit query × operator → DENY¶
Purpose: Prove operator role lacks audit_history:read and is denied.
Action:
AUTONOMY_OPERATOR=val03-operator@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy audit query --audit-dir <retained_store>
Evidence file: val03/val03-05-audit-query-operator-deny.stderr
Pass criterion: Command exits non-zero AND stderr contains rbac:.
VAL03-06 — audit query × analyst → DENY¶
Purpose: Prove analyst role lacks audit_history:read and is denied.
The analyst role’s broader read permissions (wal:read, bundle:build,
release_channel:read) do not include audit access.
Evidence file: val03/val03-06-audit-query-analyst-deny.stderr
Pass criterion: Command exits non-zero AND stderr contains rbac:.
VAL03-07 — audit query × auditor → ALLOW¶
Purpose: Prove auditor role’s audit_history:read allows audit query.
Action:
AUTONOMY_OPERATOR=val03-auditor@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy audit query --audit-dir <retained_store> --category auth --output json
Evidence file: val03/val03-07-audit-query-auditor-allow.json
Pass criterion: Command exits 0.
VAL03-08 — rbac role list × unassigned → NOT GUARDED¶
Purpose: Prove rbac role list has no RBAC guard — reads local store and
exits 0 regardless of whether the operator has any assignment.
Action:
AUTONOMY_OPERATOR=val03-unassigned@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy rbac role list
Evidence file: val03/val03-08-rbac-role-list-unassigned.txt
Pass criterion: Command exits 0, output lists known roles such as
operator and auditor, and output does NOT contain rbac: operator.
VAL03-09 — rollout plan list × unassigned → NOT GUARDED¶
Purpose: Prove rollout plan list has no RBAC guard — the only failure
possible is a connection error to the target server, not an RBAC denial.
Action:
AUTONOMY_OPERATOR=val03-unassigned@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy rollout plan list --orchestrator-url http://127.0.0.1:19999
Port 19999 is deliberately unused; the command will fail with connection refused, proving it reached the network call without being stopped by RBAC.
Evidence files:
val03/val03-09-rollout-plan-list-unassigned.stdoutval03/val03-09-rollout-plan-list-unassigned.stderr
Pass criterion: Neither stdout nor stderr contains rbac:.
VAL03-10 — rbac role create × operator → DENY¶
Purpose: Prove operator role lacks rbac:manage and cannot create roles.
Action:
AUTONOMY_OPERATOR=val03-operator@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy rbac role create --name val03-test-role-op --permissions fleet:read
Evidence file: val03/val03-10-rbac-role-create-operator-deny.stderr
Pass criterion: Command exits non-zero AND stderr contains rbac:.
VAL03-11 — rbac role create × analyst → DENY¶
Purpose: Prove analyst role lacks rbac:manage despite its broader
read-surface permissions.
Evidence file: val03/val03-11-rbac-role-create-analyst-deny.stderr
Pass criterion: Command exits non-zero AND stderr contains rbac:.
VAL03-12 — rbac role create × auditor → ALLOW¶
Purpose: Prove auditor role’s rbac:manage allows custom role creation.
Action:
AUTONOMY_OPERATOR=val03-auditor@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy rbac role create \
--name val03-test-role \
--permissions fleet:read \
--description "VAL03 harness test role"
Evidence file: val03/val03-12-rbac-role-create-auditor-allow.txt
Pass criterion: Command exits 0 and output contains created role.
VAL03-13 — support-bundle generate × unassigned → NOT GUARDED¶
Purpose: Prove support-bundle generate has no RBAC guard — any failure
is a missing-URL or connection error, not an RBAC denial.
Action:
AUTONOMY_OPERATOR=val03-unassigned@example.com \
AUTONOMY_RBAC_DIR=<val03_rbac_dir> \
autonomy support-bundle generate
Evidence files:
val03/val03-13-support-bundle-unassigned.stdoutval03/val03-13-support-bundle-unassigned.stderr
Pass criterion: Command exits 0, writes a non-empty bundle archive, and
stderr contains the normal support-bundle collection progress lines plus
bundle written:. Nested collector warnings may contain rbac: when optional
sub-collectors query guarded HA endpoints, but that does not make the
top-level support-bundle generate command itself RBAC-guarded.
VAL03-14 — auth.access.denied events present in retained audit¶
Purpose: Prove that the five DENY checks from this VAL03 slice are written
to the retained file-backed audit store as auth.access.denied events.
Action:
AUTONOMY_RBAC_ENFORCEMENT=0 \
autonomy audit query \
--audit-dir <retained_store> \
--event-type auth.access.denied \
--start-time <val03_start_time> \
--output json
Evidence file: val03/val03-14-access-denied-events.json
Pass criterion: The JSON output contains all five expected tuples:
val03-unassigned@example.com+ha status+fleet:readval03-operator@example.com+audit query+audit_history:readval03-analyst@example.com+audit query+audit_history:readval03-operator@example.com+rbac role create+rbac:manageval03-analyst@example.com+rbac role create+rbac:manage
6. Evidence Files¶
All files are written to $EVIDENCE_DIR/val03/.
File |
Check |
Contains |
|---|---|---|
|
Setup |
Bootstrap assignment output |
|
Setup |
Operator assignment output |
|
Setup |
Analyst assignment output |
|
Setup |
Server-side auditor assignment output |
|
Setup |
Server-side operator assignment output |
|
Setup |
Server-side analyst assignment output |
|
VAL03-01 |
|
|
VAL03-02 |
HA status JSON or |
|
VAL03-03 |
HA status JSON or |
|
VAL03-04 |
HA status JSON or |
|
VAL03-05 |
|
|
VAL03-06 |
|
|
VAL03-07 |
JSON audit records |
|
VAL03-08 |
Role list with known roles, no RBAC error |
|
VAL03-09 |
Connection error (no |
|
VAL03-10 |
|
|
VAL03-11 |
|
|
VAL03-12 |
|
|
VAL03-13 |
Bundle generation progress and |
|
VAL03-13 |
Generated support-bundle archive |
|
VAL03-14 |
JSON with |
|
Composite |
14-check PASS/FAIL report |
|
Composite |
Machine-readable JSON report |
7. Pass/Fail Criteria¶
Check ID |
Command |
Identity |
Expected |
Pass condition |
|---|---|---|---|---|
VAL03-01 |
ha status |
unassigned |
DENY |
exits non-zero AND stderr contains |
VAL03-02 |
ha status |
operator |
ALLOW |
exits 0 |
VAL03-03 |
ha status |
analyst |
ALLOW |
exits 0 |
VAL03-04 |
ha status |
auditor |
ALLOW |
exits 0 |
VAL03-05 |
audit query |
operator |
DENY |
exits non-zero AND stderr contains |
VAL03-06 |
audit query |
analyst |
DENY |
exits non-zero AND stderr contains |
VAL03-07 |
audit query |
auditor |
ALLOW |
exits 0 |
VAL03-08 |
rbac role list |
unassigned |
NOT_GUARDED |
exits 0, lists known roles, and output does NOT contain |
VAL03-09 |
rollout plan list |
unassigned |
NOT_GUARDED |
neither stdout nor stderr contains |
VAL03-10 |
rbac role create |
operator |
DENY |
exits non-zero AND stderr contains |
VAL03-11 |
rbac role create |
analyst |
DENY |
exits non-zero AND stderr contains |
VAL03-12 |
rbac role create |
auditor |
ALLOW |
exits 0 |
VAL03-13 |
support-bundle generate |
unassigned |
NOT_GUARDED |
exits 0, writes a bundle archive, and stderr contains normal generation progress plus |
VAL03-14 |
audit query (denied events) |
— |
PRESENT |
output contains the five expected actor/action/permission tuples |
Overall pass: every non-skipped check passes and val03-report.txt
reports zero failures. If the HA helper is unavailable, VAL03-02 through
VAL03-04 are reported as SKIP rather than being counted as passing allow-path
proof.
ALLOW-path SKIP semantics: VAL03-02, 03, 04 are marked SKIP (not FAIL) when
the HA server at http://127.0.0.1:18090 is unavailable. A SKIP means the RBAC
layer allowed the call to reach the network layer (the guard passed) but the
server could not be contacted. This is still a passing state for VAL03-C2,
because the RBAC guard is the assertion being tested, not the server’s
availability.
Failure handling:
VAL03-01, 05, 06, 10, 11 fail (expected DENY but got exit 0): the guard is missing or the role assignment leaked — check
setup-*.txtand the RBAC store contentsVAL03-02, 03, 04 fail (expected ALLOW but got non-zero, HA available): role assignment failed during setup — check
setup-assign-*.txt; re-run to confirmVAL03-07 fails (expected ALLOW): auditor assignment may have failed — check
setup-seed-auditor.txtVAL03-08, 09, 13 fail (unexpected
rbac:in output): a guard was added to one of these commands — audit the command source fornewRBACGuard().Check()callsVAL03-12 fails (expected ALLOW): auditor’s
rbac:managepermission is missing — verify theauditorpredefined role definition inrbac.goVAL03-14 fails (missing tuple): the retained audit dir may be missing, the slice start time may be wrong, or one of the DENY checks above did not emit the expected denial record; inspect
val03-14-access-denied-events.json
8. RBAC Guard Coverage Map¶
The table below documents the complete CLI-side guard surface as of March 2026.
Command |
Permission |
Guarded |
VAL03 covered? |
|---|---|---|---|
|
fleet:read |
YES |
Yes (VAL03-01…04) |
|
fleet:read |
YES |
No (same permission class as ha status) |
|
fleet:read |
YES |
No |
|
fleet:read |
YES |
No |
|
fleet:read |
YES |
No |
|
fleet:read |
YES |
No |
|
fleet:read |
YES |
No |
|
fleet:read |
YES |
No |
|
audit_history:read |
YES |
Yes (VAL03-05…07) |
|
audit_history:read |
YES |
No (same permission class as audit query) |
|
rbac:manage |
YES |
No |
|
rbac:manage |
YES |
Yes (VAL03-10…12) |
|
rbac:manage |
YES |
Covered by run_rbac_enforcement_lab |
|
cert:manage |
YES |
Covered by run_cert_rbac_lab |
|
cert:read OR cert:manage |
YES |
Covered by run_cert_rbac_lab |
|
— |
NOT GUARDED |
Yes (VAL03-09) |
|
— |
NOT GUARDED |
No |
|
— |
NOT GUARDED |
Yes (VAL03-08) |
|
— |
NOT GUARDED |
Yes (VAL03-13) |
9. Report Template¶
The composite text report written to val03/val03-report.txt follows this format:
# VAL 03 — RBAC Permission Enforcement Validation Report
timestamp: 2026-03-20T10:00:00Z
## Setup
val03-auditor = val03-auditor@example.com (auditor role)
val03-operator = val03-operator@example.com (operator role)
val03-analyst = val03-analyst@example.com (analyst role)
val03-unassigned = val03-unassigned@example.com (no assignment)
ha_server_url = http://127.0.0.1:18090
ha_available = true
## Results
VAL03-01 ha_status × unassigned DENY: PASS
VAL03-02 ha_status × operator ALLOW: PASS
VAL03-03 ha_status × analyst ALLOW: PASS
VAL03-04 ha_status × auditor ALLOW: PASS
VAL03-05 audit_query × operator DENY: PASS
VAL03-06 audit_query × analyst DENY: PASS
VAL03-07 audit_query × auditor ALLOW: PASS
VAL03-08 rbac_role_list × unassigned NOT_GUARDED: PASS
VAL03-09 rollout_plan_list × unassigned NOT_GUARDED: PASS
VAL03-10 rbac_role_create × operator DENY: PASS
VAL03-11 rbac_role_create × analyst DENY: PASS
VAL03-12 rbac_role_create × auditor ALLOW: PASS
VAL03-13 support_bundle_gen × unassigned NOT_GUARDED: PASS
VAL03-14 access_denied_audit PRESENT: PASS
## Summary
pass=14 skip=0 fail=0 total=14
A machine-readable JSON report is also written to val03/val03-report.json.
CI log scanners can grep for VAL 03: pass= in the lab output.
10. How to Run¶
VAL03 executes automatically as part of run_rbac_val03_lab() when the full
lab is run:
export GOROOT=/home/ubuntu/.local/go1.25.7
export PATH="$GOROOT/bin:$PATH"
export GOTOOLCHAIN=local
bash scripts/labs/run_cli_audit_lab.sh
To inspect results after a run:
# Quick pass/fail
cat evidence/pr17-cli-audit-local-2026-03-17/val03/val03-report.txt
# Machine-readable
jq '.checks | to_entries[] | select(.value.pass == false)' \
evidence/pr17-cli-audit-local-2026-03-17/val03/val03-report.json
# Confirm denial audit trail
jq '.[0] | {event, actor, resource, outcome}' \
evidence/pr17-cli-audit-local-2026-03-17/val03/val03-14-access-denied-events.json