mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 04:08:52 +00:00
docs: convert remaining ASCII diagrams to mermaid (audit closure)
Audit pass over docs/ found 4 files with non-mermaid (ASCII
box-drawing) diagrams in fenced code blocks. The other 9 doc files
already used mermaid blocks (architecture.md, demo-advanced.md,
ci-pipeline.md, concepts.md, est.md, legacy-est-scep.md, mcp.md,
qa-test-guide.md, scep-intune.md). Rendering parity for everything
in docs/.
Conversions:
approval-workflow.md
1 ASCII swimlane → sequenceDiagram with named participants
(Operator A / CertificateService / Job+ApprovalRequest /
Operator B / ApprovalService / Scheduler). Same content: the
same-actor RBAC reject path, the AwaitingApproval gate, the
audit + Prometheus side effects.
intermediate-ca-hierarchy.md
1 lifecycle ASCII → stateDiagram-v2 (created → active → retiring
→ retired with the drain-first refusal annotation).
3 ASCII tree patterns → 3 flowchart TD diagrams (FedRAMP 4-level
boundary CA, financial-services 3-level policy CA, internal-PKI
2-level). Same depth, same path_len + permitted-DNS labels.
runbook-cloud-targets.md
1 dual-column ASCII flow → flowchart TD with two subgraphs
(AWS ACM path, Azure Key Vault path) joining at the audit +
Prometheus exposer node. Same 6-step deploy sequence on each
side with the rollback-on-mismatch step explicit.
runbook-expiry-alerts.md
1 nested-loop ASCII flow → flowchart TD with three nested
subgraphs (per-cert main loop / per-threshold inner / per-channel
fault-isolating dispatch). Same dedup + Prometheus + audit-row
side effects per channel.
Verified locally:
Audit re-run: every fenced block in docs/*.md that does NOT open
with ```mermaid contains zero ASCII box-drawing characters
(┌ └ │ ─ ━ ═ ║ ╔ ╚ ▼ ▲).
Mermaid block tally: 39 across 13 files (up from 32 across 9
files pre-audit). The +7 new blocks are the 4 conversions plus
the lifecycle + 3 tree patterns expanded out of the single
intermediate-ca-hierarchy.md ASCII section.
No code or test changes. Doc-only commit.
This commit is contained in:
+19
-36
@@ -6,42 +6,25 @@ Closes the procurement-checklist question "How do you enforce two-person integri
|
|||||||
|
|
||||||
## End-to-end flow
|
## End-to-end flow
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Operator A (or scheduler) Operator B
|
sequenceDiagram
|
||||||
│ │
|
autonumber
|
||||||
▼ │
|
participant A as Operator A<br/>(or scheduler)
|
||||||
POST /api/v1/certificates/ │
|
participant SVC as CertificateService<br/>.TriggerRenewal
|
||||||
{id}/renew │
|
participant JOB as Job + ApprovalRequest
|
||||||
(or renewal-loop tick) │
|
participant B as Operator B
|
||||||
│ │
|
participant APR as ApprovalService.Approve
|
||||||
▼ │
|
participant SCH as Scheduler
|
||||||
CertificateService.TriggerRenewal │
|
|
||||||
├── reads profile.RequiresApproval │
|
A->>SVC: POST /api/v1/certificates/{id}/renew<br/>(or renewal-loop tick)
|
||||||
├── creates Job at │
|
SVC->>JOB: read profile.RequiresApproval;<br/>create Job @ JobStatusAwaitingApproval;<br/>create ApprovalRequest<br/>(state=pending, requested_by=Operator A)
|
||||||
│ JobStatusAwaitingApproval │
|
Note over JOB,SCH: Scheduler skips —<br/>AwaitingApproval is NOT a dispatchable status
|
||||||
└── creates parallel │
|
B->>JOB: GET /api/v1/approvals?state=pending
|
||||||
ApprovalRequest row │
|
B->>APR: POST /api/v1/approvals/{id}/approve<br/>(decided_by=Operator B, note=...)
|
||||||
(state=pending, │
|
APR->>APR: RBAC: reject if Operator B == Operator A<br/>→ ErrApproveBySameActor (HTTP 403)
|
||||||
requested_by=Operator A) │
|
APR->>JOB: ApprovalRequest → state=approved;<br/>Job AwaitingApproval → Pending;<br/>audit row (action=approval_approved,<br/>actor=Operator B);<br/>certctl_approval_decisions_total<br/>{outcome=approved,profile_id=...}++
|
||||||
│ │
|
SCH->>JOB: pick up Pending → dispatch to issuer connector
|
||||||
│ scheduler skips — │
|
JOB-->>A: cert issues normally
|
||||||
│ AwaitingApproval is │
|
|
||||||
│ NOT a dispatchable status │
|
|
||||||
│ │
|
|
||||||
│ GET /api/v1/approvals?state=pending
|
|
||||||
│ ▼
|
|
||||||
│ POST /api/v1/approvals/{id}/approve
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
ApprovalService.Approve(decided_by=Operator B, note=...)
|
|
||||||
├── RBAC: rejects if Operator B == Operator A → ErrApproveBySameActor (HTTP 403)
|
|
||||||
├── transitions ApprovalRequest to state=approved
|
|
||||||
├── transitions Job from AwaitingApproval → Pending
|
|
||||||
├── records audit row (action=approval_approved, actor=Operator B)
|
|
||||||
└── increments certctl_approval_decisions_total{outcome=approved,profile_id=...}
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Scheduler picks up Job at Pending, dispatches to issuer connector — cert issues normally.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|||||||
@@ -43,19 +43,13 @@ reference can leak.
|
|||||||
|
|
||||||
## Lifecycle states
|
## Lifecycle states
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
created (CreateRoot or CreateChild)
|
stateDiagram-v2
|
||||||
│
|
[*] --> created : CreateRoot / CreateChild
|
||||||
▼
|
created --> active : registration completes
|
||||||
active (issuing certs)
|
active --> retiring : Retire(confirm=false) —<br/>drain start; this CA stops issuing<br/>NEW children but existing children continue
|
||||||
│
|
retiring --> retired : Retire(confirm=true) —<br/>terminal; refused if active children remain<br/>(ErrCAStillHasActiveChildren → HTTP 409)
|
||||||
▼
|
retired --> [*] : no issuance;<br/>OCSP keeps responding for<br/>already-issued leaves until expiry
|
||||||
retiring (drain — children still active; this CA stops issuing
|
|
||||||
NEW children but existing children continue)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
retired (terminal — no issuance, OCSP responder keeps responding
|
|
||||||
for already-issued leaves until expiry)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Drain-first semantics: a CA in `retiring` state cannot terminalize to
|
Drain-first semantics: a CA in `retiring` state cannot terminalize to
|
||||||
@@ -67,11 +61,13 @@ the children first.
|
|||||||
|
|
||||||
### Pattern A — 4-level FedRAMP boundary CA
|
### Pattern A — 4-level FedRAMP boundary CA
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Acme Root CA (path_len=3, offline air-gapped)
|
flowchart TD
|
||||||
└── Acme Policy CA (path_len=2, FedRAMP-Moderate boundary)
|
Root["Acme Root CA<br/>path_len=3<br/>offline air-gapped"]
|
||||||
└── Acme Issuing A (path_len=0, prod workload leaves)
|
Policy["Acme Policy CA<br/>path_len=2<br/>FedRAMP-Moderate boundary"]
|
||||||
└── Acme Issuing B (path_len=0, ephemeral pod identity)
|
IssA["Acme Issuing A<br/>path_len=0<br/>prod workload leaves"]
|
||||||
|
IssB["Acme Issuing B<br/>path_len=0<br/>ephemeral pod identity"]
|
||||||
|
Root --> Policy --> IssA --> IssB
|
||||||
```
|
```
|
||||||
|
|
||||||
Operator workflow:
|
Operator workflow:
|
||||||
@@ -98,10 +94,12 @@ Operator workflow:
|
|||||||
|
|
||||||
### Pattern B — 3-level financial-services policy CA
|
### Pattern B — 3-level financial-services policy CA
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
FinCo Root CA (path_len=2)
|
flowchart TD
|
||||||
└── FinCo Trading Policy CA (path_len=1; permitted DNS = trading.finco.example)
|
Root["FinCo Root CA<br/>path_len=2"]
|
||||||
└── FinCo Trading Issuing CA (path_len=0)
|
Pol["FinCo Trading Policy CA<br/>path_len=1<br/>permitted DNS = trading.finco.example"]
|
||||||
|
Iss["FinCo Trading Issuing CA<br/>path_len=0"]
|
||||||
|
Root --> Pol --> Iss
|
||||||
```
|
```
|
||||||
|
|
||||||
Per business-unit name constraints: each policy CA carries a
|
Per business-unit name constraints: each policy CA carries a
|
||||||
@@ -113,9 +111,11 @@ excluded subtree. Operators submit `name_constraints` on the
|
|||||||
|
|
||||||
### Pattern C — 2-level internal PKI
|
### Pattern C — 2-level internal PKI
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Internal Root CA (path_len=0)
|
flowchart TD
|
||||||
└── Internal Issuing CA (path_len=0; issues leaves directly)
|
Root["Internal Root CA<br/>path_len=0"]
|
||||||
|
Iss["Internal Issuing CA<br/>path_len=0<br/>issues leaves directly"]
|
||||||
|
Root --> Iss
|
||||||
```
|
```
|
||||||
|
|
||||||
The simplest tree-mode deployment. Roughly equivalent to single mode
|
The simplest tree-mode deployment. Roughly equivalent to single mode
|
||||||
|
|||||||
@@ -15,42 +15,39 @@ install certctl.
|
|||||||
|
|
||||||
## End-to-end flow (cloud targets)
|
## End-to-end flow (cloud targets)
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
cert renewed → renewal job created
|
flowchart TD
|
||||||
│
|
Renew["cert renewed → renewal job created"]
|
||||||
▼
|
Pick["agent picks up DeployCertificate work item"]
|
||||||
agent picks up DeployCertificate work item
|
Dispatch["target.Connector.DeployCertificate(ctx, request)"]
|
||||||
│
|
|
||||||
▼
|
Renew --> Pick --> Dispatch
|
||||||
target.Connector.DeployCertificate(ctx, request)
|
Dispatch --> AWS
|
||||||
│
|
Dispatch --> AZ
|
||||||
┌──────────────────┴──────────────────┐
|
|
||||||
│ │
|
subgraph AWS["AWS ACM path"]
|
||||||
▼ ▼
|
A1["1. rotate-in-place only:<br/>DescribeCertificate(arn)"]
|
||||||
AWS ACM path Azure Key Vault path
|
A2["2. GetCertificate(arn) —<br/>capture snapshot bytes for rollback"]
|
||||||
│ │
|
A3["3. ImportCertificate(arn, new_bytes) —<br/>fresh ARN OR rotate-in-place"]
|
||||||
▼ ▼
|
A4["4. AddTagsToCertificate(arn, provenance) —<br/>ACM strips on re-import; we re-apply"]
|
||||||
1. (rotate-in-place only) 1. GetCertificate(name, "" /* latest */)
|
A5["5. DescribeCertificate(arn) —<br/>verify serial matches expected"]
|
||||||
DescribeCertificate(arn) — capture snapshot CER bytes
|
A6["6. ON MISMATCH: rollback<br/>ImportCertificate(arn, snapshot_bytes)"]
|
||||||
2. GetCertificate(arn) — capture 2. Build PFX from cert+chain+key
|
A1 --> A2 --> A3 --> A4 --> A5 --> A6
|
||||||
snapshot bytes for rollback (PKCS#12 via go-pkcs12)
|
end
|
||||||
3. ImportCertificate(arn, new_bytes) 3. ImportCertificate(name, PFX, tags)
|
|
||||||
— fresh ARN OR rotate-in-place — ALWAYS creates a new version
|
subgraph AZ["Azure Key Vault path"]
|
||||||
4. AddTagsToCertificate(arn, 4. (Tags carried forward
|
Z1["1. GetCertificate(name, '' = latest) —<br/>capture snapshot CER bytes"]
|
||||||
provenance) — ACM strips on automatically)
|
Z2["2. Build PFX from cert+chain+key<br/>(PKCS#12 via go-pkcs12)"]
|
||||||
re-import; we re-apply
|
Z3["3. ImportCertificate(name, PFX, tags) —<br/>ALWAYS creates a new version"]
|
||||||
5. DescribeCertificate(arn) — verify 5. GetCertificate(name, "" /* latest */)
|
Z4["4. Tags carried forward automatically"]
|
||||||
serial matches expected — verify serial matches expected
|
Z5["5. GetCertificate(name, '' = latest) —<br/>verify serial matches expected"]
|
||||||
6. ON MISMATCH: rollback ←──── (same shape) ────→ 6. ON MISMATCH: rollback
|
Z6["6. ON MISMATCH: rollback<br/>ImportCertificate(name, snapshot_PFX) —<br/>new version"]
|
||||||
ImportCertificate(arn, ImportCertificate(name,
|
Z1 --> Z2 --> Z3 --> Z4 --> Z5 --> Z6
|
||||||
snapshot_bytes) snapshot_PFX) — new version
|
end
|
||||||
│
|
|
||||||
▼
|
A6 --> Audit
|
||||||
7. Audit row + Prometheus counter
|
Z6 --> Audit
|
||||||
certctl_deploy_attempts_total{target_type="AWSACM"|"AzureKeyVault",
|
Audit["7. Audit row + Prometheus counters<br/>certctl_deploy_attempts_total{target_type, result}<br/>certctl_deploy_rollback_total{target_type, outcome}"]
|
||||||
result="success"|"failure"}
|
|
||||||
certctl_deploy_rollback_total{target_type=...,
|
|
||||||
outcome="restored"|"also_failed"}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -14,36 +14,37 @@ walkthrough of how to install certctl — that lives in the README.
|
|||||||
|
|
||||||
## End-to-end flow
|
## End-to-end flow
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
daily ticker (renewalCheckLoop)
|
flowchart TD
|
||||||
│
|
Tick["daily ticker (renewalCheckLoop)"]
|
||||||
▼
|
Check["RenewalService.CheckExpiringCertificates"]
|
||||||
RenewalService.CheckExpiringCertificates
|
|
||||||
│
|
Tick --> Check --> Loop
|
||||||
┌────────────────┴────────────────┐
|
|
||||||
│ for cert in expiring (≤30 days):│
|
subgraph Loop["for cert in expiring (≤30 days)"]
|
||||||
│ 1. Resolve RenewalPolicy │
|
L1["1. Resolve RenewalPolicy"]
|
||||||
│ 2. Compute daysUntil │
|
L2["2. Compute daysUntil"]
|
||||||
│ 3. updateCertExpiryStatus │
|
L3["3. updateCertExpiryStatus"]
|
||||||
│ 4. sendThresholdAlerts ──────►│ per threshold:
|
L4["4. sendThresholdAlerts"]
|
||||||
│ 5. Create renewal job (if │ a. resolve severity tier
|
L5["5. Create renewal job<br/>(if issuer registered +<br/>ARI allows)"]
|
||||||
│ issuer registered + ARI │ via AlertSeverityMap
|
L1 --> L2 --> L3 --> L4 --> L5
|
||||||
│ allows) │ b. resolve channel set
|
end
|
||||||
└──────────────────────────────────┘ via AlertChannels[tier]
|
|
||||||
c. for each channel:
|
L4 --> Threshold
|
||||||
i. dedup via
|
|
||||||
notification_events
|
subgraph Threshold["per threshold"]
|
||||||
(cert,threshold,channel)
|
T1["a. resolve severity tier<br/>via AlertSeverityMap"]
|
||||||
ii. SendThresholdAlertOnChannel
|
T2["b. resolve channel set<br/>via AlertChannels[tier]"]
|
||||||
→ notifierRegistry[channel]
|
T1 --> T2 --> Channel
|
||||||
→ Send(recipient,subj,body)
|
end
|
||||||
iii. record audit row
|
|
||||||
(event_type=expiration_alert_sent,
|
subgraph Channel["for each channel (fault-isolating)"]
|
||||||
metadata.channel,
|
C1["i. dedup via notification_events<br/>(cert, threshold, channel)"]
|
||||||
metadata.severity_tier)
|
C2["ii. SendThresholdAlertOnChannel<br/>→ notifierRegistry[channel]<br/>→ Send(recipient, subj, body)"]
|
||||||
iv. bump Prometheus counter
|
C3["iii. record audit row<br/>event_type=expiration_alert_sent<br/>metadata.channel, metadata.severity_tier"]
|
||||||
certctl_expiry_alerts_total
|
C4["iv. bump Prometheus counter<br/>certctl_expiry_alerts_total<br/>{channel, threshold, result}"]
|
||||||
{channel,threshold,result}
|
C1 --> C2 --> C3 --> C4
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The dispatch loop's per-channel error handling is
|
The dispatch loop's per-channel error handling is
|
||||||
|
|||||||
Reference in New Issue
Block a user