Skip to content

Latest commit

 

History

History
1496 lines (1187 loc) · 65.5 KB

File metadata and controls

1496 lines (1187 loc) · 65.5 KB

CVM — Credential Vending Machine: Comprehensive Project Roadmap

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.


Table of Contents

  1. Architecture Overview
  2. Phase 0: Security Foundation (Before All Features)
  3. Phase 1: Core Infrastructure
  4. v0.1.0: GitHub + Okta Integration
  5. v0.2.0: Expanded Backends & Renewal
  6. v0.3.0+: Future Vision
  7. Security Architecture
  8. Testing Strategy
  9. Appendix: Crate Recommendations
  10. Appendix: API Reality Constraints

1. Architecture Overview

Single Binary, Three Modes

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
Loading

Mode Capabilities

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.

STS Broker Architecture

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.

Critical Architectural Decision: TTL Enforcement

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 gc command provides lazy enforcement for CLI-only users — processes expired credentials from the durable TTL store on demand.

Cargo Workspace Structure

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


2. Phase 0: Security Foundation

This phase MUST be completed before ANY feature development begins.

2.1 First-Party Code Security

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"

2.2 Third-Party Dependency Security

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.rs scripts in the dependency tree via cargo metadatabuild.rs runs arbitrary code at compile time
  • Pin all GitHub Actions to commit SHA, not tags
  • Use cargo-vet to track audit status of every dependency
  • Monitor for transitive dependency changes via cargo-deny

2.3 Docker Container Security

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 static base image (distroless, ~2-3 MB, includes CA certs)
  • Non-root user (USER nonroot — Chainguard images default to this)
  • --cap-drop=ALL at 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

2.4 CI/CD Pipeline Security

# .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 json

Build 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

2.5 Dev/Test Credential Security

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 integration

Pattern: 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/credential

In 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"
        )
    })
}

3. Phase 1: Core Infrastructure

Build the trait boundaries, data models, and core engine before any platform integration.

3.1 Credential State Machine

#[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).

3.2 Lease Model

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,
}

3.3 Provider Trait

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}")]
}

3.4 Secrets Backend Trait

#[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.

3.5 Policy Engine

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

Single-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>;
}

3.6 Audit Log

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)
);

3.7 Inventory Database

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);

3.8 Async Runtime Construction

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.


4. v0.1.0: GitHub + Okta Integration

4.1 API Reality Constraints (Critical)

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)

4.2 Revised v0.1.0 GitHub Scope

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 repositories and permissions parameters 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)

4.3 Revised v0.1.0 Okta Scope

Okta OAuth 2.0 Client Credentials (fully automated)

  • Endpoint: POST /oauth2/v1/token with grant_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.

4.4 v0.1.0 CLI Commands

# 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 credentials

4.5 v0.1.0 Server Mode Endpoints

POST   /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.

4.6 v0.1.0 Feature Checklist

  • 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 keyring crate)
  • 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 init bootstrap flow
  • cvm gc garbage collection
  • cvm emergency-revoke for incident response
  • Docker image with Chainguard base
  • 100% test coverage with 3-tier test pyramid
  • SBOM + binary signing for release artifacts

4.7 v0.1.0 Milestone Dependencies

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
Loading

5. v0.2.0: Expanded Backends & Renewal

5.1 New Secrets Backends

  • 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

5.2 Credential Renewal

  • 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_renewals limit

5.3 Multi-tenancy (Namespace Model)

  • 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

5.4 New Providers: Cloudflare + Datadog

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_on UTC 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_before activation 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-ttl since 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-at ISO 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/token with grant_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_accounts with auth_type: oauth_client_secret
  • Token mint: POST /oauth2/token with grant_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) or DELETE /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/token with grant_type=client_credentials
  • Auth: client_id + client_secret (bootstrap credential, UI-created)
  • TTL: Fixed 1 hour (Tier 1, platform-native)
  • Scope: Per-token via scope parameter (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
}

5.7 Passthrough Providers (v0.2.0+)

These platforms have native OIDC federation — CVM's role is policy management and audit, not token brokering:

  • AWS IAMsts:AssumeRoleWithWebIdentity accepts 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.

5.5 Policy Engine Upgrade

  • Consider Cedar (Rust-native policy language by AWS) for cross-policy composition
  • Policy versioning and rollback
  • Policy dry-run mode: cvm policy test --request '...'

6. v0.3.0+: Future Vision

6.1 Advanced Features

  • 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

6.2 STS Scaling Roadmap

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.

6.3 Platform Expansion Candidates

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

7. Security Architecture

7.1 Threat Model Summary

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

7.2 Trust Model

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
Loading
  • 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)

7.3 Credential Delivery Security

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

7.4 Post-Creation Scope Verification

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 })
}

7.5 Audit Correlation

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_id custom claim

This enables correlating CVM's audit log with the platform's own access logs during incident investigation.


