11 — Forgeyard Policy, Authorization & Identity System Architecture
Document type: Core Security & Governance System Architecture
Project: Forgeyard CI/CD
Subsystem: Identity, principals, authentication boundaries, sessions/tokens, OIDC/SAML/SCIM integration, service/workload identity, runner identity, permission-based RBAC, authorization decisions, policy bundles, protected targets, separation of duties, break-glass, revocation, caching, auditing, and policy evaluation
Architecture style: Identity-provider-neutral authentication, permission-based authorization, deterministic policy evaluation, explicit tenant/project scoping, immutable policy snapshots for critical workflows, and deny-by-default protected actions
Status: Target production architecture for the new Forgeyard repository
Relationship to prior work: Builds on the core foundation, metadata store, events/reconciliation, Change Proposal, Run/Job, Scheduler, Runner, and transport architectures. It defines who/what may request actions and under which policy, while remaining separate from secret storage and execution isolation.
1. Purpose
Forgeyard needs one coherent security model for:
humans
service accounts
runners
automation
bots
provider integrations
workloads
signing workers
device agents
and for actions such as:
view project
modify pipeline
cancel run
approve change
submit integration
read artifact
use secret
sign artifact
deploy production
administer runner
change policy
The central rule is:
Authentication answers “who or what is this?” Authorization answers “may this principal perform this action on this resource under the current policy?”
A second rule is:
Forgeyard authorizes permissions, not UI roles. Roles are only convenient permission bundles.
A third rule is:
Security-critical decisions are bound to explicit identities, resources, immutable inputs, and policy digests; they are never inferred from mutable labels or provider-specific role names alone.
2. Architectural Position
External Identity Provider / Local Identity
│
▼
Authentication
│
▼
PrincipalContext
│
▼
Authorization Service
┌──────┼─────────┐
▼ ▼ ▼
RBAC Policy Resource
│
▼
AuthorizationDecision
│
▼
Domain Service
│
▼
Audit
3. Goals
The subsystem MUST:
- define stable
PrincipalId; - support human identities;
- support service identities;
- support runner identities;
- support workload identities;
- support local standalone authentication;
- support OIDC;
- support SAML through an enterprise adapter if required;
- support SCIM provisioning;
- support API tokens;
- support short-lived sessions;
- support permission-based RBAC;
- support tenant/project/resource scoping;
- support policy evaluation;
- support immutable policy digests;
- support protected targets;
- support separation of duties;
- support break-glass;
- support approval requirements;
- support revocation;
- support authorization caching safely;
- support audit coupling;
- support runner trust classes;
- support workload identity;
- support provider integrations;
- support policy exceptions;
- support multi-tenancy;
- support standalone mode without enterprise IdP;
- support enterprise federation without coupling domain logic to provider;
- remain deterministic and testable.
4. Non-Goals
This subsystem does not:
store secret values
execute jobs
isolate workloads
implement CAS
schedule jobs
replace enterprise identity providers
It consumes/verifies identity and decides authority.
5. Workspace Structure
crates/identity/
├── forgeyard-identity/
├── forgeyard-identity-model/
├── forgeyard-identity-local/
├── forgeyard-identity-oidc/
├── forgeyard-identity-saml/
├── forgeyard-identity-scim/
├── forgeyard-identity-token/
├── forgeyard-identity-session/
├── forgeyard-identity-runner/
├── forgeyard-identity-workload/
├── forgeyard-identity-provider/
├── forgeyard-identity-health/
└── forgeyard-identity-testkit/
Authorization:
crates/authz/
├── forgeyard-authz/
├── forgeyard-authz-model/
├── forgeyard-authz-permission/
├── forgeyard-authz-role/
├── forgeyard-authz-scope/
├── forgeyard-authz-service/
├── forgeyard-authz-cache/
├── forgeyard-authz-breakglass/
├── forgeyard-authz-separation/
└── forgeyard-authz-testkit/
Policy:
crates/policy/
├── forgeyard-policy/
├── forgeyard-policy-model/
├── forgeyard-policy-bundle/
├── forgeyard-policy-eval/
├── forgeyard-policy-store-api/
├── forgeyard-policy-exception/
├── forgeyard-policy-protected-target/
├── forgeyard-policy-compiler/
├── forgeyard-policy-diagnostic/
└── forgeyard-policy-testkit/
6. Principal
#![allow(unused)]
fn main() {
pub struct PrincipalId(Ulid);
}
Stable internal identity.
External IdP subject identifiers are bindings, not Forgeyard authority IDs.
7. Principal Kinds
#![allow(unused)]
fn main() {
pub enum PrincipalKind {
Human,
Service,
Runner,
Workload,
Bot,
System,
}
}
8. Principal Record
#![allow(unused)]
fn main() {
pub struct Principal {
pub id: PrincipalId,
pub kind: PrincipalKind,
pub state: PrincipalState,
pub display_name: Option<PrincipalDisplayName>,
pub created_at: Timestamp,
}
}
9. Principal State
#![allow(unused)]
fn main() {
pub enum PrincipalState {
Active,
Suspended,
Revoked,
Deleted,
}
}
10. External Identity Binding
#![allow(unused)]
fn main() {
pub struct ExternalIdentityBinding {
pub principal: PrincipalId,
pub provider: IdentityProviderId,
pub subject: ExternalSubjectId,
}
}
Unique:
provider + subject
11. Never Use Email as Stable Identity
Email may change.
Use provider subject / internal PrincipalId.
12. Local Standalone Identity
Mode 1 may use:
LocalOwner
or local OS-authenticated principal.
13. Standalone Security
Do not force OIDC for single-user offline mode.
But map local identity into same PrincipalId model.
14. Authentication Methods
#![allow(unused)]
fn main() {
pub enum AuthenticationMethod {
Local,
PasswordlessLocal,
Oidc,
Saml,
ApiToken,
ClientCertificate,
WorkloadIdentity,
}
}
15. OIDC
Preferred human enterprise/cloud authentication mechanism.
Forgeyard acts as relying party.
16. OIDC Responsibilities
discovery
authorization code flow
PKCE where appropriate
issuer validation
audience validation
nonce/state
JWKS rotation
subject mapping
17. OIDC Claims
Treat IdP claims as external attributes.
Map only explicitly configured claims into Forgeyard attributes/groups.
18. No Blind Group Trust
IdP group string does not automatically become Forgeyard admin.
Explicit mapping required.
19. SAML
Optional enterprise adapter.
Converts SAML assertion into normalized external identity context.
20. SCIM
Provisioning/synchronization:
users
groups
activation/deactivation
SCIM is not login.
21. SCIM State
Provisioned identity can exist before first authentication.
22. Directory Groups
#![allow(unused)]
fn main() {
pub struct ExternalGroupBinding {
pub provider: IdentityProviderId,
pub external_group: ExternalGroupId,
pub forgeyard_role: RoleId,
pub scope: AuthorizationScope,
}
}
23. Session
Browser/UI session:
#![allow(unused)]
fn main() {
pub struct UserSession {
pub id: SessionId,
pub principal: PrincipalId,
pub issued_at: Timestamp,
pub expires_at: Timestamp,
pub authn_context: AuthenticationContext,
}
}
24. Session Security
short-lived
HttpOnly
Secure
SameSite
CSRF protection
rotation
where cookie-based.
25. API Tokens
#![allow(unused)]
fn main() {
pub struct ApiTokenId(Ulid);
}
Token value stored only hashed/secret-managed.
26. API Token Scope
Token has explicit:
permissions
resource scope
expiry
27. No Permanent Omnipotent Token by Default
Admin tokens require explicit creation/expiry policy.
28. Personal Access Token
Human-owned automation token.
29. Service Account
#![allow(unused)]
fn main() {
pub struct ServiceIdentity {
pub principal: PrincipalId,
pub owner: Option<PrincipalId>,
}
}
30. Workload Identity
Jobs can receive short-lived workload identity rather than long-lived secret.
31. Workload Identity Binding
#![allow(unused)]
fn main() {
pub struct WorkloadIdentityBinding {
pub run: RunId,
pub job: JobId,
pub attempt: JobAttemptId,
pub principal: PrincipalId,
pub expires_at: Timestamp,
}
}
32. Workload Identity Rule
Identity expires with attempt/job lifetime.
33. Runner Identity
Runner uses:
RunnerId
+
principal/certificate binding
34. Runner Trust
#![allow(unused)]
fn main() {
pub enum RunnerTrustClass {
GeneralUntrustedWorkload,
InternalTrusted,
SigningRestricted,
Confidential,
}
}
35. Runner Cannot Self-Promote
Trust class comes from enrollment/admin policy, not capability self-report.
36. Signing Worker Identity
Separate principal kind/service role.
37. Device Agent Identity
Same strong client authentication model.
38. Authentication Context
#![allow(unused)]
fn main() {
pub struct AuthenticationContext {
pub method: AuthenticationMethod,
pub provider: Option<IdentityProviderId>,
pub authenticated_at: Timestamp,
pub assurance: AuthenticationAssurance,
}
}
39. Assurance Level
#![allow(unused)]
fn main() {
pub enum AuthenticationAssurance {
Local,
SingleFactor,
MultiFactor,
HardwareBacked,
}
}
40. Step-Up Authentication
Sensitive actions may require stronger recent authentication:
change policy
break-glass
production deploy
create signing key binding
41. Permission Model
Permission strings/IDs are stable typed constants.
Examples:
project.read
project.write
pipeline.edit
run.create
run.cancel
artifact.read
artifact.delete
change.review
change.approve
change.integrate
policy.read
policy.write
secret.use
secret.admin
runner.read
runner.admin
release.promote
deployment.create
deployment.production
audit.read
system.admin
42. Permission Type
#![allow(unused)]
fn main() {
pub struct PermissionId(&'static str);
}
or generated stable enum/newtype.
43. Role
Role is a permission bundle.
#![allow(unused)]
fn main() {
pub struct Role {
pub id: RoleId,
pub permissions: BTreeSet<PermissionId>,
}
}
44. Built-In Roles
Examples:
Viewer
Developer
Maintainer
Reviewer
ReleaseManager
SecurityAdmin
OrganizationAdmin
SystemAdmin
45. Roles Are Not Domain Logic
Domain checks:
permission
not:
if role == Admin
46. Custom Roles
Enterprise can define custom permission bundles.
47. Scope
#![allow(unused)]
fn main() {
pub enum AuthorizationScope {
System,
Tenant(TenantId),
Organization(OrganizationId),
Project(ProjectId),
Repository(RepositoryId),
Environment(EnvironmentId),
}
}
48. Scope Hierarchy
Potential:
System
↓
Tenant
↓
Organization
↓
Project
Repository/environment may be children of project.
49. Scope Inheritance
Role assignment at organization may inherit to projects unless policy disables.
50. Explicit Deny
Support if needed:
#![allow(unused)]
fn main() {
pub enum PermissionEffect {
Allow,
Deny,
}
}
Recommended:
deny overrides allow
51. Assignment
#![allow(unused)]
fn main() {
pub struct RoleBinding {
pub principal: PrincipalId,
pub role: RoleId,
pub scope: AuthorizationScope,
}
}
52. Group Assignment
Directory/internal group can hold role binding.
53. Authorization Request
#![allow(unused)]
fn main() {
pub struct AuthorizationRequest {
pub principal: PrincipalId,
pub action: PermissionId,
pub resource: ResourceRef,
pub context: AuthorizationContext,
}
}
54. Authorization Context
#![allow(unused)]
fn main() {
pub struct AuthorizationContext {
pub tenant: TenantId,
pub project: Option<ProjectId>,
pub authn: AuthenticationContext,
pub source_trust: Option<SourceTrust>,
pub environment: Option<EnvironmentId>,
}
}
55. Decision
#![allow(unused)]
fn main() {
pub struct AuthorizationDecision {
pub allowed: bool,
pub reasons: Vec<AuthorizationReason>,
pub policy_digest: PolicyDigest,
}
}
56. Deny by Default
If no explicit permission/policy grants:
deny
57. Authorization Service
#![allow(unused)]
fn main() {
#[async_trait]
pub trait AuthorizationService {
async fn authorize(
&self,
request: AuthorizationRequest,
) -> Result<AuthorizationDecision, AuthorizationError>;
}
}
58. Domain Service Pattern
API
↓
authenticate
↓
authorize permission/resource
↓
domain command
59. UI Is Not Security Boundary
Hiding button is UX only.
Server authorizes every action.
60. Agent Authorization
Agents do not call generic human permission APIs.
They act using server-issued leases/workload-specific authority.
61. Policy vs RBAC
RBAC:
who may attempt action
Policy:
under what conditions may action/workflow proceed
62. Example
RBAC:
Alice has change.integrate
Policy:
integration requires 2 approvals + passing CI + protected target queue
Both must pass.
63. Policy Bundle
#![allow(unused)]
fn main() {
pub struct PolicyBundle {
pub id: PolicyBundleId,
pub version: PolicyVersion,
pub rules: Vec<PolicyRule>,
}
}
64. Policy Digest
Canonical:
#![allow(unused)]
fn main() {
pub struct PolicyDigest(Digest);
}
Critical decisions record exact digest.
65. Policy Sources
system
tenant
organization
project
repository
environment
protected target
66. Policy Precedence
Higher-level policy may set minimum protections.
Lower-level policy may tighten, not weaken protected constraints.
67. Policy Merge
Deterministic.
68. Effective Policy
#![allow(unused)]
fn main() {
pub struct EffectivePolicy {
pub digest: PolicyDigest,
pub rules: Vec<EffectivePolicyRule>,
}
}
69. Policy Input
#![allow(unused)]
fn main() {
pub struct PolicyInput {
pub principal: PrincipalId,
pub action: PolicyAction,
pub resource: ResourceRef,
pub source_snapshot: Option<SourceSnapshotId>,
pub proposal_revision: Option<ProposalRevisionId>,
pub run: Option<RunId>,
pub environment: Option<EnvironmentId>,
pub context: PolicyContext,
}
}
70. Policy Decision
#![allow(unused)]
fn main() {
pub enum PolicyDecision {
Allow,
Deny(Vec<PolicyViolation>),
Require(Vec<PolicyRequirement>),
}
}
71. Requirements
Examples:
approval
specific reviewer group
required check
reproducibility
signed artifact
trusted runner
MFA
queue-only integration
72. Policy Must Be Deterministic
Same immutable input + same policy digest -> same decision.
73. No Network in Core Policy Eval
Policy evaluator should be pure over supplied input.
External facts are resolved before evaluation and included explicitly.
74. Policy Language
Prefer typed Rust-native policy model initially.
RON for human policy config.
75. Avoid General Embedded Scripting Initially
Do not embed unrestricted Lua/JS/Rego-like interpreter unless need justifies complexity.
76. Policy Compiler
RON policy
↓
parse
↓
validate
↓
normalize
↓
PolicyBundle
↓
digest
77. Policy Rule Example
Illustrative RON:
(
protected_targets: [
(
pattern: "main",
requirements: [
Approvals(2),
Check("ci"),
QueueOnly,
],
),
],
)
78. Protected Target
#![allow(unused)]
fn main() {
pub struct ProtectedTargetPolicy {
pub target: ProtectedTargetSelector,
pub requirements: Vec<ProtectionRequirement>,
}
}
79. Protection Requirements
#![allow(unused)]
fn main() {
pub enum ProtectionRequirement {
RequiredApprovals(u8),
RequiredOwners,
RequiredCheck(CheckKind),
QueueOnly,
SignedIntegration,
NoDirectPush,
SeparationOfDuties,
}
}
80. Direct Push
VCS/provider integration can detect and reject/report according to backend capability.
Forgeyard cannot magically block external VCS writes unless provider/backend enforcement exists.
81. Policy Honesty
Distinguish:
Forgeyard-enforced
provider-enforced
advisory
82. Change Proposal Integration
Reviews/approvals bind:
ProposalRevisionId
SourceSnapshotId
Policy decides whether evidence satisfies target requirements.
83. Approval Invalidation
If source revision changes, policy re-evaluates.
84. Separation of Duties
Examples:
author != approver
approver != integrator
release creator != release approver
security approver distinct from code owner
85. Separation Model
#![allow(unused)]
fn main() {
pub struct SeparationRule {
pub action_a: Responsibility,
pub action_b: Responsibility,
pub relation: SeparationRelation,
}
}
86. Break-Glass
Emergency privileged bypass.
87. Break-Glass Rule
Never a hidden “admin bypass.”
Requires:
explicit permission
strong auth
reason
scope
expiry
audit
88. Break-Glass Grant
#![allow(unused)]
fn main() {
pub struct BreakGlassGrant {
pub id: BreakGlassId,
pub principal: PrincipalId,
pub scope: AuthorizationScope,
pub permissions: BTreeSet<PermissionId>,
pub reason: BreakGlassReason,
pub expires_at: Timestamp,
}
}
89. Break-Glass Audit
Mandatory.
90. Break-Glass Notification
Enterprise policy may notify security/admin team.
91. Break-Glass Cannot Reveal Secret Automatically
Secret access remains separately scoped and audited.
92. Policy Exception
A policy violation can be explicitly excepted.
#![allow(unused)]
fn main() {
pub struct PolicyException {
pub id: PolicyExceptionId,
pub violation: PolicyViolationId,
pub granted_by: PrincipalId,
pub scope: ExceptionScope,
pub expires_at: Option<Timestamp>,
}
}
93. Exception Does Not Erase Violation
Decision records:
violation
+
exception
94. Exception Scope
Exact:
proposal revision
run
release
target
time window
Avoid broad permanent bypass.
95. Source Trust
#![allow(unused)]
fn main() {
pub enum SourceTrust {
TrustedInternal,
ExternalContribution,
Fork,
Unknown,
}
}
96. Untrusted Proposal Policy
Restrict:
production secrets
signing workers
deployment credentials
privileged networks
97. Secret Use Authorization
Pipeline contains SecretRef.
At runtime:
principal/workload
+
secret
+
job/source trust
+
policy
must authorize.
98. Runner Trust Authorization
Scheduler filters trust capability.
Policy determines minimum trust for job.
99. Workload Permission
A job may get only:
artifact upload
specific secret use
specific deployment API
not user’s full permissions.
100. Delegation
Human starts run.
Do not simply run job as human’s unrestricted identity.
Issue constrained workload identity.
101. Delegation Chain
Human Principal
↓
Run
↓
Workload Principal
↓
specific capabilities
102. Delegation Audit
Record initiating principal.
103. Service-to-Service Identity
Daemon internal components generally run within same trusted process.
External workers use certificates/tokens.
104. Provider Integration Identity
SCM provider app/webhook identity maps to service principal/provider binding.
105. Webhook Authentication
Signature verification identifies provider delivery source, not human actor.
Human actor from provider event remains external metadata.
106. Provider Actor Mapping
If external actor binding exists, map carefully.
Do not assume provider username equals Forgeyard identity.
107. Identity Linking
User can link external provider account to Forgeyard principal through verified flow.
108. Audit Actor
ActorRef may be:
human principal
service principal
runner
system
109. System Actor
Do not create fake user "system".
Use typed SystemActor.
110. Authorization Cache
High-volume permission checks may be cached.
111. Cache Key
principal
permission
scope/resource class
role-binding version
policy digest
112. Cache TTL
Short.
113. Revocation
Security-sensitive revocation must invalidate quickly.
114. Authorization Epoch
Potential:
#![allow(unused)]
fn main() {
pub struct AuthorizationEpoch(u64);
}
Increment on role/policy changes.
Cache includes epoch.
115. Tenant Policy Epoch
Can avoid global cache flush.
116. Token Revocation
Store token state/hash.
117. Session Revocation
User logout/admin suspension invalidates session.
118. Principal Suspension
Immediately deny new actions.
119. Active Jobs After Suspension
Policy decides.
Usually:
existing unprivileged build can continue
but privileged deployments/signing may be cancelled.
120. Runner Revocation
Close transport session; leases eventually cancel/lost.
121. Workload Identity Revocation
Expires quickly and can be explicitly revoked if run cancelled.
122. Policy Snapshot for Run
Run records:
PolicyDigest
for immutable execution semantics.
123. Mid-Run Policy Change
Does not silently mutate already planned ordinary job semantics.
Security emergency policy may impose immediate deny/cancel through explicit override mechanism.
124. Security Override
Examples:
revoke compromised secret
disable runner
block vulnerable toolchain
May override existing plans.
Must be explicit/audited.
125. Release Policy Snapshot
Release approval/promotion records exact policy digest.
126. Deployment Policy Snapshot
Same.
127. Change Proposal Policy Snapshot
Check/approval decisions bind policy digest.
128. Policy Re-evaluation
If policy changes before integration:
re-evaluate
old evidence may still be valid but requirements may change.
129. Authorization Error Model
#![allow(unused)]
fn main() {
pub enum AuthorizationError {
Unauthenticated,
PrincipalRevoked,
PermissionDenied,
ScopeMismatch,
StepUpRequired,
PolicyDenied,
PolicyEvaluationFailed,
Internal,
}
}
130. User-Safe Denial
Explain:
missing permission
required approval
MFA required
protected target
without leaking sensitive details.
131. Policy Diagnostics
#![allow(unused)]
fn main() {
pub struct PolicyDiagnostic {
pub code: PolicyDiagnosticCode,
pub severity: DiagnosticSeverity,
pub message: BoundedString,
}
}
132. Explain Authorization
CLI/UI can show:
principal
permission
scope
role bindings
policy requirements
decision
subject to permission to inspect policy.
133. forgeyard authz explain
Example:
forgeyard authz explain --action change.integrate --proposal 42
134. Explainability
Critical for enterprise debugging.
135. Do Not Leak Hidden Security Policy
Some details may be redacted for non-admin users.
136. Identity Store
Metadata stores:
principals
external bindings
sessions
token metadata
role bindings
groups
policy refs
137. Secret Token Values
Never plaintext in DB.
138. Token Hashing
Use strong keyed/hash mechanism appropriate for high-entropy tokens.
139. Session Cookie Secrets
Store securely.
140. OIDC Tokens
Avoid long-term storage of provider access tokens unless integration needs them.
141. Refresh Tokens
If stored, use secret provider/encrypted storage and minimal scopes.
142. Identity Provider Configuration
RON:
(
providers: [
(
id: "company-oidc",
kind: Oidc,
issuer: "https://id.example.com",
client_id: Secret("oidc/client-id"),
client_secret: Secret("oidc/client-secret"),
),
],
)
143. IdP Secrets
SecretRef, not literal.
144. Local Bootstrap Admin
First standalone/server setup needs safe bootstrap.
145. Bootstrap Token
One-time short-lived local setup token or console command.
146. Bootstrap Completion
Disable bootstrap path after initial admin created.
147. No Default Password
Never ship static default credentials.
148. MFA
Forgeyard should rely on IdP MFA where federated.
Local admin can support additional step-up later.
149. MFA Claim
Validate assurance claim only from configured trusted IdP.
150. Reauthentication Age
Sensitive action may require auth within last N minutes.
151. Role Binding Versioning
Mutable bindings have EntityVersion.
152. Role Changes
Emit events:
RoleBindingCreated
RoleBindingRevoked
and invalidate caches.
153. Policy Change Event
PolicyBundleActivated
154. Audit Coupling
Security-sensitive changes require audit in same transaction where possible.
155. Audit Examples
role change
policy change
break-glass
token creation/revocation
runner trust change
secret permission change
production deploy authorization
156. Approval Audit
Already covered by Change Proposal but integrates principal/policy.
157. Identity Deletion
Do not erase historical audit actor identity blindly.
Can pseudonymize display/profile while retaining stable internal audit ID.
158. SCIM Deprovision
Principal -> Suspended/Revoked.
159. Group Removal
Role bindings derived from group recalculate.
160. SCIM Reconciliation
Periodic sync can repair missed provisioning events.
161. OIDC JIT Provisioning
Optional:
first valid login creates Principal
if policy permits.
162. Enterprise Recommendation
SCIM + OIDC:
SCIM lifecycle
OIDC authentication
163. Group Mapping
Explicit configuration.
164. Name Collisions
External display names do not determine identity.
165. Multi-Tenant Federation
Different tenants may use different IdPs.
166. IdP Routing
Tenant/domain selection at login.
167. Tenant Discovery Security
Avoid leaking private tenant existence unnecessarily.
168. System Admin
System-scoped permission distinct from tenant admin.
169. Tenant Admin
Cannot cross tenant.
170. Project Admin
Cannot change system identity provider settings.
171. Resource Ownership
Ownership can inform policy but does not replace permission.
172. CODEOWNERS
Change ownership maps reviewer requirements, not general RBAC admin.
173. Protected Environment
#![allow(unused)]
fn main() {
pub struct EnvironmentPolicy {
pub environment: EnvironmentId,
pub required_permissions: BTreeSet<PermissionId>,
pub approvals: Vec<ApprovalRequirement>,
}
}
174. Production Environment
Can require:
deployment.production
MFA
release manager approval
signed artifact
175. Artifact Authorization
Artifact metadata has project/tenant scope.
CAS digest knowledge alone is insufficient.
176. Audit Authorization
audit.read can be separately scoped.
177. Secret Admin vs Use
Separate:
secret.admin
secret.use
A developer can use secret without reading value.
178. Signing Permission
Separate:
signing.request
signing.admin
179. Runner Admin
Separate:
runner.read
runner.drain
runner.admin
runner.trust.manage
180. Principle of Least Privilege
Split permissions when actions have materially different risk.
181. Permission Registry
Stable centrally documented list.
182. Permission Evolution
Adding permission should not silently grant through broad wildcard unless explicitly intended.
183. Wildcards
Avoid overly broad:
*
except system-admin internal role.
184. Custom Role Validation
Cannot include unknown permission.
185. System Admin Role
Explicitly high risk.
186. Policy Rule Types
Potential:
#![allow(unused)]
fn main() {
pub enum PolicyRule {
RequirePermission,
RequireApproval,
RequireCheck,
RequireOwnership,
RequireMfa,
RequireTrustedRunner,
RequireReproducibility,
DenySourceTrust,
RestrictEnvironment,
RestrictSecret,
SeparationOfDuties,
}
}
187. Policy Evaluation Output
Include:
matched rules
unmet requirements
violations
exceptions
188. Policy Engine Must Not Mutate
Pure decision only.
Services execute resulting requirements/workflows.
189. Policy Compiler Safety
Bound config size/rule count.
190. Policy Cycle
If rules reference groups/policies, detect cycles.
191. Policy Include
If supported:
immutable/local config
No moving remote policy during evaluation.
192. Policy Testing
Provide CLI:
forgeyard policy test
193. Policy Unit Cases
RON can include fixtures:
input
expected decision
194. Policy Simulation
Before activation:
show which projects/actions would change
195. Policy Activation
Versioned/atomic.
196. Policy Rollback
Activate prior known policy bundle.
197. Policy History
Retain:
bundle digest
creator
activation time
198. Policy Diff
Semantic diff:
new required approval
removed permission
new production restriction
199. Policy Change Review
High-assurance installations may require Change Proposal-like review of policy config itself.
200. Self-Hosting Policy
Forgeyard repository can protect:
main
release tags
signing
using same policy engine.
201. API Authentication
Axum middleware extracts authenticated principal context.
202. Middleware Does Not Authorize Everything Globally
Route/service calls permission-specific authz.
203. CLI Authentication
Normal CLI uses browser/OIDC/device flow/token as supported.
204. Dioxus UI
Uses user session.
205. Agent Authentication
mTLS.
206. Provider Authentication
Webhook signature/app credential.
207. Internal System Tasks
Use SystemActor + explicit system capability, not spoofed admin user.
208. Authorization Context Injection
Application layer constructs context.
Domain service can require Authorized<T> wrapper if useful.
209. Capability Token Pattern
For some internal flows:
#![allow(unused)]
fn main() {
pub struct AuthorizedAction<T> {
pub request: T,
pub decision: AuthorizationDecisionRef,
}
}
Avoid passing raw user permission booleans.
210. Security-Critical Recheck
Some long workflows re-authorize at final action.
Example:
deployment approval hours ago
before production action ensure principal/policy still valid if required.
211. Approval Evidence vs Permission
Approval can remain evidence even if approver later leaves organization depending policy.
Policy defines.
212. Revoked Malicious Approval
Security admin may invalidate explicitly.
213. Policy Exception Expiry
Durable timer/reconciler expires.
214. Token Expiry
Validated on each request.
215. Session Expiry
Same.
216. API Rate Limits
Can be identity-scoped.
Detailed rate limiter elsewhere.
217. Audit IP/User Agent
Optional diagnostic metadata.
Not identity authority.
218. Privacy
Minimize stored identity profile data.
219. Display Name
Can be synced from IdP but historical audit should not depend on current display value.
220. Email
Contact metadata, not authorization identity.
221. Authorization Metrics
authn_success
authn_failure
authz_allow
authz_deny
authz_stepup
policy_denials
breakglass_active
222. IdP Metrics
oidc_login_latency
jwks_refresh_failure
scim_sync_failure
223. Cache Metrics
authz_cache_hit
authz_cache_miss
authz_cache_invalidation
224. High Cardinality
No PrincipalId in metrics labels.
225. Tracing
Spans:
authn.oidc
authn.token
authz.evaluate
policy.evaluate
policy.compile
identity.scim.sync
226. Audit Logs
Do not log token values/claims wholesale.
227. Health
Checks:
IdP discovery
JWKS validity
session store
policy store
SCIM status
228. IdP Outage
Existing valid sessions may continue according to policy until expiry.
New login may fail.
229. Policy Store Outage
Critical authorization should fail closed if decision cannot be determined.
230. Authz Cache During Outage
Only bounded/still-valid cached decisions according to risk.
231. Fail Closed
Protected write actions default deny on uncertainty.
232. Read-Only Degraded Mode
May allow some cached/public reads.
233. Doctor
forgeyard identity doctor
forgeyard authz doctor
forgeyard policy doctor
234. CLI
forgeyard auth whoami
forgeyard auth login
forgeyard auth logout
forgeyard auth token create
forgeyard auth token revoke
forgeyard role list
forgeyard role bind
forgeyard role unbind
forgeyard policy validate
forgeyard policy compile
forgeyard policy diff
forgeyard policy test
forgeyard policy activate
forgeyard authz explain
235. whoami
Shows:
PrincipalId
auth method
tenant
role bindings summary
session expiry
236. Token Create
Shows value exactly once.
237. Token Storage
CLI/user stores securely.
238. Dioxus UI
Admin/security sections:
Users
Groups
Service Accounts
Roles
Permissions
Identity Providers
Policies
Protected Targets
Break-Glass
Audit
239. Policy UI
Should show compiled effective policy and source.
240. Authorization Explain UI
Useful for:
why can't I merge?
why can't I deploy?
why can't this job use secret?
241. Testkit
forgeyard-identity-testkit/src/
├── lib.rs
├── principal.rs
├── oidc.rs
├── session.rs
├── token.rs
└── assertions.rs
Authz:
forgeyard-authz-testkit/src/
├── lib.rs
├── roles.rs
├── permissions.rs
├── scopes.rs
├── decisions.rs
└── assertions.rs
Policy:
forgeyard-policy-testkit/src/
├── lib.rs
├── bundle.rs
├── input.rs
├── decision.rs
├── exception.rs
└── assertions.rs
242. Unit Tests
Test:
scope inheritance
deny precedence
custom roles
policy merge
policy digest
243. OIDC Tests
issuer mismatch
audience mismatch
expired token
nonce mismatch
JWKS rotation
244. SCIM Tests
create
update
deactivate
group membership
idempotent sync
245. Authorization Tests
- no binding -> deny;
- project role grants project only;
- tenant admin cannot cross tenant;
- explicit deny wins;
- revoked principal denied;
- expired token denied.
246. Policy Tests
- same input+digest -> same decision;
- lower-level policy cannot weaken protected minimum;
- exception preserves violation record;
- expired exception no longer applies;
- protected target requires queue.
247. Separation Tests
Author cannot self-approve when policy forbids.
248. Break-Glass Tests
Requires:
permission
MFA/step-up
reason
expiry
audit
249. Runner Trust Tests
General runner cannot self-report SigningRestricted.
250. Workload Identity Tests
Job receives only exact allowed scopes and expires.
251. Cache Invalidation Tests
Role change invalidates effective authz cache.
252. Multi-Tenant Tests
Cross-tenant resource access denied even if ID guessed.
253. Failure Injection
IdP unavailable
policy store unavailable
JWKS refresh failure
SCIM partial failure
cache stale
254. Security Fuzzing
Fuzz:
policy parser
scope parser
permission inputs
OIDC callback/state parsing
Use vetted libraries for JWT/SAML cryptographic parsing.
255. Performance Tests
Measure:
permission decision
effective role expansion
policy evaluation
cache invalidation
large group membership
256. Large Enterprise Scale
Test:
100k principals
thousands of groups
many projects
many role bindings
257. Authorization Indexing
Store indexes by:
principal
scope
role
group
258. Group Membership Expansion
Avoid expensive recursive expansion every request.
Use cached/materialized effective memberships with versioning.
259. Group Cycles
Reject internal group cycles.
260. External Group Sync
Normalize into membership table.
261. Event Integration
Events:
PrincipalSuspended
RoleBindingChanged
PolicyActivated
RunnerTrustChanged
TokenRevoked
262. Reconciliation
Identity/provider reconcile:
SCIM state drift
expired sessions/tokens cleanup
policy exception expiry
OIDC provider health
263. Durable Timers
Use for:
break-glass expiry
exception expiry
temporary role grants
264. Temporary Role Grant
#![allow(unused)]
fn main() {
pub struct TemporaryRoleBinding {
pub binding: RoleBinding,
pub expires_at: Timestamp,
}
}
265. Just-In-Time Privilege
Useful enterprise capability.
266. JIT Grant Requires Approval
Policy-defined.
267. JIT Expiry
Automatic through timer/reconcile.
268. Session Fixation Defense
Rotate session ID after login/privilege change.
269. CSRF
Public UI mutation endpoints protected.
270. CORS
Restrictive.
271. API Token in Browser
Avoid.
272. Device Authorization Flow
CLI login can use OIDC device flow if browser callback impractical.
273. Local CLI Auth
Standalone can use local socket/OS user trust.
274. Local Socket Permissions
Protect daemon local admin socket.
275. Unix Peer Credentials
Optional local auth mechanism.
276. Windows Named Pipe ACL
Equivalent.
277. Authn Plugin Boundary
Identity provider adapters implement normalized trait.
278. IdentityProvider Trait
#![allow(unused)]
fn main() {
#[async_trait]
pub trait IdentityProvider {
async fn authenticate(
&self,
request: AuthenticationRequest,
) -> Result<AuthenticatedExternalIdentity, IdentityError>;
}
}
Exact methods vary by flow; keep provider-specific concerns adapter-local.
279. Policy Store API
#![allow(unused)]
fn main() {
#[async_trait]
pub trait PolicyStore {
async fn effective_policy(
&self,
scope: AuthorizationScope,
) -> Result<EffectivePolicy, PolicyStoreError>;
}
}
280. Role Store API
#![allow(unused)]
fn main() {
#[async_trait]
pub trait RoleStore {
async fn bindings_for(
&self,
principal: PrincipalId,
scope: AuthorizationScope,
) -> Result<Vec<RoleBinding>, StoreError>;
}
}
281. Authorization Service Composition
Principal state
+
role/group bindings
+
scope
+
effective policy
+
request context
↓
AuthorizationDecision
282. Decision Evidence
For critical operations, persist:
principal
permission
policy digest
decision
requirements
timestamp
283. Decision Reuse
Do not reuse old decision indefinitely.
Critical long-running workflows either bind evidence or recheck at action boundary.
284. Change Approval Evidence
Approval remains explicit domain evidence.
Authorization decision only proves reviewer had permission at approval time.
285. Production Deploy
Final deployment action may require current permission + prior approvals.
286. Release Sign
Signing worker also validates server-issued request/policy proof.
287. Policy Proof
Potential signed internal capability token for restricted workers.
Detailed in Secrets & Trust next.
288. Implementation Phase 1 — Principal/Permission Model
Implement:
PrincipalId
PrincipalKind
PermissionId
Role
Scope
AuthorizationRequest/Decision
289. Phase 2 — Local Identity
Standalone/local server bootstrap.
290. Phase 3 — Role Bindings/Authz
Permission-based server enforcement.
291. Phase 4 — Policy Compiler/Evaluator
RON -> validated bundle -> digest -> deterministic decision.
292. Phase 5 — OIDC
Enterprise/user login.
293. Phase 6 — Sessions/Tokens
Browser/CLI/API automation.
294. Phase 7 — Protected Targets/Separation
Integrate Change Proposal.
295. Phase 8 — Runner/Workload Identity
Integrate mTLS and job delegation.
296. Phase 9 — SCIM/SAML
Enterprise adapters.
297. Phase 10 — Break-Glass/Exceptions/JIT
High-risk governance.
298. Phase 11 — Caching/Reconciliation
Scale/hardening.
299. Phase 12 — Security Hardening
Threat modeling, fuzzing, federation tests, audit review.
300. Acceptance Tests
- Internal identity never relies on email.
- OIDC subject maps to stable PrincipalId.
- Invalid issuer/audience denied.
- Suspended principal cannot act.
- API token permissions are scoped.
- Expired token/session denied.
- Role grants permissions, domain never checks role names.
- Project-scoped permission cannot cross project.
- Tenant-scoped admin cannot cross tenant.
- Explicit deny overrides allow where configured.
- No matching permission -> deny.
- Policy digest is deterministic.
- Same policy input yields same decision.
- Lower scope cannot weaken system/tenant protection.
- Change approval checks reviewer permission at approval time.
- Author cannot self-approve when separation rule forbids.
- Protected target can require queue-only integration.
- Policy exception preserves original violation.
- Expired exception stops applying.
- Break-glass requires strong auth/reason/expiry/audit.
- Break-glass does not become permanent hidden admin.
- Runner cannot self-promote trust.
- Workload identity is short-lived and scope-limited.
- Fork/untrusted proposal cannot receive privileged secret/signing permission.
- Artifact digest knowledge does not bypass artifact authz.
- Secret use can be granted without secret.admin.
- Session/token revocation invalidates access.
- Role/policy change invalidates authz cache.
- SCIM deprovision suspends principal.
- Existing IdP outage fails safely according to session policy.
- Policy store uncertainty fails closed for protected writes.
- Same authz semantics work in Stoolap/Postgres modes.
- Audit captures security-sensitive permission/policy changes.
- UI hidden controls are not relied on for enforcement.
- Forgeyard self-hosting uses the same policy/authz/identity subsystem.
301. Production Readiness Gates
Do not call this subsystem production-ready until:
permission model stable
scope isolation tested
local bootstrap safe
OIDC validation hardened
session/token lifecycle stable
policy compiler deterministic
policy digest binding works
protected targets integrated
separation-of-duties tested
runner trust binding works
workload identity constrained
revocation/cache invalidation tested
audit coverage verified
SCIM/SAML/break-glass/JIT can reach enterprise readiness incrementally.
302. Architectural Invariants
- authentication and authorization are separate;
- internal PrincipalId is stable authority identity;
- email/display name are not identity authority;
- external provider subjects are bindings;
- roles are permission bundles only;
- domain services check permissions, not role names;
- authorization is scope-aware;
- deny-by-default;
- tenant isolation is explicit;
- policy decisions are deterministic;
- policy bundles have immutable digests;
- lower policy cannot weaken protected higher-level requirements;
- policy exceptions preserve violation evidence;
- break-glass is explicit, expiring, strongly authenticated, audited;
- runner trust is provisioned, not self-reported;
- workloads receive delegated least-privilege identity;
- source trust affects privileged access;
- secret use is separate from secret administration;
- protected writes fail closed on uncertain authz;
- authorization cache is version/epoch bounded;
- revocation invalidates active authority quickly;
- sessions/tokens are short-lived/scoped;
- public UI is never security authority;
- provider usernames/roles do not directly become Forgeyard roles;
- security-critical decisions record policy digest;
- long workflows recheck where policy requires;
- system tasks use typed system actor;
- identity provider adapters do not leak into domain;
- standalone and enterprise share same Principal/Permission model;
- Forgeyard dogfoods its own policy and authorization rules.
303. Final Target Architecture
Human / Service / Runner / Workload
│
▼
Authentication
┌───────────┼───────────┐
▼ ▼ ▼
Local OIDC mTLS
│ │ │
└───────────┼───────────┘
▼
PrincipalContext
│
▼
Authorization Service
┌────────────────┼────────────────┐
▼ ▼ ▼
Permissions Scope Policy
│ │ │
└────────────────┼────────────────┘
▼
AuthorizationDecision
│
┌─────────┴─────────┐
▼ ▼
Deny Allow
│
▼
Domain Action
│
▼
Audit
304. Final Architectural Position
Human request:
authenticated PrincipalId
+
permission
+
resource scope
+
authn assurance
+
effective policy digest
↓
AuthorizationDecision
Workflow policy:
source snapshot
proposal revision
checks
approvals
runner trust
environment
policy digest
↓
Allow / Deny / Require
Workload authority:
initiating principal
↓
Run/Job
↓
short-lived constrained WorkloadIdentity
The key guarantee is:
Forgeyard never equates “logged in,” “has a role,” or “came from a trusted provider” with unrestricted authority. Every protected action is evaluated against an explicit principal, permission, resource scope, authentication context, and deterministic policy snapshot.
305. New-Repository Sequence
01 Core Domain & Foundation
02 Storage & Metadata
03 CAS & Artifact Data Plane
04 Pipeline IR / Parsing / Planning
05 Run / Job State Machine
06 Scheduler
07 Runner / Agent
08 Sandbox & Executor
09 Transport / QUIC / Protocol
10 Events / Reconciliation
11 Policy / Authorization / Identity
12 Secrets & Trust
13 Supply Chain / SBOM / Provenance / Signing
14 Packaging
15 Release
16 Deployment
17 Observability / Health / Doctor
18 API / Axum
19 Dioxus UI
20 Device Lab
21 SCM Provider Integrations
22 HA / Coordination / Raft
23 RBE Interop
24 Plugin / Extension Architecture
25 Operations / Backup / Upgrade / DR
26 Self-Hosting / Bootstrap / Release of Forgeyard