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 auth.access.denied audit event before any network call is made.

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 newRBACGuard().Check() call proceed regardless of operator assignment — any failure is a connection or argument error, not an RBAC denial.

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 auth.access.denied in the retained file-backed audit store with the expected actor, action, and permission tuple.


2. Scope

Covered

  • Three predefined roles: operator, analyst, auditor

  • One unassigned identity (no role in the store)

  • Two guarded commands exercising distinct permissions:

    • ha statusfleet:read

    • audit queryaudit_history:read

  • One guarded command exercising the mutation permission:

    • rbac role createrbac: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.denied events are present

Not covered (known gaps)

  • ha failover, ha quorum status, ha split-brain detect/recover: all guarded by fleet:read; representative coverage is provided by ha status

  • rollout recover, rollout gate approve: guarded by fleet:read; same permission class as ha status

  • rollback execute: guarded by fleet:read

  • audit export, audit prune: not separately exercised; audit query covers audit_history:read denial/allow

  • cert issue/rotate/revoke/sync-crl (cert:manage) and cert list/check-revocation (cert:read): covered by the cert RBAC lab (run_cert_rbac_lab); not duplicated here

  • rbac role assign: bootstrap and assignment behavior covered by run_rbac_enforcement_lab

  • Break-glass path (AUTONOMY_RBAC_BREAK_GLASS=1): covered by run_rbac_enforcement_lab

  • Opt-out backward compat (AUTONOMY_RBAC_ENFORCEMENT=0): covered by run_rbac_enforcement_lab

  • Bootstrap mode (empty store): covered by run_rbac_enforcement_lab

  • PostgreSQL-backed audit (PGAuditEmitter): covered by run_db_audit_lab


3. Identities and Roles

Identity

Role

Permissions

val03-auditor@example.com

auditor

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

val03-operator@example.com

operator

fleet:read, activation:read, telemetry:read

val03-analyst@example.com

analyst

fleet:read, activation:read, telemetry:read, release_channel:read, wal:read, bundle:build

val03-unassigned@example.com

(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 autonomy binary 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_lab on http://127.0.0.1:18090 (used for ALLOW-path ha status tests; 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.stdout

  • val03/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.stdout

  • val03/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:read

  • val03-operator@example.com + audit query + audit_history:read

  • val03-analyst@example.com + audit query + audit_history:read

  • val03-operator@example.com + rbac role create + rbac:manage

  • val03-analyst@example.com + rbac role create + rbac:manage


6. Evidence Files

All files are written to $EVIDENCE_DIR/val03/.

File

Check

Contains

setup-seed-auditor.txt

Setup

Bootstrap assignment output

setup-assign-operator.txt

Setup

Operator assignment output

setup-assign-analyst.txt

Setup

Analyst assignment output

setup-server-assign-auditor.txt

Setup

Server-side auditor assignment output

setup-server-assign-operator.txt

Setup

Server-side operator assignment output

setup-server-assign-analyst.txt

Setup

Server-side analyst assignment output

val03-01-ha-status-deny.stderr

VAL03-01

rbac: denial message

val03-02-ha-status-operator-allow.txt

VAL03-02

HA status JSON or SKIP

val03-03-ha-status-analyst-allow.txt

VAL03-03

HA status JSON or SKIP

val03-04-ha-status-auditor-allow.txt

VAL03-04

HA status JSON or SKIP

val03-05-audit-query-operator-deny.stderr

VAL03-05

rbac: denial message

val03-06-audit-query-analyst-deny.stderr

VAL03-06

rbac: denial message

val03-07-audit-query-auditor-allow.json

VAL03-07

JSON audit records

val03-08-rbac-role-list-unassigned.txt

VAL03-08

Role list with known roles, no RBAC error

val03-09-rollout-plan-list-unassigned.stderr

VAL03-09

Connection error (no rbac:)

val03-10-rbac-role-create-operator-deny.stderr

VAL03-10

rbac: denial message

val03-11-rbac-role-create-analyst-deny.stderr

VAL03-11

rbac: denial message

val03-12-rbac-role-create-auditor-allow.txt

VAL03-12

created role "val03-test-role"

val03-13-support-bundle-unassigned.stderr

VAL03-13

Bundle generation progress and bundle written: (nested collector warnings allowed)

val03-support-bundle.tar.gz

VAL03-13

Generated support-bundle archive

val03-14-access-denied-events.json

VAL03-14

JSON with auth.access.denied

val03-report.txt

Composite

14-check PASS/FAIL report

val03-report.json

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 rbac:

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 rbac:

VAL03-06

audit query

analyst

DENY

exits non-zero AND stderr contains rbac:

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 rbac: operator

VAL03-09

rollout plan list

unassigned

NOT_GUARDED

neither stdout nor stderr contains rbac:

VAL03-10

rbac role create

operator

DENY

exits non-zero AND stderr contains rbac:

VAL03-11

rbac role create

analyst

DENY

exits non-zero AND stderr contains rbac:

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 bundle written:

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-*.txt and the RBAC store contents

  • VAL03-02, 03, 04 fail (expected ALLOW but got non-zero, HA available): role assignment failed during setup — check setup-assign-*.txt; re-run to confirm

  • VAL03-07 fails (expected ALLOW): auditor assignment may have failed — check setup-seed-auditor.txt

  • VAL03-08, 09, 13 fail (unexpected rbac: in output): a guard was added to one of these commands — audit the command source for newRBACGuard().Check() calls

  • VAL03-12 fails (expected ALLOW): auditor’s rbac:manage permission is missing — verify the auditor predefined role definition in rbac.go

  • VAL03-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?

ha status

fleet:read

YES

Yes (VAL03-01…04)

ha failover

fleet:read

YES

No (same permission class as ha status)

ha quorum status

fleet:read

YES

No

ha split-brain detect

fleet:read

YES

No

ha split-brain recover

fleet:read

YES

No

rollout recover

fleet:read

YES

No

rollout gate approve

fleet:read

YES

No

rollback execute

fleet:read

YES

No

audit query

audit_history:read

YES

Yes (VAL03-05…07)

audit export

audit_history:read

YES

No (same permission class as audit query)

audit prune

rbac:manage

YES

No

rbac role create

rbac:manage

YES

Yes (VAL03-10…12)

rbac role assign

rbac:manage

YES

Covered by run_rbac_enforcement_lab

cert issue/rotate/revoke/sync-crl

cert:manage

YES

Covered by run_cert_rbac_lab

cert list/check-revocation

cert:read OR cert:manage

YES

Covered by run_cert_rbac_lab

rollout plan create/list/describe/cancel

NOT GUARDED

Yes (VAL03-09)

rollback preview

NOT GUARDED

No

rbac role list

NOT GUARDED

Yes (VAL03-08)

support-bundle generate

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