Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 6969@bnt-lap001 Assistant-Session: e8ae73f2-8385-4c9c-b714-94c3c52cdbc0
172 lines
7.5 KiB
YAML
172 lines
7.5 KiB
YAML
spec: runbook-pack/v0.2
|
|
id: nk.ssh-certificates
|
|
title: Short-lived SSH credentials for admins, agents and automations
|
|
owner: net-kingdom
|
|
outcome: An actor obtains a short-lived CA-signed SSH certificate and uses it through an ops-bridge tunnel, with no static key doing the work.
|
|
engine: native
|
|
framing:
|
|
- Certificate files must be mode 600 and are never reused across reconnects.
|
|
- A non-zero cert_command exit is a failure and must trigger backoff.
|
|
- ops-warden never vends API keys or passwords; route them with warden route find.
|
|
actors:
|
|
- id: operator
|
|
role: Operator
|
|
description: Signs and inspects the certificate and brings the tunnel up, using their own identity.
|
|
privileges: [sign-certificate, open-tunnel]
|
|
datapoints:
|
|
- id: actor_name
|
|
label: Actor name
|
|
type: string
|
|
default: agt-rehearsal
|
|
pattern: "(adm|agt|atm)-[a-z0-9-]+"
|
|
help: "Actor type prefix decides the maximum TTL: adm 48h, agt 24h, atm 8h. agt-rehearsal is an inert test actor (2h, a principal no host trusts); use your own actor for real access."
|
|
dimensions: {actor: operator, kind: identity, subsystem: ops-warden}
|
|
ask_in: ready
|
|
- id: warden_config
|
|
label: warden config file
|
|
type: path
|
|
default: ~/.config/warden/rehearsal/warden.yaml
|
|
help: The rehearsal config uses a throwaway local CA that nothing trusts and has no flex-auth gate, so it proves the signing mechanics, not the policy gate. For real access use your production config, ~/.config/warden/warden.yaml.
|
|
dimensions: {actor: operator, kind: location, subsystem: ops-warden}
|
|
ask_in: ready
|
|
- id: pubkey
|
|
label: Public key path
|
|
type: path
|
|
default: ~/.ssh/agt-rehearsal_ed25519.pub
|
|
dimensions: {actor: operator, kind: location, subsystem: ops-warden}
|
|
ask_in: ready
|
|
- id: tunnel
|
|
label: Tunnel name
|
|
type: string
|
|
default: k3s-api-railiance01
|
|
pattern: "[a-z0-9-]+"
|
|
dimensions: {kind: target, subsystem: ops-bridge}
|
|
ask_in: ready
|
|
- id: test_target
|
|
label: Is there a target that trusts your actor's principal?
|
|
type: enum
|
|
values: [available, none-yet]
|
|
default: none-yet
|
|
help: With the inert rehearsal actor no host trusts the certificate, so the tunnel cannot be proven. Choose available only with a real actor and a tunnel that uses it.
|
|
dimensions: {kind: state, subsystem: ops-bridge}
|
|
ask_in: connect
|
|
stages:
|
|
- id: ready
|
|
title: Before you start
|
|
goal: You know what is in place and which certificates exist already.
|
|
steps:
|
|
- id: prerequisites
|
|
title: Confirm the prerequisites
|
|
actor: operator
|
|
owner: workstation
|
|
description: "In place before you start: the warden CLI is installed and your actor is in the principals inventory (ops-warden); the bridge CLI and a tunnel definition exist (ops-bridge); the target hosts trust the SSH CA and carry the actor principal (railiance-infra)."
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: All three prerequisites hold
|
|
expect: manual
|
|
- id: status-before
|
|
title: Look at current certificates
|
|
actor: operator
|
|
owner: ops-warden
|
|
requires: [prerequisites]
|
|
uses: [warden_config]
|
|
command: WARDEN_CONFIG={{warden_config}} warden status
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: You know which certificates are current and which are expired
|
|
expect: manual
|
|
- id: get-certificate
|
|
title: Get a certificate
|
|
goal: A short-lived certificate for the actor exists and its lifetime is within the limit for the actor type.
|
|
requires: [ready]
|
|
steps:
|
|
- id: sign
|
|
title: Sign a public key for the actor
|
|
actor: operator
|
|
owner: ops-warden
|
|
uses: [actor_name, pubkey, warden_config]
|
|
command: WARDEN_CONFIG={{warden_config}} warden sign {{actor_name}} --pubkey {{pubkey}} > /tmp/{{actor_name}}-cert.pub
|
|
idempotent: idempotent
|
|
idempotent_note: Each run issues a new certificate and overwrites the file; the old one expires on its own.
|
|
rollback: Delete /tmp/{{actor_name}}-cert.pub; the certificate expires on its own.
|
|
evidence: [certificate_valid_before]
|
|
verify:
|
|
done_when: A certificate file exists and is readable as a certificate
|
|
command: ssh-keygen -L -f /tmp/{{actor_name}}-cert.pub
|
|
expect: exit-0
|
|
- id: inspect-ttl
|
|
title: Check the certificate lifetime
|
|
actor: operator
|
|
owner: ops-warden
|
|
requires: [sign]
|
|
uses: [actor_name]
|
|
command: ssh-keygen -L -f /tmp/{{actor_name}}-cert.pub | grep -E "Key ID|Principals|Valid"
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: "Valid before is within the actor-type TTL (adm 48h, agt 24h, atm 8h)"
|
|
expect: manual
|
|
gates:
|
|
- id: certificate-valid
|
|
title: The certificate is valid and short-lived
|
|
goal: short-lived-credentials
|
|
blocking: true
|
|
downstream_risk: A tunnel opened with a certificate that outlives its limit defeats the point of short-lived credentials.
|
|
checks:
|
|
- done_when: The lifetime is within the limit for the actor type
|
|
expect: manual
|
|
- done_when: The certificate file can be read
|
|
command: ssh-keygen -L -f /tmp/{{actor_name}}-cert.pub
|
|
expect: exit-0
|
|
- id: connect
|
|
title: Use it through a tunnel
|
|
goal: The tunnel that uses the certificate is up and the signing is on record.
|
|
requires: ["gate:certificate-valid"]
|
|
steps:
|
|
- id: tunnel-up
|
|
title: Bring up the tunnel that uses cert_command
|
|
actor: operator
|
|
owner: ops-bridge
|
|
uses: [tunnel, test_target]
|
|
description: A tunnel only proves the certificate if a host trusts its principal. With the inert rehearsal actor none does, so record that no target exists yet; the stage is then not proven.
|
|
idempotent: idempotent
|
|
one_of:
|
|
- id: bring-up
|
|
title: Bring up the tunnel that uses the certificate
|
|
recommended_when: {test_target: [available]}
|
|
command: bridge up {{tunnel}}
|
|
rollback: bridge down {{tunnel}}
|
|
verify:
|
|
done_when: The tunnel shows connected
|
|
command: bridge status
|
|
expect: output-contains
|
|
contains: "{{tunnel}}"
|
|
- id: no-target-yet
|
|
title: No target trusts this actor yet; tunnel not proven
|
|
recommended_when: {test_target: [none-yet]}
|
|
verify:
|
|
done_when: You have recorded that the tunnel stage was not proven
|
|
expect: manual
|
|
- id: audit
|
|
title: Confirm the signing was audited
|
|
actor: operator
|
|
owner: ops-warden
|
|
requires: [tunnel-up]
|
|
uses: [warden_config]
|
|
command: WARDEN_CONFIG={{warden_config}} warden log | tail -5
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: Your signing appears in the history
|
|
expect: manual
|
|
gates:
|
|
- id: tunnel-connected
|
|
title: The tunnel is connected, or its absence is recorded
|
|
goal: private-access
|
|
blocking: true
|
|
downstream_risk: A pass here could be read as proof that private access works.
|
|
checks:
|
|
- done_when: bridge reports the tunnel as connected, or "tunnel not proven" is recorded because no target exists
|
|
expect: manual
|
|
ownership:
|
|
- {concern: Certificate issuance and TTL policy, owner: ops-warden}
|
|
- {concern: Tunnel lifecycle and refresh, owner: ops-bridge}
|
|
- {concern: Host CA trust and principals, owner: railiance-infra}
|