Private
Public Access
1
0

feat(M11+M12): Email notifications, audit hardening, deployment packaging, backup/DR, integration testing

M11 - Email Notifications + Audit Logging Hardening:
- Email notifier (lettre crate) with templates for patch failure, job completion, maintenance reminders
- Audit log hash chaining (prev_hash + row_hash) for tamper-evident logging
- Periodic + on-demand audit integrity verification
- Audit logging for all config changes and certificate operations
- Frontend: email settings integration, audit integrity verification action

M12 - Deployment Packaging, Backup/DR, Integration Testing:
- scripts/backup.sh: Nightly pg_dump, CA backup (GPG), config backup (secrets excluded unless encrypted)
- scripts/setup.sh: Enhanced with backup dir, seed migration, backup cron, systemd target install
- systemd units: Restart=always, WatchdogSec, ReadWritePaths, security hardening
- systemd/patch-manager.target: Service target for coordinated lifecycle
- docs/runbooks/restore.md: Full DR runbook with RPO 24h / RTO 4h targets
- scripts/integration-test.sh: 9 test suites covering full API lifecycle
- scripts/performance-test.sh: NFR validation (dashboard <5s, CIDR /22 <10s, API <2s)
- docs/security-review.md: Comprehensive security control verification
- docs/compliance-mapping.md: HIPAA (6 sections) + PCI-DSS v4.0 (9 requirements) mapped
This commit is contained in:
2026-04-24 00:45:51 +00:00
parent 84ab92f4f0
commit 297bf1bd83
26 changed files with 2651 additions and 65 deletions

View File

@ -22,6 +22,7 @@ use axum::{
};
use chrono::{DateTime, Utc};
use pm_auth::rbac::AuthUser;
use pm_core::audit::{log_event, AuditAction};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use uuid::Uuid;
@ -129,9 +130,23 @@ fn db_error(e: sqlx::Error) -> (StatusCode, Json<Value>) {
/// Download the root CA certificate as a PEM file.
async fn download_root_ca(
State(state): State<AppState>,
_auth: AuthUser,
auth: AuthUser,
) -> Result<Response<Body>, (StatusCode, Json<Value>)> {
let pem = state.ca.root_cert_pem().to_owned();
log_event(
&state.db,
AuditAction::CertificateDownloaded,
Some(auth.user_id),
Some(&auth.username),
Some("certificate"),
Some("root_ca"),
json!({ "operation": "download_root_ca" }),
None,
None,
)
.await;
pem_response(pem, "ca.crt")
}
@ -230,7 +245,21 @@ async fn download_client_cert(
})?;
match cert_pem {
Some(pem) => pem_response(pem, "client.crt"),
Some(pem) => {
log_event(
&state.db,
AuditAction::CertificateDownloaded,
Some(auth.user_id),
Some(&auth.username),
Some("certificate"),
Some(&host_id.to_string()),
json!({ "operation": "download_client_cert" }),
None,
None,
)
.await;
pem_response(pem, "client.crt")
}
None => Err((
StatusCode::NOT_FOUND,
Json(json!({
@ -268,6 +297,19 @@ async fn issue_client_cert(
)
})?;
log_event(
&state.db,
AuditAction::CertificateIssued,
Some(auth.user_id),
Some(&auth.username),
Some("certificate"),
Some(&host_id.to_string()),
json!({ "hostname": req.hostname, "serial_number": issued.serial_number }),
None,
None,
)
.await;
Ok(Json(json!({
"cert_pem": issued.cert_pem,
"key_pem": issued.key_pem,
@ -306,6 +348,19 @@ async fn renew_cert(
}
})?;
log_event(
&state.db,
AuditAction::CertificateRenewed,
Some(auth.user_id),
Some(&auth.username),
Some("certificate"),
Some(&cert_id.to_string()),
json!({ "serial_number": issued.serial_number }),
None,
None,
)
.await;
Ok(Json(json!({
"cert_pem": issued.cert_pem,
"key_pem": issued.key_pem,
@ -345,5 +400,19 @@ async fn revoke_cert(
})?;
tracing::info!(%cert_id, "Certificate revoked via API");
log_event(
&state.db,
AuditAction::CertificateRevoked,
Some(auth.user_id),
Some(&auth.username),
Some("certificate"),
Some(&cert_id.to_string()),
json!({ "operation": "revoke" }),
None,
None,
)
.await;
Ok(Json(json!({ "revoked": true })))
}