Layer 4: Conditional Access and Admin Governance¶
Layer 4 is the admin override plane. It answers whether the organization currently allows the caller to operate, even when the developer-configured transport and RBAC layers would otherwise allow it.
Two Enforcement Moments¶
Layer 4a: Token-Time Governance¶
Microsoft Entra can block or shape token issuance before the request reaches the resource:
- deny token issuance
- require stronger conditions
- apply organization-wide policy to the caller identity
Layer 4b: Data-Plane Governance¶
The platform also re-evaluates governance at request time through sidecar or app-layer checks:
- current agent risk
- current custom security attribute values
- current policy/tag match state
- current
agent_statekill-switch behavior
This closes the gap between token issuance time and request time.
What Is Shared Versus Scoped¶
Shared across environments:
- Conditional Access policy model
- custom security attribute schema such as
AgentIdentity.Department - tenant-wide admin and viewer groups for portal access
Scoped per environment:
- Agent Identity Blueprint for new scoped environments
- agent identities
- federated identity credentials
- env-scoped portal app registrations
That split keeps the governance model reusable while isolating the actual governed service principals per environment.
Common Governance Controls In This Repo¶
- risk-based deny
- tag matching through Entra custom security attributes
agent_stateenabled or disabled- sync from Microsoft Graph into the local policy and risk views
Tag evidence is held in the sidecar's in-memory TagStore. deploy.sh
rehydrates AgentIdentity.Department from Graph with scripts/sync_agent_tags.py
after every rollout (except --portal-only and SKIP_ENTRA=true), before final
verification; a Graph or management failure stops the deployment. After any
other gateway restart, use the administrator-only POST /api/sync-attributes
(portal: Sync from Entra (Graph)) before verifying governed calls.
Missing tag evidence correctly produces agent_tag_mismatch, even when risk
enforcement is Off. Do not disable tag governance or invent a matching tag to
work around unavailable evidence.
Why Layer 4 Is Separate¶
Layers 1 to 3 express what the application owner intended. Layer 4 expresses what the enterprise currently allows. In regulated environments those are different authorities and both must exist.
Data-Plane Evidence and Cache Contract¶
The sidecar evaluates the disabled-agent kill switch, Graph-sourced risk policy, and target-tag governance before RBAC and JWT. The shared Python evaluator applies the same risk-policy availability distinction to direct A2A calls.
- A configured CA policy cache must successfully observe a policy list before
it can allow traffic. Initial outages, invalid JSON, and missing/null
valuearrays are unavailable, not an empty list. The sidecar denies at Layer 4 with403 ca_policy_unavailable; Python returns a fail-closed block. - A successfully observed empty list, or a list with no enabled blocking risk policies, imposes no risk restriction. Disabled, report-only, and non-block policies retain their existing semantics. Tag checks and later layers still apply.
- Failed policy refreshes preserve the last-known-good list, including a
legitimately empty list; they do not reset it or renew the successful-fetch
timestamp. Go policy evidence expires after three sync intervals; after that,
even a previously empty list is unavailable and cannot authorize traffic.
/mgmt/ca-policy-effectiveexposesready,last_fetch, andlast_errorso unavailable startup and stale-but-initialized policy remain distinguishable. - When risk blocking is active, absent stores/entries and unrecognized risk
values deny with
403 agent_risk_unavailable. The sidecar risk API reports missing evidence and initialprevious_levelasunknown, neverlow. An authenticated explicit low-risk signal is required to permit low risk; a process restart does not manufacture one. - Production Go gateways use their own Entra risk cache, not the portal's
monitoring cache.
risk_cache_secondsdefaults to 90; zero fetches on every governed call. Evidence at the age limit is expired. A failed required refresh invalidates it; missing/404/mismatched/invalid responses deny. SPIFFE/aid/identity must match policy Entra metadata, and broad prefixes cannot stand in for a single identity. Explicit Entranoneis accepted; manual evidence may increase risk but cannot clear Entra risk or act as a safe fallback. CA_RISK_PROVIDER=sidecarselects durable explicit Blob evidence in the Go gateway and remains explicitly separate fromentrain Python. Entra mode never uses sidecar fallback evidence. A Graph response must include a recognizedriskLevel(none,low,medium, orhigh) beforeconfirmedSafecan clear it. The existing resolved-service-principalriskyAgents404/no-risk behavior is unchanged.- A configured
TagStoreis authoritative. Missing, removed, or empty Graph tags cannot fall back to an allowing YAMLca.agent_tag; required tag mismatch denies with403 agent_tag_mismatch. Populating the store through authenticated attribute synchronization restores matching callers. The explicitskip_target_tag_checkexemption affects only tags, not the disabled state or required risk checks.
Optional integration behavior is unchanged: an engine with no CA policy cache
does not perform Graph risk enforcement, and an engine with no TagStore
supports static YAML-only tags. Production ingress constructs a TagStore;
an unpopulated production store therefore denies required tags rather than
using YAML as evidence. These prototype modes do not claim full tenant CA
coverage or implement the generic filter expressions tracked in #31.