Purpose: CVM solves the information security challenges associated with static, long-lived API or programmatic access credentials. It enables human and non-human identities to securely request short-lived, scoped credentials with TTLs, network ACLs, and permission boundaries. If a credential leaks, impact is minimal due to automatic expiry and active revocation.
- Architecture Overview
- Phase 0: Security Foundation (Before All Features)
- Phase 1: Core Infrastructure
- v0.1.0: GitHub + Okta Integration
- v0.2.0: Expanded Backends & Renewal
- v0.3.0+: Future Vision
- Security Architecture
- Testing Strategy
- Appendix: Crate Recommendations
- Appendix: API Reality Constraints
CVM is a single Rust binary that operates in three modes:
graph TD
subgraph CVM["CVM Binary"]
CLI["CLI Mode<br/><i>clap · oneshot</i>"]
TUI["TUI Mode<br/><i>ratatui · interactive</i>"]
Server["Server Mode<br/><i>axum REST API · long-running + TTL</i>"]
subgraph Core["Core Engine · cvm-core lib"]
Policy["Policy Engine<br/><i>YAML ABAC policies</i>"]
Lifecycle["Lifecycle<br/><i>State machine + leases</i>"]
TTL["TTL Scheduler<br/><i>Durable state · crash-safe</i>"]
Audit["Audit Log<br/><i>HMAC-chained · tamper-evident</i>"]
Secrets["Secrets Backend<br/><i>Pluggable backends</i>"]
end
CLI --> Core
TUI --> Core
Server --> Core
end
Core --> GitHub["GitHub Provider"]
Core --> Okta["Okta Provider"]
Core --> Future["Future Provider"]
style CVM fill:#1a1a2e,stroke:#e94560,color:#eee
style Core fill:#16213e,stroke:#0f3460,color:#eee
style CLI fill:#0f3460,stroke:#53a8b6,color:#eee
style TUI fill:#0f3460,stroke:#53a8b6,color:#eee
style Server fill:#0f3460,stroke:#53a8b6,color:#eee
style Policy fill:#1a1a2e,stroke:#e94560,color:#ccc
style Lifecycle fill:#1a1a2e,stroke:#e94560,color:#ccc
style TTL fill:#1a1a2e,stroke:#e94560,color:#ccc
style Audit fill:#1a1a2e,stroke:#e94560,color:#ccc
style Secrets fill:#1a1a2e,stroke:#e94560,color:#ccc
style GitHub fill:#238636,stroke:#2ea043,color:#fff
style Okta fill:#238636,stroke:#2ea043,color:#fff
style Future fill:#238636,stroke:#2ea043,color:#fff
| Capability | CLI | TUI | Server |
|---|---|---|---|
| Credential creation | Yes | Yes | Yes |
| TTL enforcement (Tier 1: platform-native) | Yes | Yes | Yes |
| TTL enforcement (Tier 2: suspend) | No* | No* | Yes |
| TTL enforcement (Tier 3: delete/revoke) | No* | No* | Yes |
| Background scheduling | No | No | Yes |
| mTLS client authentication | N/A | N/A | Yes |
| Multi-user/RBAC | No | No | Yes |
* CLI/TUI record pending TTL actions to durable storage. Server mode or cvm gc processes them later.
CVM operates as a Security Token Service (STS) — inspired by Chainguard's Octo-STS for GitHub. An STS exchanges a short-lived third-party token for a short-lived first-party token, after checking that the caller has permission to make the exchange.
Two operating modes based on platform capability:
| Mode | When | How |
|---|---|---|
| Broker mode | Platform lacks native OIDC federation (GitHub, Okta, Cloudflare, Datadog) | CVM accepts OIDC token → validates against trust policy → mints platform credential using stored admin key |
| Passthrough mode | Platform has native OIDC federation (AWS, GCP, Azure) | CVM configures the platform's native OIDC trust policies. CVM's value = policy management, audit, and lifecycle — not token brokering |
CVM is most impactful for platforms in broker mode — where no existing tool provides OIDC-federated short-lived credential vending.
Trust Policy Format (v0.2.0+):
apiVersion: cvm/v1
kind: TrustPolicy
metadata:
name: ci-deploy
provider: cloudflare # github | okta | cloudflare | datadog
# OIDC identity matching (same for every provider)
identity:
issuer: https://token.actions.githubusercontent.com
subject: repo:my-org/my-app:ref:refs/heads/main
claim_patterns:
workflow_ref: "my-org/my-app/.github/workflows/deploy.yml@.*"
# TTL override (bounded by provider max)
ttl: 30m
# Provider-specific permissions (validated by provider crate)
permissions:
# Cloudflare example:
permission_groups:
- id: "<dns-edit-group-id>"
resources:
"com.cloudflare.api.account.zone.<ZONE_ID>": "*"The trust policy uses a generic envelope (identity matching + TTL) with a provider-specific permissions block. The identity section is the same for all providers because OIDC tokens have the same claims structure. The permissions block is provider-specific because each platform's authorization model is fundamentally different.
CLI mode exits after command execution. It cannot schedule future API calls. Therefore:
- CLI/TUI mode creates credentials with platform-native TTL (Tier 1) only. If the platform doesn't support native TTL, CLI mode refuses to vend unless server mode is confirmed reachable, or the user explicitly opts in to reduced enforcement with
--acknowledge-no-ttl. - Server mode handles all TTL tiers via
tokio-based background scheduling with durable state persistence (SQLite). On crash/restart, it sweeps for overdue expirations and immediately revokes them. cvm gccommand provides lazy enforcement for CLI-only users — processes expired credentials from the durable TTL store on demand.
cvm/
Cargo.toml # Workspace root
deny.toml # cargo-deny policy
rust-toolchain.toml # Pinned Rust version
supply-chain/ # cargo-vet audits
crates/
cvm-core/ # #![forbid(unsafe_code)]
src/
lib.rs
credential.rs # Credential types, state machine
lifecycle.rs # Lease model, TTL logic
policy.rs # YAML ABAC policy engine
audit.rs # HMAC-chained audit log
storage.rs # SecretsBackend trait
provider.rs # Provider trait, AnyProvider enum
error.rs # Error types (never contain credentials)
cvm-memsec/ # #![allow(unsafe_code)] — ONLY exception
src/lib.rs # mlock, zeroize wrappers
cvm-provider-github/ # #![forbid(unsafe_code)]
cvm-provider-okta/ # #![forbid(unsafe_code)]
cvm-provider-cloudflare/ # #![forbid(unsafe_code)] (v0.2.0)
cvm-provider-datadog/ # #![forbid(unsafe_code)] (v0.2.0)
cvm-provider-terraform/ # #![forbid(unsafe_code)] (v0.2.0)
cvm-provider-atlassian/ # #![forbid(unsafe_code)] (v0.2.0)
cvm-provider-snyk/ # #![forbid(unsafe_code)] (v0.2.0)
cvm-provider-vanta/ # #![forbid(unsafe_code)] (v0.3.0)
cvm-storage-keychain/ # OS native keychain (via keyring crate)
cvm-storage-onepassword/ # 1Password Connect API (v0.2.0)
cvm-storage-vault/ # HashiCorp Vault (v0.2.0)
cvm-storage-aws/ # AWS Secrets Manager (v0.2.0)
cvm-cli/ # CLI interface (clap)
cvm-tui/ # TUI interface (ratatui)
cvm-server/ # REST API server (axum)
cvm/ # Unified binary entry point
Cargo.toml # Feature flags for optional components
src/main.rs # Mode dispatch, runtime construction
Feature flags (unified binary):
[features]
default = ["cli", "tui", "github", "okta", "storage-keychain"]
cli = ["dep:cvm-cli"]
tui = ["dep:cvm-tui"]
server = ["dep:cvm-server"]
github = ["dep:cvm-provider-github"]
okta = ["dep:cvm-provider-okta"]
cloudflare = ["dep:cvm-provider-cloudflare"]
datadog = ["dep:cvm-provider-datadog"]
terraform = ["dep:cvm-provider-terraform"]
atlassian = ["dep:cvm-provider-atlassian"]
snyk = ["dep:cvm-provider-snyk"]
vanta = ["dep:cvm-provider-vanta"]
storage-keychain = ["dep:cvm-storage-keychain"]
storage-1password = ["dep:cvm-storage-onepassword"]
storage-vault = ["dep:cvm-storage-vault"]
storage-aws = ["dep:cvm-storage-aws"]Container builds (v0.1.0): --no-default-features --features server,github,okta,storage-keychain
Container builds (v0.2.0+): --no-default-features --features server,github,okta,cloudflare,datadog,terraform,atlassian,storage-keychain
This phase MUST be completed before ANY feature development begins.
Rust-level protections:
| Protection | Implementation |
|---|---|
| No unsafe code in application logic | #![forbid(unsafe_code)] on all crates except cvm-memsec |
| Security-focused clippy lints | unwrap_used = "deny", panic = "deny", print_stdout = "deny", dbg_macro = "deny" |
| No credential material in errors | Custom error types via thiserror that never include token values |
| Memory zeroization | zeroize crate with ZeroizeOnDrop derive on all credential-holding structs |
| Secret string types | secrecy::SecretString — prevents Debug/Display from leaking values |
| mlock for credential cache | cvm-memsec crate with mlock() wrappers to prevent swap-to-disk |
Clippy configuration (Cargo.toml workspace lints):
[workspace.lints.clippy]
correctness = { level = "deny" }
suspicious = { level = "deny" }
unwrap_used = "deny"
expect_used = "warn"
panic = "deny"
indexing_slicing = "warn"
dbg_macro = "deny"
print_stdout = "deny"
print_stderr = "deny"
todo = "deny"
unimplemented = "deny"
[workspace.lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"Tooling:
| Tool | Purpose | CI Integration |
|---|---|---|
cargo-audit |
CVE scanning against RustSec Advisory DB | rustsec/audit-check GitHub Action, fail on any advisory |
cargo-deny |
License, ban, source, and advisory policy enforcement | EmbarkStudios/cargo-deny-action, runs on every PR |
cargo-vet |
Supply chain auditing — track who audited each dep version | Manual + CI verification |
cargo-geiger |
Count unsafe usage across dependency tree | CI artifact report |
| Semgrep | SAST with Rust rules | semgrep/semgrep-action with p/rust + p/security-audit |
deny.toml (cargo-deny policy):
[advisories]
vulnerability = "deny"
unmaintained = "warn"
yanked = "deny"
[licenses]
unlicensed = "deny"
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-3.0", "Zlib"]
copyleft = "deny"
[bans]
wildcards = "deny"
deny = [
{ name = "openssl" }, # rustls exclusively
{ name = "openssl-sys" },
{ name = "native-tls" }, # rustls exclusively
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]Supply chain attack defenses:
- Commit
Cargo.lock(mandatory for binaries) - Audit all
build.rsscripts in the dependency tree viacargo metadata—build.rsruns arbitrary code at compile time - Pin all GitHub Actions to commit SHA, not tags
- Use
cargo-vetto track audit status of every dependency - Monitor for transitive dependency changes via
cargo-deny
Multi-stage build with Chainguard images:
# Stage 1: Build
FROM cgr.dev/chainguard/rust:latest AS builder
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl \
--no-default-features --features server,github,okta,storage-keychain
# Stage 2: Runtime (distroless)
FROM cgr.dev/chainguard/static:latest
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/cvm /cvm
ENTRYPOINT ["/cvm"]Container hardening checklist:
- Chainguard
staticbase image (distroless, ~2-3 MB, includes CA certs) - Non-root user (
USER nonroot— Chainguard images default to this) -
--cap-drop=ALLat runtime - Read-only root filesystem (
--read-only) - No Docker socket mount
- Static musl binary (no libc dependency)
- TLS via
rustls+webpki-roots(Mozilla CA bundle compiled in)
CI image scanning:
- name: Scan image
uses: aquasecurity/trivy-action@v0.24
with:
image-ref: cvm:latest
severity: CRITICAL,HIGH
exit-code: 1# .github/workflows/security.yml
name: Security
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<pinned-sha>
- uses: rustsec/audit-check@<pinned-sha>
deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<pinned-sha>
- uses: EmbarkStudios/cargo-deny-action@<pinned-sha>
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<pinned-sha>
- run: cargo clippy --all-targets --all-features -- -D warnings
geiger:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<pinned-sha>
- run: cargo install cargo-geiger
- run: cargo geiger --all-features --output-format=json > geiger-report.json
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<pinned-sha>
- run: cargo install cargo-cyclonedx
- run: cargo cyclonedx --all --format jsonBuild provenance:
- SBOM generation (CycloneDX format) on every release
- Binary signing with Sigstore/cosign
- Reproducible builds (pinned
rust-toolchain.toml, compare hashes across runners) - SLSA Level 3 provenance for release artifacts
1Password CLI integration for testing:
CVM's integration tests require real platform credentials (GitHub admin token, Okta admin token). These MUST NOT be:
- Written to disk as plaintext
- Passed through any AI assistant's context window
- Hardcoded in test files or environment scripts
Pattern: op run for local testing:
# .env.test (safe to commit — contains only 1Password references)
GITHUB_ADMIN_TOKEN="op://CVM-Dev/GitHub Admin/credential"
OKTA_ADMIN_TOKEN="op://CVM-Dev/Okta Admin/credential"
OKTA_ORG_URL="op://CVM-Dev/Okta Admin/url"
# Run integration tests with secrets injected as env vars
op run --env-file=.env.test -- cargo test --test integrationPattern: CI/CD with 1Password Connect:
# GitHub Actions
- name: Load secrets
uses: 1password/load-secrets-action@<pinned-sha>
with:
export-env: true
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
GITHUB_ADMIN_TOKEN: op://CVM-CI/GitHub Admin/credential
OKTA_ADMIN_TOKEN: op://CVM-CI/Okta Admin/credentialIn Rust test code:
fn get_test_credential(name: &str) -> String {
std::env::var(name).unwrap_or_else(|_| {
panic!(
"{name} not set. Run tests with: op run --env-file=.env.test -- cargo test"
)
})
}Build the trait boundaries, data models, and core engine before any platform integration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CredentialState {
Pending, // Platform API call in flight
Active, // Credential is live and usable
Suspended, // Disabled on platform (reversible)
Revoked, // Permanently invalidated (terminal)
Deleted, // Record removed (tombstone kept for audit)
}Valid transitions enforced at the type level — invalid transitions return Err(LifecycleError::InvalidTransition).
Every credential issued by CVM gets a lease:
pub struct Lease {
pub lease_id: LeaseId, // ULID
pub credential_id: CredentialId,
pub platform: Platform,
pub scopes: Vec<Scope>,
pub issued_at: DateTime<Utc>,
pub ttl: Duration,
pub max_ttl: Duration, // Policy-enforced maximum
pub expires_at: DateTime<Utc>,
pub renewable: bool, // v0.1.0: always false
pub state: CredentialState,
pub correlation_id: String, // Embedded in credential metadata on platform
pub requestor: Identity,
}pub struct ProviderCapabilities {
pub native_ttl: bool, // Can set expiry at creation
pub suspendable: bool, // Can disable/suspend credentials
pub revocable: bool, // Can delete/revoke credentials
pub scopeable: bool, // Can limit permission scopes
pub verifiable: bool, // Can verify granted scopes post-creation
}
#[async_trait]
pub trait Provider: Send + Sync {
type Credential: CredentialInfo + Serialize;
type Config: DeserializeOwned + Validate;
fn provider_id(&self) -> &'static str;
fn capabilities(&self) -> ProviderCapabilities;
async fn vend(&self, req: &VendRequest) -> Result<VendResult<Self::Credential>, ProviderError>;
async fn revoke(&self, id: &CredentialId) -> Result<RevokeResult, ProviderError>;
async fn suspend(&self, id: &CredentialId) -> Result<(), ProviderError>;
async fn status(&self, id: &CredentialId) -> Result<CredentialStatus, ProviderError>;
async fn verify_scopes(&self, id: &CredentialId) -> Result<Vec<Scope>, ProviderError>;
async fn health_check(&self) -> HealthStatus;
}
// Enum dispatch instead of trait objects (exhaustive matching)
pub enum AnyProvider {
GitHub(GitHubProvider),
Okta(OktaProvider),
#[cfg(feature = "cloudflare")]
Cloudflare(CloudflareProvider), // v0.2.0
#[cfg(feature = "datadog")]
Datadog(DatadogProvider), // v0.2.0
#[cfg(feature = "terraform")]
Terraform(TerraformProvider), // v0.2.0
#[cfg(feature = "atlassian")]
Atlassian(AtlassianProvider), // v0.2.0
#[cfg(feature = "snyk")]
Snyk(SnykProvider), // v0.2.0
#[cfg(feature = "vanta")]
Vanta(VantaProvider), // v0.3.0
}ProviderError must NEVER contain credential material:
#[derive(Debug, thiserror::Error)]
pub enum ProviderError {
#[error("authentication failed for provider {provider}")]
AuthFailed { provider: String },
#[error("rate limited, retry after {retry_after:?}")]
RateLimited { retry_after: Duration },
#[error("network error: {message}")]
NetworkError { message: String },
#[error("operation not supported: {operation}")]
NotSupported { operation: String },
// Never: #[error("auth failed with token {token}")]
}#[async_trait]
pub trait SecretsBackend: Send + Sync {
async fn store(&self, key: &str, value: &SecretBytes, meta: &SecretMetadata) -> Result<(), SecretsError>;
async fn retrieve(&self, key: &str) -> Result<Option<StoredSecret>, SecretsError>;
async fn delete(&self, key: &str) -> Result<(), SecretsError>;
async fn list(&self, prefix: &str) -> Result<Vec<String>, SecretsError>;
async fn health_check(&self) -> Result<BackendHealth, SecretsError>;
fn capabilities(&self) -> BackendCapabilities;
}SecretBytes wraps Vec<u8> with ZeroizeOnDrop and blocks Debug/Display.
v0.1.0: YAML ABAC policies
# ~/.config/cvm/policy.yaml
version: "1"
rules:
- name: "default-allow-all"
effect: "allow"
identity: {}
resource: {}
constraints:
max_ttl: "1h"
max_renewals: 0Single-user mode with a default-allow policy. The policy engine trait exists for v0.2.0 extensibility:
pub trait PolicyEngine: Send + Sync {
fn evaluate(&self, request: &PolicyRequest) -> Result<PolicyDecision, PolicyError>;
}SQLite-backed, HMAC-chained, tamper-evident:
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL UNIQUE, -- ULID
timestamp TEXT NOT NULL, -- ISO 8601 UTC
event_type TEXT NOT NULL, -- credential.created, credential.revoked, etc.
actor_id TEXT NOT NULL,
platform TEXT NOT NULL,
lease_id TEXT,
action TEXT NOT NULL, -- create, revoke, suspend, delete
result TEXT NOT NULL, -- success, failure, denied
details TEXT, -- JSON (never contains credential values)
hash_chain TEXT NOT NULL -- SHA-256(prev_hash || this_record)
);SQLite at ~/.local/share/cvm/inventory.db:
CREATE TABLE leases (
lease_id TEXT PRIMARY KEY,
credential_id TEXT NOT NULL,
platform TEXT NOT NULL,
namespace TEXT NOT NULL DEFAULT 'default',
state TEXT NOT NULL,
scopes TEXT NOT NULL, -- JSON array
issued_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
correlation_id TEXT NOT NULL,
requestor TEXT NOT NULL,
renewable INTEGER NOT NULL DEFAULT 0,
renew_count INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_leases_expires ON leases(expires_at) WHERE state = 'active';
CREATE INDEX idx_leases_platform ON leases(platform, state);Do NOT use #[tokio::main]. Construct the runtime per mode:
fn main() {
let args = cli::parse_args(); // synchronous
match args.mode {
Mode::Cli(cmd) => {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(cli::execute(cmd));
}
Mode::Tui => {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(tui::run());
}
Mode::Server => {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
.unwrap();
rt.block_on(server::run());
}
}
}CLI mode pays zero cost for multi-thread runtime initialization.
Research verified the following constraints that reshape v0.1.0 scope:
| Credential Type | Create via API? | TTL Control? | Revoke via API? |
|---|---|---|---|
| GitHub Classic PAT | NO (web UI only) | NO | YES (POST /credentials/revoke) |
| GitHub Fine-Grained PAT | NO (web UI only) | NO | YES (same + org endpoints) |
| GitHub OAuth Token | YES (requires user interaction via device flow) | Limited (8h with refresh tokens) | YES (DELETE /applications/{id}/token) |
| GitHub App Installation Token | YES (fully automated) | NO (fixed 1h) | YES (DELETE /installation/token) |
| Okta SSWS API Token | NO (admin console only) | NO (30-day sliding window) | NO (admin console only) |
| Okta OAuth 2.0 Token | YES (client credentials flow) | YES (configurable) | YES (POST /oauth2/v1/revoke) |
Primary: GitHub App Installation Tokens (fully automated)
- Endpoint:
POST /app/installations/{installation_id}/access_tokens - Auth: JWT signed with App's private key
- TTL: Fixed 1 hour (platform-native, Tier 1) — aligns perfectly with CVM's model
- Scoping: Optional
repositoriesandpermissionsparameters for least-privilege - Revocation:
DELETE /installation/token - CVM workflow: Create GitHub App → Install on org/repos → CVM uses App's private key to mint short-lived installation tokens
Secondary: GitHub OAuth Tokens (user interaction required)
- Flow: Device flow (
POST /login/device/code) for CLI/TUI compatibility - User enters code at
github.com/device - TTL: 8 hours with refresh token rotation enabled; otherwise indefinite
- Revocation:
DELETE /applications/{client_id}/token - CVM workflow: Register OAuth App → User authorizes via device flow → CVM manages token lifecycle
Not supported in v0.1.0: Classic PATs, fine-grained PATs (no API creation endpoint exists)
Okta OAuth 2.0 Client Credentials (fully automated)
- Endpoint:
POST /oauth2/v1/tokenwithgrant_type=client_credentials - Auth: OAuth service app with admin roles assigned
- TTL: Configurable (default 1 hour, Tier 1 native TTL)
- Scoping: Scoped to specific Okta API permissions
- Revocation:
POST /oauth2/v1/revoke - CVM workflow: Create Okta OAuth service app → Assign scoped admin roles → CVM uses client credentials to mint tokens
Not supported in v0.1.0: SSWS API tokens (no API creation or revocation endpoints)
v0.2.0+ STS broker mode: CVM will accept OIDC tokens from any identity provider and exchange them for scoped Okta OAuth tokens. See Section 6.2 (STS Scaling Roadmap) and Appendix (Okta STS Design) for the full architecture.
# Bootstrap
cvm init # First-run setup wizard
cvm init --platform github # Set up GitHub App credentials
cvm init --platform okta # Set up Okta OAuth app credentials
# Credential vending
cvm create github --type app-token \
--repos my-repo --permissions contents:read \
--ttl 1h # GitHub App installation token
cvm create github --type oauth \
--scopes repo,workflow # GitHub OAuth via device flow
cvm create okta --scopes users:read,groups:read \
--ttl 30m # Okta OAuth token
# Lifecycle management
cvm list # List active credentials
cvm list --platform github --state active
cvm status # Platform connectivity + bootstrap health
cvm revoke <lease-id> # Manually revoke a credential
cvm gc # Process expired credentials
# Emergency
cvm emergency-revoke --platform github # Revoke ALL GitHub credentials
cvm bootstrap rotate --platform github # Rotate CVM's own admin credentialsPOST /v1/credentials # Vend a credential
GET /v1/credentials # List active credentials
GET /v1/credentials/{id} # Get credential status
DELETE /v1/credentials/{id} # Revoke a credential
POST /v1/credentials/gc # Trigger garbage collection
GET /v1/health # Health check (per-provider status)
GET /v1/metrics # Prometheus metrics
Server mode requires mTLS. CVM generates a CA at cvm init for client certificate signing.
- Phase 0 security foundation complete (all tooling, lints, CI pipeline)
- Core traits implemented: Provider, SecretsBackend, PolicyEngine
- Credential state machine with enforced transitions
- Lease model with durable SQLite persistence
- Audit log with HMAC chain integrity
- Native keychain secrets backend (via
keyringcrate) - 1Password CLI integration for dev/test credential injection
- GitHub App installation token provider
- GitHub OAuth device flow provider
- Okta OAuth client credentials provider
- CLI mode with full command set
- TUI mode with credential creation wizard
- Server mode with REST API + TTL scheduler
-
cvm initbootstrap flow -
cvm gcgarbage collection -
cvm emergency-revokefor incident response - Docker image with Chainguard base
- 100% test coverage with 3-tier test pyramid
- SBOM + binary signing for release artifacts
graph TD
P0["Phase 0<br/><b>Security Foundation</b>"]
P1["Phase 1<br/><b>Core Infrastructure</b>"]
SM["Credential state machine + lease model"]
PT["Provider trait + AnyProvider enum"]
SB["SecretsBackend trait + keychain impl"]
PE["Policy engine · default-allow"]
AL["Audit log with HMAC chain"]
DB["SQLite inventory database"]
PP["Platform Providers<br/><i>parallel</i>"]
GH["GitHub App installation tokens"]
GO["GitHub OAuth device flow"]
OK["Okta OAuth client credentials"]
IM["Interface Modes<br/><i>parallel</i>"]
CLI["CLI · clap"]
TUI["TUI · ratatui"]
SRV["Server · axum + TTL scheduler"]
IT["Integration Testing<br/><i>E2E with real APIs</i>"]
DC["Docker + CI/CD + Release"]
REL(["v0.1.0 Release"])
P0 --> P1
P1 --> SM & PT & SB & PE & AL & DB
SM & PT & SB & PE & AL & DB --> PP
PP --> GH & GO & OK
GH & GO & OK --> IM
IM --> CLI & TUI & SRV
CLI & TUI & SRV --> IT
IT --> DC
DC --> REL
style P0 fill:#e94560,stroke:#c81d4e,color:#fff
style P1 fill:#0f3460,stroke:#16213e,color:#fff
style PP fill:#0f3460,stroke:#16213e,color:#fff
style IM fill:#0f3460,stroke:#16213e,color:#fff
style IT fill:#533483,stroke:#5b2c8e,color:#fff
style DC fill:#533483,stroke:#5b2c8e,color:#fff
style REL fill:#238636,stroke:#2ea043,color:#fff
- 1Password Connect API — For team/org use cases where 1Password is the standard
- HashiCorp Vault — AppRole auth, KV v2 engine. Explore Vault's own GitHub/Okta secrets engines as an alternative to CVM doing the credential creation directly
- AWS Secrets Manager — IAM role auth, resource tags for metadata
- Lease renewal before expiry (configurable renewal window, e.g., at 80% TTL elapsed)
- Zero-downtime rotation: create new credential → overlap window → revoke old
cvm renew <lease-id>CLI command- Policy-enforced
max_renewalslimit
- Namespace CRUD:
cvm namespace create platform-team - All resources scoped to namespaces in SQLite and secrets backend keys
- Policy rules scoped per namespace
- Per-namespace rate limiting in server mode
Cloudflare — API Tokens (Tier 1: platform-native TTL)
CVM vends scoped Cloudflare API tokens with expires_on for platform-native expiry. Cloudflare is the ideal Tier 1 target — CVM creates a token, the platform enforces the TTL, no CVM scheduler needed.
- Create:
POST /client/v4/accounts/{account_id}/tokens - Auth: Bearer token with "API Tokens Write" permission (bootstrap credential)
- TTL: Native via
expires_onUTC timestamp field (Tier 1) - Scope: Policies with permission groups + resource IDs (account/zone/user granularity)
- Revoke:
DELETE /client/v4/user/tokens/{token_id} - Verify:
GET /client/v4/user/tokens/verify - Restrictions: IP allowlist/denylist in CIDR notation,
not_beforeactivation time - Constraint: Child tokens cannot have broader permissions than the parent token
ProviderCapabilities {
native_ttl: true, // expires_on field — Tier 1
suspendable: false, // No suspend API
revocable: true, // DELETE endpoint
scopeable: true, // Policies with permission groups + resources
verifiable: true, // GET /tokens/verify
}Datadog — Application Keys (Tier 3: CVM-managed revocation)
CVM vends scoped Datadog application keys on service accounts. Application keys never expire — CVM is the sole TTL enforcement mechanism via active deletion. This is the highest-risk provider and the best stress test for CVM's zombie credential protections.
- Create:
POST /api/v2/service_accounts/{service_account_id}/application_keys - Auth: API key + admin application key headers (
DD-API-KEY+DD-APPLICATION-KEY) - TTL: None — application keys never expire (Tier 3 only)
- Scope: Fine-grained scopes on keys (
dashboards_read,monitors_read,metrics_read,logs_read_data, etc.) - Revoke:
DELETE /api/v2/service_accounts/{service_account_id}/application_keys/{app_key_id} - OTR: One-Time Read mode (default since Aug 2025) — key value shown once at creation, never retrievable later
- Constraint: CLI mode must warn or refuse without
--acknowledge-no-ttlsince platform has no native TTL
ProviderCapabilities {
native_ttl: false, // Keys never expire — Tier 3 only
suspendable: false, // No suspend API (disabling service account revokes ALL keys)
revocable: true, // DELETE endpoint
scopeable: true, // Scoped application keys
verifiable: false, // No scope verification endpoint
}HCP Terraform (Terraform Cloud) — Team Tokens (Tier 1: platform-native TTL)
CVM vends scoped team tokens with expired-at for platform-native expiry. TFC acts as an OIDC provider for cloud access, but has no OIDC-based API authentication — all API access uses static tokens. CVM fills this gap by vending short-lived team tokens.
- Create:
POST /api/v2/teams/{team_id}/authentication-tokens - Auth:
Authorization: Bearer {ADMIN_TOKEN}(user or org token with team management permissions) - TTL: Native via
expired-atISO 8601 timestamp field (Tier 1). Default: 2 years. CVM sets short TTLs (e.g., 1h). - Scope: Team-based — team permissions determine workspace/project access (one indirection layer)
- Revoke:
DELETE /api/v2/authentication-tokens/{token_id} - List:
GET /api/v2/teams/{team_id}/authentication-tokens - Org TTL enforcement: Orgs can set per-token-type maximum TTL caps that override creation requests
- Constraint: Token value shown once at creation, never recoverable. Scoping requires pre-created teams with correct workspace permissions.
ProviderCapabilities {
native_ttl: true, // expired-at field — Tier 1
suspendable: false, // No suspend API
revocable: true, // DELETE endpoint
scopeable: true, // Via team → workspace permission mapping
verifiable: false, // No scope verification endpoint post-creation
}Atlassian Jira Cloud — OAuth 2.0 Client Credentials Access Tokens (Tier 1: platform-native TTL)
CVM vends 1-hour OAuth access tokens via client credentials flow. Atlassian's token endpoint issues tokens with a fixed 1-hour TTL, no refresh tokens — the simplest possible STS integration. However, scopes are fixed at app registration time (not per-token), so CVM needs tiered app registrations for different scope profiles.
- Create:
POST https://api.atlassian.com/oauth/tokenwithgrant_type=client_credentials - Auth:
client_id+client_secret(stored as bootstrap credential) - TTL: Fixed 1 hour (platform-native, Tier 1). No refresh tokens issued.
- Scope: Granular OAuth scopes (
read:issue:jira,write:issue:jira,read:project:jira, etc.) — but fixed at app registration, NOT per-token - Revoke: No per-token revocation API. Rely on 1-hour TTL as the security boundary. Emergency revocation requires admin deauthorization of the app at
admin.atlassian.com. - Cloud ID discovery:
GET https://api.atlassian.com/oauth/token/accessible-resources - API base:
https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/... - Constraint: Scope changes require admin re-authorization. Client secret rotation is UI-only.
ProviderCapabilities {
native_ttl: true, // Fixed 1h TTL — Tier 1
suspendable: false, // No suspend API
revocable: false, // No per-token revocation API
scopeable: false, // Scopes fixed at app registration, not per-token
verifiable: false, // No scope verification endpoint
}Snyk — OAuth 2.0 Access Token (Tier 1: platform-native TTL)
CVM vends 1-hour Snyk OAuth access tokens via pre-provisioned service account client credentials. Snyk's OAuth server enforces the TTL natively. Role-based scoping via service account role_id.
- Service account create:
POST /rest/orgs/{orgId}/service_accountswithauth_type: oauth_client_secret - Token mint:
POST /oauth2/tokenwithgrant_type=client_credentials - Auth:
client_id+client_secret(bootstrap credential per service account) - TTL: Native via
access_token_ttl_seconds(default 1h, max 1y, Tier 1) - Scope: Role-based — Org Admin, Org Collaborator, or custom roles
- Revoke:
POST /oauth2/revoke(token-level) orDELETE /rest/orgs/{orgId}/service_accounts/{id}(account-level) - Constraint: Enterprise plan required. Scopes are per-service-account, not per-token.
ProviderCapabilities {
native_ttl: true, // access_token_ttl_seconds — Tier 1
suspendable: false, // No suspend API
revocable: true, // POST /oauth2/revoke + DELETE service account
scopeable: true, // Role-based via role_id
verifiable: false, // No scope verification endpoint
}Vanta — OAuth 2.0 Access Token (Tier 1: fixed 1h TTL, v0.3.0)
CVM vends 1-hour Vanta OAuth access tokens via client credentials. Per-token scoping via the scope parameter makes this a clean STS integration — but the single-active-token-per-app constraint requires either multi-app bootstrap or token caching.
- Create:
POST https://api.vanta.com/oauth/tokenwithgrant_type=client_credentials - Auth:
client_id+client_secret(bootstrap credential, UI-created) - TTL: Fixed 1 hour (Tier 1, platform-native)
- Scope: Per-token via
scopeparameter (vanta-api.{resource}:{read|write}) - Revoke: Implicit only — re-minting invalidates previous token
- Constraint: One active token per app. Concurrent workloads need separate apps or token caching.
ProviderCapabilities {
native_ttl: true, // Fixed 1h — Tier 1
suspendable: false, // No suspend API
revocable: false, // No explicit revocation endpoint
scopeable: true, // Per-token scope subsetting at mint time
verifiable: false, // No verification endpoint
}These platforms have native OIDC federation — CVM's role is policy management and audit, not token brokering:
- AWS IAM —
sts:AssumeRoleWithWebIdentityaccepts OIDC tokens natively. CVM manages IAM role trust policies + audit trail. - GCP — Workload Identity Federation accepts OIDC tokens natively. CVM manages pool/provider configs + audit trail.
- Azure AD — Federated Identity Credentials accept OIDC tokens natively. CVM manages app registration configs + audit trail.
- Consider Cedar (Rust-native policy language by AWS) for cross-policy composition
- Policy versioning and rollback
- Policy dry-run mode:
cvm policy test --request '...'
- Credential proxy mode — CVM sits between client and platform, automatically injecting credentials. Client never sees the raw credential.
- OIDC federation (STS broker mode) — CVM accepts third-party OIDC tokens (from GitHub Actions, GCP, AWS, Kubernetes, SPIFFE) and exchanges them for platform-specific short-lived credentials. Inspired by Chainguard's Octo-STS. Trust policies define which identities can request which credentials with which scopes.
- Trust policy GitOps — Trust policies stored in a git repo, applied via webhook on push. PR-based review workflow for policy changes.
- Kubernetes operator — CRD-based credential requests, sidecar injection of credentials into pods
- Web dashboard — Credential inventory visualization, audit log search, policy editor
- Alerting — Webhook/Slack notifications for: credential vend failures, TTL enforcement failures, unusual vending patterns, bootstrap credential rotation due
- Compliance reports — SOC2/ISO 27001 evidence exports from audit log
| Phase | STS Capability |
|---|---|
| v0.1.0 | CLI/TUI/Server credential vending for GitHub + Okta. No OIDC federation yet. |
| v0.2.0 | STS broker endpoint (/v1/sts/exchange). Trust policies in local YAML files. Cloudflare + Datadog providers. Token caching for identical trust policy matches. |
| v0.3.0 | Trust policies in git repo (GitOps). Policy hot-reload. AWS/GCP/Azure passthrough mode (CVM configures native OIDC federation, manages policy + audit). |
| v1.0.0 | Full STS: any OIDC identity → any platform credential. Self-service trust policy onboarding. Credential proxy mode (workload never sees raw token). |
Okta-STS Design Note: For Okta, CVM operates as a broker for a pool of pre-provisioned service app identities. Okta service apps require admin role assignment (not just scopes), so CVM uses a tiered model:
- Tier 1 (Read-Only):
okta.users.read,okta.groups.read,okta.logs.read— CI pipelines, monitoring - Tier 2 (Group-Scoped):
okta.users.manage,okta.groups.manage— team provisioning automation - Tier 3 (Org Admin): All
okta.*scopes — emergency operations, org-wide automation
Scopes are subsetted at token request time (the scope parameter in the /token call), so each tier covers many trust policies without needing one service app per workload.
| Platform | CVM Mode | Credential Type | TTL Tier | OIDC Federation? | Create via API? | Revoke via API? |
|---|---|---|---|---|---|---|
| GitHub | Broker | App installation token | Tier 1 (1h fixed) | No | Yes | Yes |
| Okta | Broker | OAuth 2.0 access token | Tier 1 (1h configurable) | No | Yes | Yes |
| Cloudflare | Broker | API token | Tier 1 (expires_on) |
No | Yes | Yes |
| Datadog | Broker | Application key | Tier 3 (no native TTL) | No | Yes | Yes |
| HCP Terraform | Broker | Team token | Tier 1 (expired-at) |
No (outbound only) | Yes | Yes |
| Atlassian Jira | Broker | OAuth access token | Tier 1 (1h fixed) | No | Yes | No (rely on TTL) |
| Snyk | Broker | OAuth access token | Tier 1 (1h default, configurable) | No | Yes | Yes |
| Vanta | Broker | OAuth access token | Tier 1 (1h fixed) | No | No (implicit only) | Yes |
| AWS IAM | Passthrough | STS session tokens | Tier 1 (15min–12h) | Yes (native) | N/A | N/A |
| GCP IAM | Passthrough | OAuth access token | Tier 1 (configurable) | Yes (native) | N/A | N/A |
| Azure AD | Passthrough | OAuth access token | Tier 1 (configurable) | Yes (native) | N/A | N/A |
| Slack | Broker | Bot tokens | Tier 3 (no native TTL) | No | TBD | TBD |
| PagerDuty | Broker | OAuth scoped tokens | Tier 3 (30-day expiry) | No | TBD | TBD |
| HashiCorp Vault | Passthrough | Dynamic secrets | Tier 1 (lease-based) | Yes (native) | N/A | N/A |
Crown jewels: CVM's own admin credentials for each platform. If compromised, all connected platforms are compromised.
Top 5 threats (from Red Team analysis):
| # | Threat | Severity | Mitigation |
|---|---|---|---|
| 1 | Zombie credentials: scheduler crash = credentials live forever | CRITICAL | Durable SQLite TTL state + startup sweep |
| 2 | Admin credentials in memory without zeroization | CRITICAL | zeroize + secrecy crates + mlock |
| 3 | No post-creation scope verification | HIGH | Verify actual permissions via platform API after creation |
| 4 | CLI cannot enforce TTLs without running server | HIGH | Refuse non-Tier-1 in CLI mode unless server reachable |
| 5 | build.rs supply chain attack surface |
HIGH | Audit all build scripts, cargo-deny + cargo-vet |
graph TD
User["User / Service"]
CVM["CVM Server"]
Policy["Policy Engine<br/><i>authz check</i>"]
Admin["CVM Admin Credentials<br/><i>stored in keychain/vault</i>"]
APIs["Platform APIs<br/><i>GitHub, Okta, ...</i>"]
User -- "mTLS<br/><b>Boundary 1</b>" --> CVM
CVM --> Policy
Policy -- "<b>Boundary 2</b>" --> Admin
Admin -- "HTTPS<br/><b>Boundary 3</b>" --> APIs
style User fill:#533483,stroke:#5b2c8e,color:#fff
style CVM fill:#0f3460,stroke:#16213e,color:#fff
style Policy fill:#1a1a2e,stroke:#e94560,color:#eee
style Admin fill:#e94560,stroke:#c81d4e,color:#fff
style APIs fill:#238636,stroke:#2ea043,color:#fff
- Boundary 1: User → CVM (mTLS in server mode, OS session in CLI mode)
- Boundary 2: CVM → Secrets Backend (keychain ACLs, Vault AppRole)
- Boundary 3: CVM → Platform APIs (HTTPS, admin credentials)
| Mode | Delivery | Security Properties |
|---|---|---|
| CLI (default) | stdout | Ephemeral, pipe-friendly. Risk: terminal scrollback |
CLI --clipboard |
Clipboard | Auto-clear after 60s. Risk: clipboard managers |
CLI --env |
Shell env var export | Dies with shell session. Risk: child processes |
CLI --format json |
Structured stdout | For automation piping |
| Server | HTTPS response body | mTLS encrypted channel. No logging of response body |
After creating a credential on any platform, CVM MUST verify the actual granted permissions before returning the credential to the requestor. This prevents scope escalation bugs:
// In provider implementation:
async fn vend(&self, req: &VendRequest) -> Result<VendResult, ProviderError> {
let credential = self.create_on_platform(req).await?;
// Verify what was actually granted
let actual_scopes = self.verify_scopes(&credential.id).await?;
if !req.scopes.is_subset_of(&actual_scopes) {
// Granted more than requested — log warning but allow
// (platform may grant broader scopes)
}
if !actual_scopes.satisfies(&req.scopes) {
// Granted less than requested — revoke and fail
self.revoke(&credential.id).await?;
return Err(ProviderError::InsufficientScopes { ... });
}
Ok(VendResult { credential, actual_scopes, lease })
}Every credential CVM creates embeds a CVM correlation ID in the platform's metadata:
- GitHub App tokens: Token description/note includes
cvm:lease-{id} - GitHub OAuth: App name includes CVM identifier
- Okta OAuth: Token metadata includes
cvm_lease_idcustom claim
This enables correlating CVM's audit log with the platform's own access logs during incident investigation.
100% line coverage measured by cargo-llvm-cov. CI gate fails below 100%.
Coverage exclusions (require code review approval):
main()entry point in unified binary- Platform-specific feature-gated code not compiled in test profile
#[cfg(not(tarpaulin_include))]on truly trivial derive-generated code
| Tier | Type | Tools | Runtime | CI Trigger |
|---|---|---|---|---|
| 1 | Unit + Property | #[test], proptest |
< 30s | Every push |
| 2 | Integration (HTTP mock) | wiremock, mockall, insta |
< 5 min | Every PR |
| 3 | E2E (Real APIs) | Real platform accounts, op run |
< 15 min | Nightly + release |
Correctness tests:
- Credential state machine transitions (valid and invalid)
- Lease TTL arithmetic and expiry calculation
- Policy engine evaluation (allow, deny, TTL clamping)
- Provider vend/revoke/suspend/status flows
Property-based tests (proptest):
- Arbitrary TTL durations produce valid
expires_attimestamps - All valid scope strings are accepted, all invalid strings are rejected
- Policy evaluation is deterministic (same input always same output)
- Audit log hash chain is valid for any sequence of events
Security hygiene tests:
SecretByteszeroes memory on drop (viaunsafetest-only memory inspection)DebugandDisplayimpls of credential types never emit secret valuesProviderErrorvariants never contain credential material- Error messages grep-clean for token patterns (
ghp_,SSWS,Bearer)
Integration tests (wiremock):
- GitHub App installation token flow: JWT auth → create token → verify scopes → revoke
- Okta OAuth flow: client credentials → token creation → revocation
- Error handling: rate limits, auth failures, network timeouts
- Audit log entries generated for each operation
Snapshot tests (insta):
- CLI command output format
- Error message format
- Audit log JSON structure
- API response format (server mode)
E2E tests (real APIs, nightly):
- Full round-trip: create GitHub App token → use it → revoke it
- Full round-trip: create Okta OAuth token → use it → revoke it
- Bootstrap flow:
cvm init→cvm create→cvm revoke - TTL enforcement: create credential → wait → verify revoked (short TTL)
- Credential injection via
op run --env-file=.env.test
- Each E2E test run uses unique lease IDs and correlation IDs
- Tests clean up all created credentials in teardown (even on failure)
- Dedicated test GitHub App and Okta OAuth app with minimal permissions
- Test accounts managed by CVM itself (dogfooding)
- Flaky test policy: quarantined within 24h, fixed or deleted within 48h
| Use Case | Crate | Version | Notes |
|---|---|---|---|
| Async runtime | tokio |
1.x | rt-multi-thread, macros, net, time, signal, sync |
| HTTP client | reqwest |
0.12 | default-features = false, features = ["rustls-tls", "json"] |
| REST API server | axum |
0.8 | Tower middleware for auth, rate limiting, tracing |
| CLI parsing | clap |
4 | derive feature for argument structs |
| TUI framework | ratatui |
0.29 | crossterm backend |
| Configuration | config |
0.15 | Layered: defaults → file → env → CLI args. TOML format |
| Serialization | serde + serde_json + toml |
1 / 1 / 0.8 | |
| Structured logging | tracing + tracing-subscriber |
0.1 / 0.3 | JSON output for audit, env-filter for log levels |
| Error handling | thiserror (lib) + anyhow (bin) |
2 / 1 | |
| TLS | rustls + webpki-roots |
0.23 / 0.26 | Pure Rust, no OpenSSL |
| Secret handling | secrecy + zeroize |
0.10 / 1 | ZeroizeOnDrop derive |
| Memory locking | memsecurity |
3 | mlock + zeroize wrapper |
| Cryptographic HMAC | ring |
0.17 | For audit log hash chain |
| Encryption at rest | chacha20poly1305 |
0.10 | AEAD for local encrypted storage |
| Database | rusqlite |
0.32 | SQLite for inventory + audit log |
| Keychain | keyring |
3 | Cross-platform native keychain |
| UUIDs | ulid |
1 | Sortable unique IDs for leases |
| Time | chrono |
0.4 | Timestamp handling |
| Use Case | Crate | Notes |
|---|---|---|
| Property testing | proptest |
Fuzz inputs, TTL arithmetic, scope validation |
| HTTP mocking | wiremock |
Mock platform API responses |
| Trait mocking | mockall |
#[automock] on Provider, SecretsBackend |
| Snapshot testing | insta |
CLI output, error messages, audit log format |
| Coverage | cargo-llvm-cov |
100% coverage gate in CI |
| Fuzzing | cargo-fuzz |
Config parsing, API response deserialization |
| Tool | Purpose |
|---|---|
cargo-audit |
CVE scanning against RustSec DB |
cargo-deny |
License, ban, source, advisory policy |
cargo-vet |
Supply chain auditing |
cargo-geiger |
Unsafe code counting |
| Semgrep | SAST with Rust rules |
| Trivy/Grype | Container image scanning |
cargo-cyclonedx |
SBOM generation |
What works for automated credential vending:
-
GitHub App Installation Tokens — The only fully automated, short-lived credential type
- Create:
POST /app/installations/{id}/access_tokens(JWT auth with App private key) - TTL: Fixed 1 hour (cannot customize)
- Scope down:
repositories+permissionsparameters - Revoke:
DELETE /installation/token
- Create:
-
OAuth Tokens via Device Flow — Requires one-time user interaction
- Device code:
POST /login/device/code - User visits:
github.com/deviceand enters code - Poll for token:
POST /login/oauth/access_token - TTL: Indefinite by default; 8h with refresh token rotation enabled
- Revoke:
DELETE /applications/{client_id}/token
- Device code:
-
Credential Revocation API — Revoke any token type
POST /credentials/revoke(unauthenticated, up to 1000 tokens per request)- Works for classic PATs, fine-grained PATs, OAuth tokens, App user tokens
What does NOT work:
- Classic PATs: Cannot be created via API (web UI only)
- Fine-grained PATs: Cannot be created via API (web UI only, org approval flow only)
What works for automated credential vending:
- OAuth 2.0 Client Credentials — Fully automated
- Create:
POST /oauth2/v1/tokenwithgrant_type=client_credentials - TTL: Configurable (default 1 hour)
- Scoped: Specific Okta API permissions
- Revoke:
POST /oauth2/v1/revoke - Requires: OAuth service app with admin roles in Okta
- Create:
What does NOT work:
- SSWS API Tokens: Cannot be created via API (admin console only)
- SSWS API Tokens: Cannot be revoked via API (admin console only)
- SSWS API Tokens: No configurable TTL (30-day sliding window only)
- CVM's value proposition for GitHub is strongest with GitHub Apps (the recommended GitHub integration pattern anyway)
- CVM's value proposition for Okta is strongest with OAuth 2.0 (also Okta's recommended programmatic access pattern)
- Both platforms' recommended approaches align perfectly with CVM's automated, short-lived credential model
- Classic PATs and SSWS tokens could be tracked/managed by CVM (manual import + lifecycle management) in a future version, but cannot be vended automatically
What works for automated credential vending:
- API Tokens — Fully automated, scoped, with platform-native TTL
- Create:
POST /client/v4/accounts/{account_id}/tokens(account-owned) orPOST /client/v4/user/tokens(user-owned) - Auth:
Authorization: Bearer {PARENT_TOKEN}— parent token must have "API Tokens Write" permission - TTL: Native via
expires_onUTC timestamp (e.g.,"2026-04-04T01:15:00Z") — Tier 1 - Activation: Optional
not_beforetimestamp for delayed activation - Scope: Policies with
effect(allow/deny),permission_groups(by ID),resources(account/zone/user) - IP restrictions:
condition.request.ip.in/condition.request.ip.not_in(CIDR notation) - Revoke:
DELETE /client/v4/user/tokens/{token_id}orDELETE /client/v4/accounts/{account_id}/tokens/{token_id} - Verify:
GET /client/v4/user/tokens/verify(confirms token is active) - List permission groups:
GET /client/v4/user/tokens/permission_groups
- Create:
Key constraints:
- Child tokens cannot have broader permissions than the parent token
- Token value shown once at creation, cannot be retrieved later
- Permission groups are identified by opaque IDs, not human-readable names — CVM must cache the ID↔name mapping
- Default: tokens do not expire if
expires_onis not set (CVM must always set it) - No OIDC federation for Cloudflare API access — there is an open community feature request with no Cloudflare response
Permission resource types:
| Type | Scope Format | Example |
|---|---|---|
| Account | com.cloudflare.api.account.<ACCOUNT_ID> |
Account settings, Workers, R2, D1 |
| Zone | com.cloudflare.api.account.zone.<ZONE_ID> |
DNS, Firewall, Page Rules, SSL |
| User | com.cloudflare.api.user.<USER_TAG> |
API tokens, memberships, user details |
What works for automated credential vending:
- Application Keys on Service Accounts — Fully automated, scoped, no native TTL
- Create:
POST /api/v2/service_accounts/{service_account_id}/application_keys - Auth:
DD-API-KEY: {api_key}+DD-APPLICATION-KEY: {admin_app_key}headers - TTL: None — application keys never expire (Tier 3: CVM-managed revocation only)
- Scope: Fine-grained scopes on keys (e.g.,
dashboards_read,monitors_read,metrics_read,logs_read_data,synthetics_read,security_monitoring_rules_read,incidents_read) - Revoke:
DELETE /api/v2/service_accounts/{service_account_id}/application_keys/{app_key_id} - List:
GET /api/v2/service_accounts/{service_account_id}/application_keys - Service account create:
POST /api/v2/service_accounts(requiresservice_account_writepermission) - OTR mode: One-Time Read (default for new orgs since August 2025) — key value shown once at creation
- Create:
Key constraints:
- Application keys NEVER expire — CVM is the sole TTL enforcement mechanism
- Datadog OAuth 2.0 exists but is restricted to Marketplace integrations (authorization code + PKCE only, no client credentials for general API access)
- Key creation/revocation has propagation delay ("may take a few seconds" due to distributed architecture)
- No OIDC federation for Datadog API access
- Scoped application keys can use any Datadog permission as a scope; unscoped keys inherit full permissions of the creating user/service account
- Datadog API keys (org-level, for data submission) are separate from application keys (for API reads/management)
DD-API-KEYheader is always required in addition toDD-APPLICATION-KEY— CVM vends the application key but the API key is shared
Zombie credential risk: If CVM crashes after creating a Datadog application key but before recording it in the durable TTL store, the key lives forever with no mechanism for automatic cleanup. CVM's durable SQLite state + startup sweep (Section 7.1, Threat #1) is critical for this provider.
What works for automated credential vending:
- Team Tokens — Fully automated, scoped via team permissions, with native TTL
- Create:
POST /api/v2/teams/{team_id}/authentication-tokenswithexpired-atfield - Auth: Bearer token from user/org token with team management permissions
- TTL: Native via
expired-atISO 8601 timestamp. Default 2 years, CVM sets short values (e.g., 1h). Orgs can enforce max TTL caps. - Scope: Team → workspace/project permission mapping (indirect)
- Revoke:
DELETE /api/v2/authentication-tokens/{token_id} - List:
GET /api/v2/teams/{team_id}/authentication-tokens
- Create:
Token types NOT suitable for CVM:
- User tokens: Inherit all permissions of the user across all orgs — violates least privilege
- Organization tokens: One per org, replaces previous — CVM would clobber existing tokens
- Agent tokens: Scoped to agent pools only, not for general API access
- Audit trail tokens: Read-only, one per org
Key constraints:
- No OIDC for API auth: TFC acts as an OIDC provider (outbound to AWS/GCP/Azure during runs) but does NOT accept OIDC tokens for API authentication
- Token value shown once at creation, never retrievable
- Scoping requires pre-created teams with correct workspace permissions — CVM bootstrap must create teams
- Org-level TTL enforcement caps can override CVM's requested
expired-at - HashiCorp Vault has a TFC secrets engine that already does dynamic TFC token generation — CVM should evaluate whether to wrap Vault or implement directly
OIDC details (outbound only, not for CVM API auth):
- Issuer:
https://app.terraform.io - JWKS:
https://app.terraform.io/.well-known/jwks - JWT claims include:
terraform_organization_name,terraform_workspace_name,terraform_run_id,terraform_run_phase - Sub format:
organization:{name}:project:{name}:workspace:{name}:run_phase:{phase}
What works for automated credential vending:
- OAuth 2.0 Client Credentials — Fully automated, 1-hour tokens, no user interaction
- Token endpoint:
POST https://api.atlassian.com/oauth/tokenwithgrant_type=client_credentials - Auth:
client_id+client_secret(from app registration atdeveloper.atlassian.com) - TTL: Fixed 1 hour (platform-native, Tier 1). No refresh tokens issued in client credentials flow.
- Scope: Granular OAuth scopes fixed at app registration time
- API base:
https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/... - Cloud ID:
GET https://api.atlassian.com/oauth/token/accessible-resources
- Token endpoint:
Key constraints:
- No per-token scoping: Scopes are set at app registration time, not per-token request. CVM must register tiered apps for different scope profiles (read-only, read-write, admin)
- No per-token revocation API: Access tokens cannot be individually revoked. The 1-hour TTL is the revocation boundary. Emergency revocation = admin deauthorizes the app.
- No OIDC for API auth: Atlassian supports OIDC for SSO, but not for programmatic API authentication
- No programmatic client secret rotation: Secret rotation requires developer console UI
- Scope changes require admin re-authorization at
admin.atlassian.com - API tokens (Basic Auth) are UI-only — cannot be created or revoked via API
Granular scope format: <verb>:<resource>:<product>
Examples:
read:issue:jira,write:issue:jira,delete:issue:jiraread:project:jira,write:project:jiraread:sprint:jira-software,write:sprint:jira-softwareread:board-scope:jira-softwareread:audit-log:jiraadmin:organization
What works for automated credential vending:
- OAuth 2.0 Access Token (via service account client credentials) — Fully automated
- Service account create:
POST /rest/orgs/{orgId}/service_accountswithauth_type: oauth_client_secretandrole_id - Token mint:
POST /oauth2/tokenwithgrant_type=client_credentials(form-urlencoded) - Auth:
client_id+client_secretfrom pre-provisioned service account - TTL: Native via
access_token_ttl_seconds(default 1h, max 1 year, Tier 1) - Scope: Role-based via
role_id(Org Admin, Org Collaborator, custom roles) - Revoke token:
POST /oauth2/revoke - Revoke service account:
DELETE /rest/orgs/{orgId}/service_accounts/{id} - Rotate secret:
POST /rest/orgs/{orgId}/service_accounts/{id}/secretswithmode: replace
- Service account create:
Key constraints:
- Enterprise plan required — OAuth service accounts not available on Free/Team plans
- Scoping is per-service-account (via role_id), not per-token — CVM needs tiered service accounts
- Max 2 active secrets per service account
access_token_ttl_secondsimmutable after creation — TTL change requires new service accountclient_secretshown once at creation (OTR)- No OIDC federation for API access
- Alternative:
private_key_jwtauth_type eliminates stored secret (requires hosting JWKS endpoint)
What works for automated credential vending:
- OAuth 2.0 Access Token (via client credentials) — Fully automated token minting
- Create:
POST https://api.vanta.com/oauth/tokenwithgrant_type=client_credentials - Auth:
client_id+client_secretfrom pre-provisioned "Manage Vanta" OAuth application - TTL: Fixed 1 hour (platform-native, Tier 1)
- Scope: Per-token via
scopeparameter —vanta-api.{resource}:{read|write}format - Revoke: No explicit endpoint — re-minting a token implicitly revokes the previous one
- Create:
Key constraints:
- Single active token per OAuth application — minting a new token invalidates the previous one. CVM needs one application per concurrent workload OR a token caching/sharing layer.
- Application creation is UI-only (Settings → Developer Console) — no programmatic bootstrap
- Client secret rotation is UI-only — no API for secret management
- Fixed 1h TTL — cannot be shortened or lengthened
- No OIDC federation for API access
- No explicit token revocation endpoint
Okta-STS architecture for OIDC federation:
CVM operates as an STS broker for Okta API access — no existing tool provides this capability. The market treats Okta as an identity source (Okta → AWS/GCP credentials), not a credential target (OIDC → Okta API tokens). CVM fills this gap.
- What CVM vends: Okta OAuth 2.0 access tokens (1h TTL, scoped to specific
okta.*API scopes) - How it works: CVM accepts third-party OIDC tokens, validates against trust policy, then uses pre-provisioned Okta service app client credentials (
private_key_jwt) to mint a scoped access token from Okta's/oauth2/v1/tokenendpoint - Key mechanism: The
scopeparameter at token request time allows CVM to request only a subset of the service app's granted scopes — same pattern as Octo-STS subsetting GitHub App permissions - Bootstrap: Pre-provision a tiered pool of Okta service apps (Read-Only Admin, Group Admin, App Admin, Org Admin) — each tier covers many trust policies
- Scoping: Okta uses a two-layer model:
okta.{resource}.{read|manage}scopes + admin role assignment (the admin role further constrains which resources the scopes apply to)
| Control | CVM Implementation |
|---|---|
| CC6.1 Logical access security | Policy engine controls credential access |
| CC6.2 Credentials and authentication | Short-lived credentials reduce exposure window |
| CC6.3 Authorization to access | ABAC policy evaluation before minting |
| CC6.6 Restricting component access | Scoped credentials with minimum privilege |
| CC7.1 Unauthorized access detection | Audit log captures all operations |
| CC7.2 Monitoring | cvm status, audit log, Prometheus metrics |
| Control | CVM Implementation |
|---|---|
| A.9.2.3 Privileged access management | Policy constrains admin-scoped credentials |
| A.9.4.3 Password management | Credential vending replaces static credential sharing |
| A.12.4.1 Event logging | Tamper-evident HMAC-chained audit log |
| A.12.4.2 Log protection | Hash chain integrity, keychain-protected signing |
| A.18.1.4 Privacy | Audit logs contain no credential values |
This roadmap was developed through First Principles decomposition, multi-perspective Council debate (Security Architect, Rust Systems Engineer, DevOps/SRE, Product Manager), adversarial Red Team analysis, and API capability research.