Private
Public Access
1
0

fix(security): harden enrollment PKI bundle retrieval (#12)

- Add single-retrieval semantics: approved PKI bundles are atomically
  removed from the in-memory cache on first retrieval via DashMap::remove(),
  preventing concurrent requests from obtaining the private key
- Add TTL expiry: ApprovedEntry wraps PkiBundle with approved_at and ttl
  fields; bundles expire after ENROLLMENT_BUNDLE_TTL_SECS (600s / 10 min)
- Replace brute-force clear() purge with TTL-based retain() in background
  task, running every 60s instead of every 600s
- Audit tracing calls: confirm no raw polling token is logged; add security
  comment documenting this policy
- Document CSR-based enrollment as future enhancement in both enrollment.rs
  and SECURITY.md, explaining why server-generated keys are used currently
This commit is contained in:
Draco-Lunaris-Echo
2026-06-02 15:16:44 -05:00
committed by GitHub
parent 59df98504c
commit 8873b2c70c
4 changed files with 112 additions and 16 deletions

38
crates/pm-core/src/models.rs Executable file → Normal file
View File

@ -180,6 +180,44 @@ pub struct PkiBundle {
pub server_key: String,
}
/// Time-to-live for approved enrollment PKI bundles (10 minutes).
///
/// After approval, the agent has this duration to retrieve its PKI bundle
/// via the polling endpoint. Once retrieved (single-use) or expired,
/// the bundle is permanently removed from the in-memory cache.
///
/// This TTL balances security (limiting private key exposure in memory)
/// against reliability (giving agents enough time to poll after approval).
pub const ENROLLMENT_BUNDLE_TTL_SECS: u32 = 600; // 10 minutes
/// An approved enrollment PKI bundle awaiting single-use retrieval.
///
/// Stored in the in-memory cache between admin approval and agent pickup.
/// The entry is removed atomically on first retrieval and expires after
/// the configured TTL, whichever comes first.
#[derive(Debug, Clone)]
pub struct ApprovedEntry {
pub pki: PkiBundle,
pub approved_at: chrono::DateTime<Utc>,
pub ttl: chrono::Duration,
}
impl ApprovedEntry {
/// Create a new entry with the current timestamp and default TTL.
pub fn new(pki: PkiBundle) -> Self {
Self {
pki,
approved_at: Utc::now(),
ttl: chrono::Duration::seconds(ENROLLMENT_BUNDLE_TTL_SECS as i64),
}
}
/// Returns true if this entry has exceeded its TTL.
pub fn is_expired(&self) -> bool {
Utc::now() > self.approved_at + self.ttl
}
}
// ============================================================
// Health Checks
// ============================================================