Skip to main content

greentic_deployer/credentials/
mod.rs

1//! C1: credentials contract for deployer env-packs.
2//!
3//! Every deployer env-pack ships a [`DeployerCredentials`] implementation
4//! that declares what capabilities its credentials must satisfy and how to
5//! probe them. Phase A's CLI surface (`gtc op credentials …`) drives this
6//! contract through the env-pack registry — `requirements` validates against
7//! the bound deployer, `bootstrap` runs the deployer's bootstrap path.
8//!
9//! Admin credentials are never intentionally persisted. The
10//! [`ZeroizedAdmin`] wrapper zeroizes its in-process buffer on drop where
11//! the language/runtime allows it. The contract is honest about what it
12//! cannot guarantee: process-wide memory erasure is impossible (the OS may
13//! have paged the buffer, the cloud SDK may hold its own copy, ambient
14//! profile chains live outside our control). Callers should run on
15//! short-lived processes when this matters.
16//!
17//! ## Phase A constraint
18//!
19//! Env-pack handlers are metadata-only in Phase A (see
20//! [`env_packs::slot`](crate::env_packs::slot)) — there is no wired secrets
21//! backend yet. Probes that need credential material (reading a key from
22//! AWS-SM, calling AWS STS) cannot run today; impls report
23//! [`CapabilityStatus::Skipped`] for those entries instead of panicking.
24//! Local-process credentials work today because they probe only the local
25//! environment (filesystem writability, port availability) and need no
26//! credential material at all (C2).
27
28pub mod bootstrap;
29pub mod rotate;
30pub mod rules_export;
31pub mod validate;
32
33pub use bootstrap::{
34    BootstrapError, BootstrapInput, BootstrapOutcome, BoundSecretSink, RunBootstrapError,
35    ZeroizedAdmin, run_bootstrap,
36};
37pub use rotate::{RotateOutcome, RunRotateError, run_rotate};
38pub use rules_export::{RulesExportError, RulesPack, RulesPackEntry, write_rules_pack};
39pub use validate::{
40    Capability, CapabilityCheck, CapabilityStatus, RequirementsReport, ValidateError,
41    ValidationContext, validate_requirements,
42};
43
44use chrono::{DateTime, Utc};
45
46/// Contract a deployer env-pack handler implements to surface its
47/// credentials story to the `gtc op credentials` CLI.
48///
49/// Object-safe so the env-pack registry can return `&dyn`. Implementations
50/// must be `Send + Sync` because the registry is shared across the
51/// operator's request handlers.
52pub trait DeployerCredentials: std::fmt::Debug + Send + Sync {
53    /// Whether this deployer requires real credential material at all.
54    ///
55    /// Deployers that run purely locally (e.g. local-process — no IAM
56    /// roles, no cluster RBAC, no cloud credentials) return `false`.
57    /// When `false`:
58    /// - `validate_requirements` skips the `NoCredentialsRef` rejection
59    ///   for envs that have no `credentials_ref`.
60    /// - `bootstrap` should return [`BootstrapError::NotApplicable`].
61    ///
62    /// Default is `true`, preserving Phase D AWS/K8s/GCP/Azure behavior.
63    fn requires_credentials_material(&self) -> bool {
64        true
65    }
66
67    /// The set of capabilities the deployer's credentials must satisfy.
68    /// Order is stable — the CLI renders this as the column order in
69    /// `gtc op credentials requirements` output. Used both as the
70    /// declaration of what *would* be checked (`--schema`-like surface) and
71    /// as the iteration order for [`validate`](Self::validate).
72    fn required_capabilities(&self) -> Vec<Capability>;
73
74    /// Probe the env's local state against [`required_capabilities`]. The
75    /// validator MUST NOT mutate `ctx` and MUST NOT panic on probe failure;
76    /// it returns a structured [`Failed`](CapabilityStatus::Fail) or
77    /// [`Skipped`](CapabilityStatus::Skipped) entry instead.
78    fn validate(&self, ctx: &ValidationContext<'_>) -> RequirementsReport;
79
80    /// Run the deployer's bootstrap path against ephemeral admin
81    /// credentials.
82    ///
83    /// Implementations with no admin escalation (e.g. the local-process
84    /// deployer — there are no IAM roles or cluster RBAC to provision
85    /// locally) MUST return [`BootstrapError::NotApplicable`] with a
86    /// message telling the user to run `requirements` instead. Returning
87    /// `Ok` with an empty outcome would be dishonest (no admin was
88    /// actually consumed) and would leave a sentinel `credentials_ref`
89    /// pointing at nothing.
90    fn bootstrap(&self, input: &BootstrapInput<'_>) -> Result<BootstrapOutcome, BootstrapError>;
91
92    /// Best-effort compensating cleanup for a bootstrap that wrote durable
93    /// credential material to a REMOTE backend (e.g. the K8s `--bind` path
94    /// writes the minted bearer into an in-cluster Secret) but then failed a
95    /// later persistence step. Without this a failed bootstrap could leave a
96    /// live bearer in the backend while the env stays unbound. The CLI calls it
97    /// on the bootstrap error path; the delete is idempotent (a never-written
98    /// Secret 404s harmlessly), so an unconditional call is safe.
99    ///
100    /// Default is a no-op — deployers that bind no remote material (the
101    /// local-process / render-only paths) have nothing to undo. Implementations
102    /// MUST NOT panic; cleanup failures are swallowed (the caller already has a
103    /// bootstrap error to report). It does NOT cover a hard process crash
104    /// between the remote write and the local commit — short-lived bound tokens
105    /// + rotation are the systemic mitigation for that residual window.
106    fn rollback_bound_material(&self, _env_id: &greentic_deploy_spec::EnvId) {}
107
108    /// The absolute time the given bound credential *material* should be
109    /// rotated, derived from the material's own self-reported lifetime (e.g.
110    /// the K8s projected-ServiceAccount-token JWT's `iat`/`exp` claims).
111    /// Returns `None` when the lifetime can't be determined.
112    ///
113    /// Default is `None`: deployers that mint no time-bounded material (the
114    /// render-only / local paths, and AWS until its STS producer lands) have
115    /// no rotation point to compute. Backends that mint bounded credentials
116    /// override this to decode their own material format — the rotation
117    /// *policy* (rotate at 80% of lifetime) stays shared in
118    /// [`rotate::rotate_at_from_window`], only the decode varies.
119    fn rotate_at(&self, _material: &str) -> Option<DateTime<Utc>> {
120        None
121    }
122
123    /// Whether the given bound material is at/past its rotation point.
124    ///
125    /// Fails OPEN: material whose lifetime can't be decoded
126    /// ([`rotate_at`](Self::rotate_at) returns `None`) is treated as due, so
127    /// `op credentials rotate --if-needed` errs toward rotating rather than
128    /// letting an opaque token silently lapse. The policy lives here; only the
129    /// per-backend `rotate_at` decode is overridden, so impls should not need
130    /// to override this.
131    fn rotation_due(&self, material: &str, now: DateTime<Utc>) -> bool {
132        match self.rotate_at(material) {
133            Some(rotate_at) => now >= rotate_at,
134            None => true,
135        }
136    }
137}