8. Testing Strategy

8.1 Coverage Mandate

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

8.2 Three-Tier Test Pyramid

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

8.3 Test Categories

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_at timestamps
  • 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:

  • SecretBytes zeroes memory on drop (via unsafe test-only memory inspection)
  • Debug and Display impls of credential types never emit secret values
  • ProviderError variants 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 initcvm createcvm revoke
  • TTL enforcement: create credential → wait → verify revoked (short TTL)
  • Credential injection via op run --env-file=.env.test

8.4 Test Credential Isolation

  • 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

9. Appendix: Crate Recommendations

Core Dependencies

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

Testing Dependencies

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

Security Tooling

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

10. Appendix: API Reality Constraints

GitHub

What works for automated credential vending:

  1. 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 + permissions parameters
    • Revoke: DELETE /installation/token
  2. OAuth Tokens via Device Flow — Requires one-time user interaction

    • Device code: POST /login/device/code
    • User visits: github.com/device and 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
  3. 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)

Okta

What works for automated credential vending:

  1. OAuth 2.0 Client Credentials — Fully automated
    • Create: POST /oauth2/v1/token with grant_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

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)

Implications for CVM v0.1.0

  1. CVM's value proposition for GitHub is strongest with GitHub Apps (the recommended GitHub integration pattern anyway)
  2. CVM's value proposition for Okta is strongest with OAuth 2.0 (also Okta's recommended programmatic access pattern)
  3. Both platforms' recommended approaches align perfectly with CVM's automated, short-lived credential model
  4. Classic PATs and SSWS tokens could be tracked/managed by CVM (manual import + lifecycle management) in a future version, but cannot be vended automatically

Cloudflare (v0.2.0)

What works for automated credential vending:

  1. API Tokens — Fully automated, scoped, with platform-native TTL
    • Create: POST /client/v4/accounts/{account_id}/tokens (account-owned) or POST /client/v4/user/tokens (user-owned)
    • Auth: Authorization: Bearer {PARENT_TOKEN} — parent token must have "API Tokens Write" permission
    • TTL: Native via expires_on UTC timestamp (e.g., "2026-04-04T01:15:00Z") — Tier 1
    • Activation: Optional not_before timestamp 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} or DELETE /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

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_on is 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

Datadog (v0.2.0)

What works for automated credential vending:

  1. 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 (requires service_account_write permission)
    • OTR mode: One-Time Read (default for new orgs since August 2025) — key value shown once at creation

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-KEY header is always required in addition to DD-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.

HCP Terraform (v0.2.0)

What works for automated credential vending:

  1. Team Tokens — Fully automated, scoped via team permissions, with native TTL
    • Create: POST /api/v2/teams/{team_id}/authentication-tokens with expired-at field
    • Auth: Bearer token from user/org token with team management permissions
    • TTL: Native via expired-at ISO 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

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}

Atlassian Jira Cloud (v0.2.0)

What works for automated credential vending:

  1. OAuth 2.0 Client Credentials — Fully automated, 1-hour tokens, no user interaction
    • Token endpoint: POST https://api.atlassian.com/oauth/token with grant_type=client_credentials
    • Auth: client_id + client_secret (from app registration at developer.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

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:jira
  • read:project:jira, write:project:jira
  • read:sprint:jira-software, write:sprint:jira-software
  • read:board-scope:jira-software
  • read:audit-log:jira
  • admin:organization

Snyk (v0.2.0)

What works for automated credential vending:

  1. OAuth 2.0 Access Token (via service account client credentials) — Fully automated
    • Service account create: POST /rest/orgs/{orgId}/service_accounts with auth_type: oauth_client_secret and role_id
    • Token mint: POST /oauth2/token with grant_type=client_credentials (form-urlencoded)
    • Auth: client_id + client_secret from 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}/secrets with mode: replace

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_seconds immutable after creation — TTL change requires new service account
  • client_secret shown once at creation (OTR)
  • No OIDC federation for API access
  • Alternative: private_key_jwt auth_type eliminates stored secret (requires hosting JWKS endpoint)

Vanta (v0.3.0+)

What works for automated credential vending:

  1. OAuth 2.0 Access Token (via client credentials) — Fully automated token minting
    • Create: POST https://api.vanta.com/oauth/token with grant_type=client_credentials
    • Auth: client_id + client_secret from pre-provisioned "Manage Vanta" OAuth application
    • TTL: Fixed 1 hour (platform-native, Tier 1)
    • Scope: Per-token via scope parameter — vanta-api.{resource}:{read|write} format
    • Revoke: No explicit endpoint — re-minting a token implicitly revokes the previous one

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 Design (v0.2.0+)

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/token endpoint
  • Key mechanism: The scope parameter 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)

Compliance Alignment

SOC2 Type II Controls Supported by CVM

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

ISO 27001 Annex A Controls

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.