Security Architecture Overview
Security Principles
| Principle | Implementation |
|---|---|
| Defense in depth | Multiple layers: network, auth, authz, audit |
| Least privilege | Role-based access; minimal permissions |
| Zero trust | All requests authenticated; mTLS for agents |
| Secrets hygiene | Secrets in vault; never in DB; ephemeral injection |
| Audit everything | All mutations logged; evidence trail |
| Immutable evidence | Evidence packets append-only; cryptographically signed |
Authentication Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION ARCHITECTURE │
│ │
│ Human Users Service/Agent │
│ ┌──────────┐ ┌──────────┐ │
│ │ Browser │ │ Agent │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ │ OAuth 2.0 │ mTLS + JWT │
│ │ Authorization Code │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ AUTHORITY MODULE │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ OAuth 2.0 │ │ mTLS │ │ API Key │ │ │
│ │ │ Provider │ │ Validator │ │ Validator │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ TOKEN ISSUER │ │ │
│ │ │ - Short-lived JWT (15 min) │ │ │
│ │ │ - Contains: user_id, tenant_id, roles, permissions │ │ │
│ │ │ - Signed with RS256 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ API GATEWAY │ │
│ │ │ │
│ │ - Validate JWT signature │ │
│ │ - Check token expiration │ │
│ │ - Extract tenant context │ │
│ │ - Enforce rate limits │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Authorization Model
Permission Structure
interface Permission {
resource: ResourceType;
action: ActionType;
scope?: ScopeType;
conditions?: Condition[];
}
type ResourceType =
| "environment"
| "release"
| "promotion"
| "target"
| "agent"
| "workflow"
| "plugin"
| "integration"
| "evidence";
type ActionType =
| "create"
| "read"
| "update"
| "delete"
| "execute"
| "approve"
| "deploy"
| "rollback";
type ScopeType =
| "*" // All resources
| { environmentId: UUID } // Specific environment
| { labels: Record<string, string> }; // Label-based
Role Definitions
| Role | Permissions |
|---|---|
admin | All permissions on all resources |
release-manager | Full access to releases, promotions; read environments/targets |
deployer | Read releases; create/read promotions; read targets |
approver | Read/approve promotions |
viewer | Read-only access to all resources |
Environment-Scoped Roles
Roles can be scoped to specific environments:
// Example: Production deployer can only deploy to production
const prodDeployer = {
role: "deployer",
scope: { environmentId: "prod-environment-uuid" }
};
Policy Enforcement Points
┌─────────────────────────────────────────────────────────────────────────────┐
│ POLICY ENFORCEMENT POINTS │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ API LAYER (PEP 1) │ │
│ │ - Authenticate request │ │
│ │ - Check resource-level permissions │ │
│ │ - Enforce tenant isolation │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ SERVICE LAYER (PEP 2) │ │
│ │ - Check business-level permissions │ │
│ │ - Validate separation of duties │ │
│ │ - Enforce approval policies │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ DECISION ENGINE (PEP 3) │ │
│ │ - Evaluate security gates │ │
│ │ - Evaluate custom OPA policies │ │
│ │ - Produce signed decision records │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ DATA LAYER (PEP 4) │ │
│ │ - Row-level security (tenant_id) │ │
│ │ - Append-only enforcement (evidence) │ │
│ │ - Encryption at rest │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Agent Security Model
See Agent Security for detailed agent security architecture.
Key features:
- mTLS authentication with CA-signed certificates
- One-time registration tokens
- Short-lived JWT for task execution
- Encrypted task payloads
- Scoped credentials per task
Secrets Management
┌─────────────────────────────────────────────────────────────────────────────┐
│ SECRETS FLOW (NEVER STORED IN DB) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ VAULT │ │ STELLA CORE │ │ AGENT │ │
│ │ (Source) │ │ (Broker) │ │ (Consumer) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ │ Task requires secret │ │
│ │ │ │ │
│ │ Fetch with service │ │ │
│ │ account token │ │ │
│ │◄─────────────────────── │ │
│ │ │ │ │
│ │ Return secret │ │ │
│ │ (wrapped, short TTL) │ │ │
│ │───────────────────────► │ │
│ │ │ │ │
│ │ │ Embed in task payload │ │
│ │ │ (encrypted) │ │
│ │ │───────────────────────► │
│ │ │ │ │
│ │ │ │ Decrypt │
│ │ │ │ Use for task │
│ │ │ │ Discard │
│ │
│ Rules: │
│ - Secrets NEVER stored in Stella database │
│ - Only Vault references stored │
│ - Secrets fetched at execution time only │
│ - Secrets not logged (masked in logs) │
│ - Secrets not persisted in agent memory beyond task scope │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
As-built credential handling (2026-07-04, SPRINT_20260703_002 / DTC-12)
The diagram above is the design-time contract; this subsection is the verified as-built state of target credentials in src/ (updated when the implementation changes).
Target connection configs are encrypted at rest. The release.targets column connection_config_encrypted historically stored plain UTF-8 JSON despite its name. It now stores an AEAD-sealed envelope produced by AeadTargetConnectionConfigProtector (src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Target/TargetConnectionConfigProtector.cs):
- Envelope encryption via the shared platform
CredentialAead(per-tenant DEK derived by HKDF from the host’s builtin master KEK — the same unifiedIKekSourcethe deployment bundle and secret broker use), in a dedicated crypto domain (ro-target-connection*AAD prefix / KDF label / KEK-id prefix) so target ciphertexts are mutually undecryptable with every other sealed surface. - The AAD binds (tenant, target row): a ciphertext copied across rows or tenants fails authentication instead of decrypting.
- Legacy plaintext rows keep reading (encrypted payloads carry a magic prefix; plain JSON parses directly) and are re-sealed on their next write — no data migration, per the forward-only posture (ADR-004). A host without a registered protector fails loudly on an encrypted row rather than parsing ciphertext (
PostgresTargetStore.ReadConnectionConfig).
API reads mask inline secrets. Secret-bearing connection-config fields are canonical secret references. Pointer schemes (vault://, builtin://, openbao://, authref://, file://) are non-secret pointers and return verbatim so operators can view/edit them; inline values (plain:, base64:, raw literals) are replaced with the *** sentinel on every target read, and the update path substitutes the stored value back when a client echoes the sentinel (TargetConnectionSecretMasking, applied in ReleaseOrchestratorEnvironmentEndpoints).
Deploy-time secretRef resolution is fail-closed. All per-transport resolvers (SSH, WinRM, Ansible, ECS, Nomad) route through the unified ISecretProvider (builtin | Vault | OpenBao) with tenant scope — including WinRM, which was previously the one resolver never provider-routed (a vault:// WinRM password silently resolved to null). A configured reference that fails to resolve throws DeploymentSecretResolutionException and fails that target’s deployment with a clear per-target error (message carries only target name, field, and scheme — never the path or value) instead of dispatching a credential-less task (TargetExecutor.ResolveConfiguredSecretAsync). Broker-mode (pull) resolution is separately fail-closed per ADR-034 (DeploymentSecretBrokerService).
Decision audit trail is written. Approval decisions (grant/reject, single and batch) and deployment lifecycle decisions (create, pause, resume, cancel, rollback, target retry) append to the tamper-evident release_orchestrator.audit_entries hash chain via ReleaseOrchestratorDecisionAuditTrail (src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Services/). The chain (per-tenant sequence + previous-hash link through release_orchestrator.audit_sequences, verified by verify_audit_chain) existed since migration 001 but had no writers before this sprint. Ledger writes never carry secret material and never fail the user action (failures are logged and swallowed; the primary approval/deployment row has already committed).
Threat Model
| Threat | Attack Vector | Mitigation |
|---|---|---|
| Credential theft | Database breach | Secrets never in DB; only vault refs |
| Token replay | Stolen JWT | Short-lived tokens (15 min); refresh tokens rotated |
| Agent impersonation | Fake agent | mTLS with CA-signed certs; registration token one-time |
| Digest tampering | Modified image | Digest verification at pull time; mismatch = failure |
| Evidence tampering | Modified audit records | Append-only table; cryptographic signing |
| Privilege escalation | Compromised account | Role-based access; SoD enforcement; audit logs |
| Supply chain attack | Malicious plugin | Plugin sandbox; capability declarations; review process |
| Lateral movement | Compromised target | Short-lived task credentials; scoped permissions |
| Data exfiltration | Log/artifact theft | Encryption at rest; network segmentation |
| Denial of service | Resource exhaustion | Rate limiting; resource quotas; circuit breakers |
Audit Trail
Audit Event Structure
interface AuditEvent {
id: UUID;
timestamp: DateTime;
tenantId: UUID;
// Actor
actorType: "user" | "agent" | "system" | "plugin";
actorId: UUID;
actorName: string;
actorIp?: string;
// Action
action: string; // "promotion.approved", "deployment.started"
resource: string; // "promotion"
resourceId: UUID;
// Context
environmentId?: UUID;
releaseId?: UUID;
promotionId?: UUID;
// Details
before?: object; // State before (for updates)
after?: object; // State after
metadata?: object; // Additional context
// Integrity
previousEventHash: string; // Hash chain for tamper detection
eventHash: string;
}
Audited Operations
| Category | Operations |
|---|---|
| Authentication | Login, logout, token refresh, failed attempts |
| Authorization | Permission denied events |
| Environments | Create, update, delete, freeze window changes |
| Releases | Create, deprecate, archive |
| Promotions | Request, approve, reject, cancel |
| Deployments | Start, complete, fail, rollback |
| Targets | Register, update, delete, health changes |
| Agents | Register, heartbeat gaps, capability changes |
| Integrations | Create, update, delete, test |
| Plugins | Enable, disable, config changes |
| Evidence | Create (never update/delete) |
