Skip to content

A walkthrough of the operator console

The web console reference lists every page and the endpoints behind it. This page does something different: it walks the console in the order you actually meet it, from an empty deployment to one issuing, revoking and proving things — and shows each step.

That order is the one thing worth knowing before you begin. A PKI is built root-first and torn down leaf-first. Until a certificate authority exists there is nothing to issue under, nothing to revoke, no CRL to publish and no chain to validate; most of the console's pages are empty or refuse to act. So the walk starts at Authorities, not at Certificates — even though Certificates is the tab the console opens on, because that is where you spend the rest of your life.

The screenshots are of a throwaway PKI belonging to an invented company, Northwind Logistics GmbH, under .example names, rebuilt from scratch by scripts/screenshots/capture.sh and destroyed afterwards — so every serial, subject and audit entry in them is one the product generated for the picture, and none of it is anyone's. Every certificate was signed by a key generated inside a SoftHSM token; the CRLs are real CRLs and the audit log is a real hash chain. Why it is done this way.


Before you start

The console is served by the server itself, at /console/, out of the binary — there is no separate front-end to deploy, and nothing extra to turn on:

secsy-pki-server -config config.yaml
# → https://pki.example.internal:8443/console/

/ redirects there, and it is the only UI: the separate Bootstrap SPA that used to be served from disk is gone, along with the question of which of two pages was authoritative.

It holds no privileges of its own. Every button on every page calls the same RBAC-gated, audited REST API that secsy-ca calls, with your credentials attached, so the console can do exactly what you can do and nothing more — and everything it does lands in the audit log under your name. That is also why this walkthrough is useful even if you intend to script everything: the pages map onto commands almost one-to-one — section 13 is the single exception — and the parity matrix names the mapping both ways.


1. Signing in

The sign-in card: the title "Secsy PKI", the line "Operator console — sign in to continue", a Username field pre-filled with "root", a masked Password field, and a Sign in button.

The form tries, in order, the authentication methods the server has configured: a directory (LDAP/AD) bind, then the built-in root password. Both establish a server-side session with a CSRF token, which is what lets the console offer WebAuthn step-up on high-risk operations later. With OIDC configured, a Sign in with SSO button appears below the divider — under a line naming the protection the redirect will use, "OpenID Connect Authorization Code flow with PKCE (S256)", read from the server's own discovery document rather than asserted by the page — and the password form stays for break-glass. With several identity providers configured, that becomes one button per provider — Sign in with Microsoft Entra ID, Sign in with Okta — so an operator picks their own IdP.

The root account in the screenshot is the one to plan to stop using. It is a shared password with full authority over every tenant, it exists so a brand-new deployment can be administered at all, and it is the obvious thing to find in a breach. Once real operators can sign in — through OIDC, LDAP, or client certificates — set policy.allow_root_basic_auth: false and it is gone. The options and the trade-offs are in operator authentication.


2. Build the hierarchy

The Certificate authorities view: a table with Label, Subject, Key, Kind, Status and Not after columns, listing northwind-root as an ACTIVE ecdsa-sha2-nistp384 root expiring 2046-10-26 and three ACTIVE ecdsa-sha2-nistp256 intermediates — the TLS, Device and Workload issuing CAs — each expiring 2031-10-30. Every row ends in Chain and Public key buttons; the three intermediates also carry Rotate key, which the root does not.

Authorities is the hierarchy, and it is the first page that matters. Each row is a CA: its label (which is also its key label on the token), its subject, the key the HSM holds for it, whether it is a root or an intermediate, and how long it has left.

The shape above is the one to copy. One offline-ish root with a long life, several short-lived intermediates underneath it, one per purpose. Splitting by purpose — public TLS, device fleet, platform workloads — is not bureaucracy: an intermediate is the unit you can rotate, constrain and revoke independently, so a compromised device-enrollment key costs you the device fleet rather than the company. The root signs three certificates in its life and otherwise does nothing.

Rotate key starts a dual-chain rollover: the CA gets a new signing key while the old one keeps serving its already-issued certificates through an overlap window, so nothing breaks at the moment of the swap. There is no button that destroys a key in one click.

Public key is on every row, including the root's, and asks which shape you want before it exports: SPKI PEM for X.509 tooling, or the single OpenSSH line an sshd TrustedUserCAKeys file wants. It is the half of the key pair that is meant to be published — the private half has never left the token and has no export button anywhere in this console.

The Create root CA, Issue intermediate CA and External subordinate CA (CSR) forms side by side. The first two are filled in — a label, ecdsa-p384, 7300 days validity and the subject "Northwind Logistics Root CA R2" on the left; a parent CA picker set to northwind-root, label northwind-issuing-tls-g3, ecdsa-p256, 1825 days and path length 0 in the middle. The third explains that it generates an HSM-backed key and a PKCS#10 CSR for an offline or third-party parent to sign.

Below the table are the three ways a CA comes into existence, and which one you want depends on who owns the trust anchor:

Form Use it when
Create root CA this PKI is its own trust anchor — an internal/private PKI
Issue intermediate CA the parent is a CA this deployment already holds
External subordinate CA (CSR) the parent is an offline corporate root or a third party; you get a key and a CSR, they return a certificate (details)

Two fields deserve a decision rather than a default. Key type is the one thing you cannot change later without re-issuing everything beneath it — ecdsa-p384 for a root, ecdsa-p256 for issuers, RSA only when a client in your estate still cannot do better. And path length 0 on an intermediate means this CA may issue leaves and no further CAs, which is the constraint that stops an issuing CA from quietly becoming a parent. Set it.

Whichever form you use, the private key is generated inside the token and is never in the server's memory. There is no form here that accepts one, because there is no code path that would do anything with it. (Adopting a CA that already exists elsewhere is a deliberate, separate operation — importing an existing CA.)

The extensions you only get to choose once

The X.509 extension dialog over the Authorities view, titled "Subject and X.509 extensions — new intermediate CA", with collapsible sections for key usage, extended key usage, subject, alternative names, revocation and discovery pointers, policy/path/key-period constraints, directory attributes and legacy flags, custom extensions, and a request preview. The extended-key-usage section is open: serverAuth is checked among nine named usages, a further-usages box holds the dotted OID 1.3.6.1.4.1.311.20.2.2, and "mark extKeyUsage critical" is ticked. At the foot, the request preview shows the JSON that will be sent, beginning with key_usages certSign and crlSign.

Each of the three forms carries an X.509 extensions… button, and this is where most of the thinking goes. A leaf is short-lived and reissued constantly; a CA's extensions bind every certificate that will ever chain through it, and the only way to change them is a new key and a trust-store rollout. So the whole common extension vocabulary is accepted here, once, before the key exists:

Section What it decides
Key usage what this key may do. keyCertSign cannot be cleared — a CA that cannot sign certificates is a misconfiguration, and the server refuses it
Extended key usage the strongest constraint available on a delegated authority. A validator intersects EKUs down the chain, so an issuer carrying only serverAuth cannot mint a code-signing certificate even if a profile here allows one
Subject the rest of the DN — registry identifiers, a postal address, domain components, and any attribute at all by name or OID
Alternative names subjectAltName / issuerAltName, every GeneralName type including UPNs and otherName
Revocation and discovery pointers where the parent publishes this certificate's CRL and OCSP, where the parent's own certificate can be fetched, and what this CA itself offers (its repository, its TSA)
Policy, path and key-period inhibitAnyPolicy, and the private-key usage period that retires the key before the certificate expires
Legacy and flags must-staple, ocsp-nocheck, the two Netscape extensions, an explicit subject key identifier
Custom extensions any OID at all, in eleven value encodings — including raw DER for a syntax nothing here models

The dialog's last section is a request preview: the exact JSON body the form will send. Read it before you press Create — and note that the server compiles and validates the same set before generating a key, so a rejected setting costs nothing but the error message. Name constraints and certificate policies are not in this dialog; they have their own editor on the Restrictions view (section 18). The full reference is the CA certificate extension set.


3. Prove where the keys are

The HSM key inventory panel: "Provider pkcs11 · 8 key(s) · none extractable · 3 not bound to a CA (KEK/TSA/signing keys)", then a table of eight keys — the root, three issuing CAs, the SSH CA, the KEK, the TSA key and the code-signing key — each showing its key type, a green NO in the Extractable column, "yes" under Sensitive, and the CA or role it belongs to.

This panel, at the bottom of the same page, is the product's central claim made checkable. For every key on the provider it reports the two PKCS#11 attributes that decide whether "HSM-backed" means anything:

  • CKA_EXTRACTABLE = false — the token will not export this key, wrapped or otherwise. The column reads NO, and that is the answer you want.
  • CKA_SENSITIVE = true — the token will not reveal the key's value to an application, even to the one that created it.

A key showing yes under Extractable is a key that could leave, and the summary line counts them so a single bad one cannot hide in a long list. The not bound to a CA count is not a warning: the KEK, the timestamping key and the code-signing key are legitimately not CA keys.

This is an inventory, not a proof. It reports what this token says about itself, which is exactly as trustworthy as the token. On a YubiHSM you can go further and get a certificate, signed by a key Yubico put in the device at manufacture, attesting that a named key was generated on-device and cannot be exported — see key attestation. On SoftHSM, as here, there is no such hardware claim to make, and that is the honest difference between a development token and an HSM.


4. Issue a certificate

The Issue view: a CA picker set to northwind-issuing-tls, a Profile picker reading "server — TLS server certificate (serverAuth)", a Validity field defaulting to the profile default, a profile policy line reading "default validity 397d · max 397d · key usage: digitalSignature, keyEncipherment · EKU: serverAuth", a textarea holding a PEM certificate signing request, and Issue certificate / Preview (dry run) buttons.

Issuance takes a CSR, a CA and a profile — and the profile is the part worth understanding, because it is where policy lives rather than in the request.

A profile fixes the key usages, the extended key usages, the maximum validity and which per-request overrides are even offered. The grey profile policy line under the pickers is that policy, restated for the profile you just chose, before you submit anything: 397 days maximum, digitalSignature and keyEncipherment, serverAuth. A request asking for more is refused; a request asking for less gets less.

This is also why the form changes shape as you pick: a UPN field appears only for smartcard-logon profiles, an OCSP Must-Staple override only where the profile permits one, a PSD2 authorization group only on a qualified profile. Controls that would be rejected are not rendered, so the form cannot be used to build a request the gates will throw away.

Note what the form does not take: a private key. The subject generates its own key and sends a CSR. (When the subject genuinely cannot — a device enrolling by hand, an S/MIME recipient — the PKCS#12 page generates a key server-side and hands back a password-protected bundle, which is a different trade-off, deliberately on a different page.)


5. Dry-run it first

The pre-issuance dry-run result: a green "WOULD ISSUE — server under northwind-issuing-tls. Dry run only: nothing signed, no serial, no audit." banner, a table of the resolved leaf (subject, SANs, key usage, EKU, validity 397d, OCSP Must-Staple no), and a "Pre-issuance gates" table listing smime, upn, certificate_policy, must_staple, qcstatements, private_key_usage_period, lint, caa, name_constraints, keycheck, validity, approval and attestation with PASS or SKIPPED and a reason for each.

Preview (dry run) runs the entire pre-issuance gate stack and stops short of the signature. No serial is allocated, no audit event is written, the HSM is never asked for anything. The result is the certificate that would exist and the verdict of every gate that would have had to pass.

Read it as two halves. The top is the resolved leaf — what the profile and your request combined to, including the fields you did not supply. The bottom is the gate ledger, and SKIPPED there is as informative as PASS: "CAA gate disabled for the profile" and "issuing CA imposes no name constraints" are each telling you a control is not in force, which is sometimes the finding.

Use it in the two situations where issuing to find out is expensive: when you have just changed a profile and want to know what it now does, and when a request is being refused and the error alone does not say which of a dozen gates refused it. secsy-ca issue -dry-run is the same thing from a terminal; the full gate list is in pre-issuance preview.


6. The issued estate

The Certificates view for northwind-issuing-tls: filter controls for search, status and profile, Download CRL and Delta CRL links, a CRL freshness strip reading "CRL #2 · 2 revoked · updated 2026-10-31 03:37Z · next 2026-11-07 03:42Z | delta #3 (base #2) · 0 entries · next 2026-10-31 04:42Z | partitioned into 2 shards: 0 1", and a table of thirteen certificates with serial, common name, profile, not-after and status — mostly VALID, one HELD with a Release button, one REVOKED with no action buttons, and Renew / Suspend / Revoke on the rest.

This is the page you live on. One CA at a time, server-side filtered and paged — the table is keyset-paginated with a Load more action rather than loading an estate of a hundred thousand certificates into a browser tab.

The strip above the table is the CRL, and it is the part operators forget to look at. It names the current CRL number, how many entries it carries, when it was regenerated and — the one that matters — when it next will be. A relying party that fetches a stale CRL fails open or fails closed depending on its own configuration, neither of which you chose. If next is in the past, revocation is not being published and that is an incident, not a cosmetic issue. The delta CRL and the per-shard links are beside it, for the estates large enough to need partitioned CRLs.

The per-row actions are the lifecycle, and the distinction between two of them is the one to get right:

  • Revoke is permanent. RFC 5280 gives no way back, and the reason code you pick is published to every relying party. Choose it deliberately — keyCompromise is a statement about the subject's key, superseded and cessationOfOperation are statements about the service.
  • Suspend places the certificate on hold (certificateHold) and is reversible: the row gains a Release button, which emits removeFromCRL in the next delta CRL and returns it to service. Reach for this when you suspect rather than know — a host that has stopped responding, a laptop reported missing an hour ago.
  • Renew issues a replacement with a fresh serial, leaving the old one in place and valid until it expires. It is not an edit; both exist. (The two depot-fra.northwind.example rows above are exactly that — a renewal and its predecessor.)

7. When a key is compromised

The Bulk revocation — incident response panel, outlined in red: filter fields for profile, a CN/SAN glob set to *.fleet.northwind.example, issued-after and issued-before dates, a reason picker set to keyCompromise, an optional serial-list textarea, and a completed dry run reading "5 certificate(s) will be revoked (reason keyCompromise, filter: pattern=*.fleet.northwind.example)" above a sample of the five matching scanner and telematics certificates, then a field labelled "Type the certificate count to confirm (5)" and a Revoke certificates button.

Revoking one certificate is a button. Revoking the certificates of an entire device fleet, against the CA/Browser Forum's 24-hour clock, at three in the morning, is where mistakes get made — so this panel is built to be slow in exactly one place.

The selection is a filter: profile, a CN/SAN glob, an issuance window, and optionally a pasted list of serials (including serials the inventory has never seen, which is how you revoke certificates an attacker issued with a stolen CA key — they go onto the CRL as bare entries).

Then the contract. Preview (dry run) reports the count and a sample, and nothing happens. To proceed you must type that exact count into the confirmation field. A filter that matched 4,000 certificates when you expected 5 cannot be executed by muscle memory, because the number you have to type is the number the preview actually found, and if those disagree you stop.

Execution is batched and resumable: re-running the same selection after an interruption continues rather than duplicating. The CRL and delta CRL are regenerated once at the end, not per certificate, and the cached OCSP responses are invalidated — so the propagation you care about happens once and completely. The full procedure, including how to verify propagation afterwards, is incident response: mass revocation.


8. What breaks next

The Expiry & renewal status view: a minimum-severity filter, Refresh and "Scan & auto-renew" buttons, a summary line reading "ok 15 · warning 4 · critical 1 · expired 0", and a table ranked by remaining validity — gateway.northwind.example CRITICAL at 4d 23h, four WARNING rows between 24 and 29 days, then OK rows running out to 1094 days.

The expiry monitor is a background job; this page is its output, ranked by the only column that matters operationally — time left, not expiry date. The counters at the top are the whole fleet in one line, and the two bands are configurable (monitor.warning_days, monitor.critical_days).

Scan & auto-renew runs a scan immediately and, for profiles listed in monitor.renew_profiles, renews what is inside the renewal window. The reason auto-renewal is opt-in per profile rather than global is that renewal is only safe when something will deploy the result; a certificate renewed into the database while the server keeps serving the old one has fixed nothing. Wire the delivery first — ACME, the enrollment agent, or your own automation against the API — then enable it for those profiles.

The same information reaches a pager through the monitor's notification sinks and the Prometheus metrics; this page is for looking, not for finding out. See expiry monitoring and auto-renewal.


9. Across every CA

The Certificate inventory view: filters for CA, profile, free-text search and status, an Export CSV button, a count reading "23 of 23 certificate(s)", a collapsed "Key-compromise search — find every certificate that shares a public key (SubjectPublicKeyInfo SHA-256)" panel, and a table spanning all CAs with serial, common name, profile, CA, not-after, status (including a REVOKED row annotated keyCompromise, another annotated cessationOfOperation, and a HELD row), a CT column and a LINT column reading PASS.

Certificates is per-CA; Inventory is the whole deployment at once, which is what you want when the question is about a name rather than about an issuer. It adds two columns the per-CA view does not carry: whether the certificate has CT SCTs, and its lint verdict. Export CSV takes the current filter, not the whole table.

The collapsed panel at the top is the one to remember before you need it. Key-compromise search takes a SubjectPublicKeyInfo SHA-256 fingerprint — or a pasted public key, which your browser fingerprints locally — and finds every certificate across every CA that shares that key. When a private key leaks, the blast radius is not "the certificate in the ticket"; it is every certificate ever issued over that key, possibly under several names and several CAs. This finds them and hands you the ready-to-run revoke-bulk command for the set.


10. Prove a chain

The Certificate chain validation view: a trust-anchor CA picker, a "skip revocation" checkbox, textareas for the certificate and optional intermediates, and a verdict panel headed "VALID — chain built against northwind-issuing-tls". A check table lists chain PASS ("built a 3-certificate path to trust anchor Northwind Logistics Root CA R1"), validity PASS, revocation PASS, name_constraints and certificate_policy SKIPPED, key_usage, weak_key and weak_signature PASS. Below, a "Resolved chain" table shows the leaf wms.northwind.example with revocation GOOD, the TLS Issuing CA flagged CA, and the Root CA flagged ANCHOR.

Validate answers "would a client accept this?" without involving a client. Paste a leaf, pick the trust anchor, and get a structured verdict instead of an opaque success or failure: the path it built, the validity window, live CRL and OCSP revocation status per certificate in the chain, name-constraint and certificate-policy conformance, and weak-key or weak-signature flags.

Nothing is signed and nothing is stored, which makes it the right first tool for a support question. The three things it catches that eyeballing a certificate does not: a chain that cannot actually be built (the intermediate your server serves is not the one that signed the leaf), a revocation status that is unknown rather than good because the responder is not reachable, and a certificate that is valid today and will not be in a week. The Resolved chain table is where to look when the verdict surprises you — it names every certificate the path builder chose and flags which one is the anchor.

Full semantics in chain and path validation.


11. What a relying party needs

The Trust bundle & chain view: a CA picker, a Download chain (PEM) link, a textarea showing the issuer chain in PEM, an "Alternate chains" panel listing one row — native (primary), trust anchor northwind-root, depth 2 certs, with a Download PEM button — and a "SPIFFE trust bundle (JWKS)" textarea showing a JSON key set with an x509-svid EC P-256 key.

This is the page you send people to, rather than pasting certificates into a chat. It serves the issuer chain a relying party needs to validate this CA's leaves, kept correct across a key rollover — during an overlap window the bundle contains both the old and the new issuer, which is the entire point of the overlap.

Alternate chains is the panel that matters during a root transition. A cross-signed CA can be reached by more than one path to more than one trust anchor, and which path you serve decides whether an old client succeeds. Each row is one such path, with its anchor and depth, downloadable separately: serve the alternate to the clients that trust only the older root, serve the native chain to everyone else. With no cross-signatures configured — as here — there is exactly one row, and that is the correct answer rather than an empty state.

Below it, when SPIFFE is enabled, the JWKS trust bundle and the X.509-SVID and JWT-SVID minters for workload identity.


12. The other CA: SSH

The SSH certificate authority view: an SSH CA picker set to "northwind-ssh-ops — max 12h" with CA public key and Download KRL links; a "Maximum certificate lifetime (this CA)" panel reading "northwind-ssh-ops signs no certificate longer than 12h (43200s). Longer requests — from any profile, the API, or the CLI — are clamped to it. Certificates already signed keep their lifetime.", with a ceiling field holding 12h and Apply ceiling / Remove ceiling buttons; a Create SSH CA form with label, key type and maximum-certificate-lifetime fields; a "Sign a public key" form with type, profile, validity, key ID, principals and an authorized_keys-line textarea; a "Signed certificates" table listing a host certificate for bastion.northwind.example and two user certificates for j.lindqvist and m.okonkwo, all VALID with Revoke buttons; and an empty "Revocations (KRL entries)" panel.

OpenSSH certificates are not X.509 and do not appear anywhere in the pages above — but they are signed by a key in the same token, under the same RBAC, into the same audit log. If you run SSH at any scale this page replaces authorized_keys management entirely: hosts trust the CA's public key once, and access becomes a matter of who holds a current certificate.

The mechanics are the same shape as X.509 with different words. A profile fixes the certificate type, the validity bounds, which principals may be requested and which extensions and critical options are permitted — so an operator profile can allow 8-hour certificates with a PTY while a ci-deploy profile allows 15 minutes, no PTY, and principals matching deploy-* only. Sign a public key takes an authorized_keys line, not a CSR, because that is what SSH has.

Above those forms is the one control that is not per-profile: the CA's maximum certificate lifetime. A profile bounds the certificates signed under it; this bounds every certificate signed by this key, whichever profile or endpoint asked — including the profile's own default, which is why the host certificate above expires in 12 hours rather than the 90 days host-default would otherwise grant. Tighten it here and the next signature is shorter; certificates already signed keep what they were signed with, so a tightening is a policy change, not a revocation.

Revocation is the KRL rather than a CRL: revoke a certificate here and download the KRL for your hosts' RevokedKeys. The validity windows are the real control — the two user certificates above expire the same day they were issued — and the KRL is the backstop. SSH CA reference.


13. What a signing request may ask for

The Issuance restrictions view: an amber banner warning that X.509 sets are stored but not enforced; a Type filter with New SSH set, New X.509 set and Refresh buttons; and a table of seven sets. Four are built-ins scoped global — "Permit all signatures" and "Disallow all signatures" for each of ssh and x509, the x509 pair annotated "not enforced" and the disallow pair badged DENY ALL. Then ssh-operators, global, max validity 8h, constraints "principals: *, cert type: user, reason required" and a NO CRITICAL OPTIONS badge; ssh-ci-deploy, scoped to northwind-ssh-ops, 15m, "principals: deploy-*, cert type: user, reason required" with NO EXTENSIONS and NO CRITICAL OPTIONS badges; and x509-leaf-only, scoped to northwind-issuing-tls, unlimited validity, key usage digitalSignature and keyEncipherment, ext key usage serverAuth, SAN type dns, SAN *.northwind.example and a NO CA CERTS badge. Below, a "Per-CA default sets" panel lists every CA with a Default SSH set and Default X.509 set picker — northwind-ssh-ops set to "ssh-operators (global)", every other row to "Disallow all signatures (global)".

The page before this one put a ceiling on a whole CA. Restrictions puts one on a requester — and it is the only stop on this walk with no secsy-ca equivalent. Restriction sets are administered through this view and the API or not at all, which inverts the relationship the rest of this document relies on.

Three ceilings now stand between a request and a signature, and they compose by taking the smallest: the profile bounds what may be asked for, the CA's maximum lifetime from section 12 bounds every signature made by that key, and the effective restriction set bounds this requester.

A restriction set enforces in two different ways, and the difference decides what a caller sees. Asking for something the set does not permit — a principal outside the list, the wrong certificate type, a custom extension or a critical option under a deny, or anything at all under deny all — is refused, 403, nothing signed. Asking for too long a lifetime is not refused but silently clamped to the cap, exactly as the CA ceiling clamps. So a CI job that asks ssh-ci-deploy for 24 hours does not fail; it gets a 15-minute certificate and has to be built to re-request, which is the behaviour you want but not the one "refused when it exceeds that set" leads you to expect.

Which set is "effective" is a resolution, and its order is worth knowing because only the last step is visible here: the requester's own permission, then their groups', then the CA default in the table at the bottom. The policy above is the usual shape — a permissive global set for humans (ssh-operators: 8h, user certificates, a reason recorded in every key ID) and a far tighter per-CA one for machines (ssh-ci-deploy: 15 minutes, principals matching deploy-*, no extensions, no critical options).

The dangerous setting is the one that reads as cautious. When nothing resolves, enforcement is skipped entirely and the request is unrestricted — so setting a CA's default to none does not deny signing, it stops checking. The deny is the built-in Disallow all signatures, which is also what a new CA starts with. Reach for that one; none is a hole.

The banner at the top is not boilerplate either. The X.509 half of this page is stored and enforced by nothing: no issuance path consults it — not POST /api/ca/{id}/issue, not the raw /api/keys/{id}/sign-x509 — so the x509-leaf-only row above constrains precisely nothing, and a CA whose X.509 default is Disallow all signatures will still issue. What binds X.509 is the certificate profile from section 4 — and the page below. Restrictions reference.

The ceiling that does bind

The Signing Policy view with the Kind filter set to certificate: a table of six CEL rules with priority, kind, name, state, effect, mode, scope and expression columns, each row carrying Disable, Edit, Dry-run and Delete buttons. max-lifetime (10, enabled, require, enforce, every issuance) returns a formatted string naming the requested day count; names-under-northwind (20) filters and maps the DNS names outside the domain; wildcard-entitlement (30) tests a jwt.entitlements claim and carries the message "a wildcard certificate requires the pki.wildcard entitlement in your token"; corporate-ip-ranges (40, scoped to profiles server and server-client) uses cidr("10.0.0.0/8").containsIP; change-ticket-required (50) is badged MONITOR and matches body.reason against a ticket pattern; subordinate-ca-needs-admin (60) is badged DISABLED and greyed, with an Enable button in place of Disable. Above the table, a Show filter, a Kind filter set to "certificate — issuance", New rule and Refresh buttons; above those, an introduction describing the two rule kinds and the facts each reads, and a note stating that a rule is fail-closed and should be written disabled or in monitor mode first.

A profile fixes a shape: these key usages, that validity ceiling, those SAN types, the same for everyone. Signing policy is for the limits that are conditional — on who is asking, on what their token says, on what else is in the request — and that a field in a configuration file cannot express. The rules are CEL, they are evaluated before every signature on every path (this console, the API, the CLI, ACME/EST/SCEP/CMP), and they live in the database, so a rule written here applies to the next request with no restart.

The Kind column is the first thing to read. One table holds two kinds of rule: a certificate rule gates certificate generation and reasons about request; a signing rule gates everything else these keys sign — an OpenSSH certificate, a release artifact, a raw signature — and reasons about signing. The filter above the table is set to certificate for the shot above; the signing half is two screenshots further down.

Read the max-lifetime row carefully, because it is the point of the feature. It does not return true or false; it returns the sentence the requester is shown:

request.validity_days <= 90 ? "" :
  "requested %d days of validity; this profile allows at most 90".format([request.validity_days])

An expression may yield a bool (the condition), a string (the reason itself, with "" meaning satisfied), or a list of strings (one reason per offending name). That is why a refusal from this engine comes back as "requested 1460 days of validity; this profile allows at most 90" and not as 403 Forbidden.

The five rules above show the vocabulary: request (the certificate, as the to-be-signed template — so the clamped validity and the profile's usages, not the request JSON), jwt (the verified claims of the caller's token), url and path (the request as an object), body (its parameters — change-ticket-required reads a field the certificate does not carry), and principal (the caller's roles and groups).

The Dry-run panel, run: a Kind picker set to "certificate — issuance", a Rules picker set to "every enabled rule", an Issuing CA picker, a Profile picker, a validity field, an empty CSR box and a Facts JSON box showing a request with a wildcard, two DNS names and two IP addresses. Below the Dry-run button, a red "WOULD BE REFUSED" badge, the line "5 rule(s) evaluated, 0 skipped — considered: max-lifetime, names-under-northwind, wildcard-entitlement, corporate-ip-ranges, change-ticket-required", and five reasons: max-lifetime "requested 1460 days of validity; this profile allows at most 90"; names-under-northwind "DNS name shop.partner.example is outside northwind.example, which this CA may not certify"; wildcard-entitlement "a wildcard certificate requires the pki.wildcard entitlement in your token"; corporate-ip-ranges "IP address 203.0.113.7 is outside the corporate range 10.0.0.0/8"; and change-ticket-required, badged MONITOR — WOULD NOT BLOCK, "the reason parameter must name a change or incident ticket, e.g. CHG-10482". A collapsed "Facts the rules saw" disclosure sits below.

Do not write a rule straight into enforcement. A rule is fail-closed — one that cannot be evaluated refuses the request rather than letting it through — so a mistake here refuses every certificate under its scope. The two safe routes are both on this page. Save the rule disabled (subordinate-ca-needs-admin above) and dry-run it; or save it in monitor mode (change-ticket-required), which records what it would have refused, in the decision and in the cert.policy audit event, and issues anyway. Measure, then flip it to enforce.

The dry-run is the same evaluator the gate runs, and giving it a CSR under a real CA and profile derives the certificate facts through the same code — so a rule that passes here behaves the same way when that CSR is actually submitted. Note what the run above does for the requester: five rules, four reasons, one response. Someone who got four things wrong learns four things, instead of discovering them one failed request at a time.

Over the API that is an HTTP 422 — not 403, because the credentials were fine and the request was not — carrying each reason as {rule, message}. CEL issuance restrictions.

The same engine, in front of the signatures that are not certificates

The Signing Policy view with the Kind filter set to "signing — SSH, artifacts, data, SVIDs": a table of six signing rules with priority, kind, name, state, effect, mode, scope and expression columns, every kind cell reading SIGNING. no-privileged-principals (10, enabled, require, enforce, operations: ssh-certificate) filters the principals that are shared privileged accounts and maps each to its own reason; ssh-validity-ceiling (20, operations: ssh-certificate) compares signing.ssh.validity against duration("12h") and formats the refusal; release-key-is-pipeline-only (30, keys: northwind-release, operations: artifact) tests jwt.repository and carries the message "the release-signing key is reserved for the northwind/product release workflow"; artifacts-need-a-timestamp (40, operations: artifact) requires signing.artifact.timestamped; no-caller-supplied-digests (50, scope "every signature") is badged MONITOR and tests !signing.payload.prehashed; ssh-keys-must-be-fido-resident (60, operations: ssh-certificate) is badged DISABLED and greyed, with an Enable button in place of Disable. Below the table the editor is open on release-key-is-pipeline-only: a Kind selector labelled "fixed once saved" and set to signing, the name, priority, effect and mode fields, the expression box with a Check expression button and an "insert an example" picker, the message field, a Profiles / CAs / Tenant row, and below it a Keys / Operations row — holding northwind-release and artifact — that a certificate rule does not have.

Nothing above is a second feature. It is the same table, the same editor, the same compile check, the same fail-closed reading and the same function library — pointed at the other signatures this HSM makes. Only the vocabulary differs, and the Kind selector in the editor switches it: the reference panel, the insertable examples and the compile check all follow it, and the Keys and Operations scope row appears, because a signing rule narrows by key and operation where a certificate rule narrows by profile and CA.

The two vocabularies are separate environments on purpose. A signing rule that mentions request.dns_names does not compile; a certificate rule that mentions signing.key.name does not compile either. A merged one would accept both and then evaluate the wrong vocabulary against an empty object — a rule that reads as working and in fact never fires. That is also why a rule's kind is fixed once saved.

What the six rules are doing is worth a pass, because each is a thing no configuration field could say:

  • no-privileged-principals — no OpenSSH certificate for root or admin, on any CA, by anyone. The SSH profile can restrict principals by pattern; it cannot say "never these, everywhere".
  • ssh-validity-ceiling — a lifetime ceiling that reports the duration it refused. On the real path the facts come from the resolved certificate — after the profile's defaults and after the CA's own ceiling — so a request the CA has already clamped is judged on what it was clamped to, not on what was asked for. (The dry-run below is hand-written facts, so there is nothing to clamp: it refuses the 24h it was given.)
  • release-key-is-pipeline-only — the release key belongs to one repository's workflow token. A human with this console cannot use it at all.
  • artifacts-need-a-timestamp — a CAdES-B signature stops verifying the day its certificate expires, which is not what "signed release" is supposed to mean.
  • no-caller-supplied-digests — in monitor mode, measuring how much traffic asks this PKI to attest to bytes it never saw.
  • ssh-keys-must-be-fido-resident — parked, because not every operator has a security key yet. An OpenSSH sk- key type is the one thing a public key proves about where its private half lives.

The signing Dry-run, run: a Kind picker set to signing, a Rules picker set to "every rule, enabled or not", no CSR / issuing-CA / profile fields, and a Facts JSON box holding an ssh-certificate operation from source rest for ci@northwind.example, signed with the Ed25519 northwind-ssh-ca on a pkcs11 provider. Below the Dry-run button, a red "WOULD BE REFUSED" badge, the line "4 rule(s) evaluated, 2 skipped — considered: no-privileged-principals, ssh-validity-ceiling, release-key-is-pipeline-only, artifacts-need-a-timestamp, no-caller-supplied-digests, ssh-keys-must-be-fido-resident", and three reasons: no-privileged-principals "principal root is a shared privileged account; use a named account and sudo"; ssh-validity-ceiling "requested 86400s of SSH certificate validity; this CA signs at most 12h"; and ssh-keys-must-be-fido-resident "a user SSH certificate is only signed for a FIDO-resident key (ssh-keygen -t ed25519-sk)". A collapsed "Facts the rules saw" disclosure sits below.

The dry-run has its own kind selector, and switching it replaces the fact set with that vocabulary's worked example — certificate facts in a signing dry-run would evaluate every rule against an empty signing request and report that nothing is refused, which is the one wrong answer this panel must not give. The CSR, CA and profile inputs disappear for a signing rule: there is no CSR whose resolution the server could replay, so the facts are the fact set.

A refusal here is the same HTTP 422 with the same {rule, message} list, on POST /api/ssh/cas/{id}/sign, POST /api/sign and POST /api/secret/signing-keys/{name}/sign alike — and on the CLI, because each gate sits inside the service that owns the key rather than in a handler. CEL signing restrictions.


14. Signing artifacts

The Artifact signing view: a "Configured signers" table with one row — release-signing, subject "Northwind Logistics Release Signing", digest sha256, timestamp default yes, level default T, expiring 2029 — and Sign and Verify panels side by side. Sign offers a signer picker, a CAdES level selector, an artifact file upload or a hex digest, and a Sign artifact button; Verify takes a detached PKCS#7 signature, an artifact or digest, an optional trust anchor, a require-level selector, a "require timestamp" checkbox and a Verify signature button.

The same HSM that signs certificates signs release artifacts, as detached CMS/PKCS#7. The CAdES level column is the part to decide once and then stop thinking about:

Level What it adds What it buys
B signed attributes a signature
T + an RFC 3161 timestamp proof it was signed before the key was revoked
LT + embedded revocation material verifiable after the signing certificate expires

The jump from B to T is the one that matters, and it is why the signer above defaults to T with timestamp: yes. Without a trusted timestamp, revoking a compromised code-signing key invalidates every signature it ever made, including the honest ones, because nothing establishes which came first. With one, the signatures made before the compromise still verify. Choose LT for anything that must still verify in ten years, when the signing certificate is long expired and the CRL that covered it is gone.

Verify is the other half, and the require level and require timestamp controls are what make it a gate rather than a formality: a release pipeline that accepts any valid signature accepts a B signature made yesterday with a key revoked last month. Artifact signing.


15. The other half: secrets

The Secret envelopes view: a KEK status line reading "KEK northwind-kek (family northwind-kek v1) · pkcs11 · 2048-bit · wrap RSA-OAEP-SHA1 · data AES-256-GCM", Encrypt and Decrypt panels each with a context/AAD field, and a "Crypto service — stateless: nothing you submit is stored" section containing Data key, Keyed HMAC and Random bytes panels.

secsy-pki is a certificate authority and a password/data encryption service, and this is the second half. It is envelope encryption: a fresh AES-256-GCM data key per secret, that data key wrapped by an RSA KEK that never leaves the token. The status line names the KEK, its version and both algorithms, which is the whole cryptographic design in one line.

The consequence worth understanding: decryption requires the HSM, every time. An attacker who walks off with your database walks off with ciphertext and wrapped data keys and nothing else. The flip side is that the HSM is in the path of every decrypt, which is why the data key is AES — the HSM unwraps one small key, not your payload.

Context / AAD is the field people skip and then wish they had used. It binds a ciphertext to a context — a tenant, a record ID, an environment — and the exact same value is required to decrypt. Without it, a ciphertext lifted from one row and pasted into another decrypts happily; with it, that attack produces an authentication failure.

The Crypto service panels below are stateless and store nothing: mint a data key for client-side envelope encryption, compute and verify a keyed HMAC under an HSM-derived MAC key, or draw bytes from the HSM's RNG. Further down the page (not pictured) are named signing keys, FF1 tokenization, and the KEK rotation lifecycle. Secret encryption reference.


16. The stored-secret registry

The Stored secrets panel: a table with Name, Version, KEK, Escrow, Expires, Rotate every, Value changed and Labels columns listing carrier-api/dhl-token at v1 expiring 2026-11-30, smtp/relay-password at v1 rotating every 180 days, and wms/db-password at v2 rotating every 90 days — all three wrapped under northwind-kek v1 — each with Reveal, Versions, Update and Delete actions — above a "Create / update" form and an empty "Version history" panel.

Encrypt/decrypt is stateless; this registry is the server holding the envelopes for you, and it changes what is possible in two ways.

Nothing is ever overwritten. Every write appends a version, and the old ones remain decryptable. So Versions shows the history, Reveal opens any version, and rollback makes an older value current by appending a copy of it — history is never rewritten. A rotation that broke production is undone in one click, not restored from a backup.

The estate becomes enumerable. Because the server knows which secrets exist and which KEK version each was wrapped under, a KEK rotation can re-wrap all of them and then tell you when retiring the old KEK is actually safe. An unenumerable pile of ciphertext in someone else's database cannot do that.

The Expires and Rotate every columns arm reminders rather than expiries — a secret past its date keeps working and shows up on the Lifecycle attention table below. The alternative, silently breaking production on a rotation deadline, is not better security.


17. The audit log

The Audit log view: action and actor filters, a page-size selector, and Verify chain, Anchor chain head, Export JSON, CEF and Syslog buttons. A green banner reads "Chain intact — 20 event(s) verified against the genesis hash", and a "20 event(s) · showing 1–20" line sits under a Live tail control. The table of events lists crl.publish rows for each CA’s base and delta CRL, auth.login, audit.anchor with a sequence number and head hash, key.block, webhook.create, two token.create rows, secret.put and secret.store, three ssh.sign rows and ssh.ca_init — each with time, actor, tenant, target, a SUCCESS result and detail.

Every mutating operation in every page above wrote a row here, and the rows are hash-chained: each entry commits to its predecessor, so removing or editing one breaks every link after it. Verify chain re-walks the chain end to end and reports the first broken link, which is the difference between a log you keep and a log you can rely on.

A hash chain alone only proves internal consistency — an attacker who controls the database can rewrite the whole chain from any point and recompute it. Anchor chain head is the answer: it takes an RFC 3161 timestamp over the current head and stores the token. A rewrite would have to produce a head that matches an anchor signed by a third party at a time in the past, which it cannot. The audit.anchor row in the screenshot is that operation recording itself.

Export JSON / CEF / Syslog are the SIEM formats, and Live tail streams events over SSE as they happen. Getting the log off this machine is the point: a tamper-evident log that an attacker can delete is a log you will discover is missing. See audit and SIEM export.

Below the fold — past the bottom of the screenshot — are two older tables the chain supersedes but does not contain: the signing-operation log, one row per certificate signed through POST /api/keys/{id}/sign with the restriction set that admitted it (section 13), and the HTTP access log of method, path, status and source IP. Neither is hash-chained, so neither carries the guarantee the view above it does. They are there because the event log began later than the server did, and an incident that predates it still has to be answerable.


18. Delegating one CA

The Access view: a resource picker set to "northwind-issuing-device — ca/9743e4c8-c68f-4558-950f-7ec4cb5bda37", a grants table showing one row — role ca-admin, entity group:fleet-ops, scope self, source DATABASE, granted 2026-10-31 03:42Z, with a Revoke button — and "Grant access" and "Effective access" panels below. Grant access offers a role picker (ca-manager with its capabilities listed), a user-or-group selector, an identifier field and a scope selector reading "self — this resource only".

Tenant-wide roles are too coarse for the common ask: let the fleet team run the device CA, and keep them away from the root. Access is per-resource delegation, and the row above is exactly that — the group fleet-ops holds ca-admin on the device issuing CA alone.

Three properties make it safe to hand out:

  • Grants are additive. They widen what a principal may do at one resource. Nothing here can take away authority a platform or tenant role already confers, so a grant cannot be used to lock an administrator out.
  • Scope is explicit. self is this CA only; subtree is this CA and every CA created beneath it later. Pick self unless you mean to delegate a branch.
  • Roles are capability sets, printed in the picker. ca-manager reads audit:read ca:configure ca:manage cert:issue right there in the dropdown, so you are not granting a word whose meaning you have to go and look up.

Effective access answers the question the other way round — what may this person actually do here, and which rule produced that? — which is the one you need during an access review, or when someone insists they should be able to do something and cannot. Per-resource grants.


19. Machine callers

The API Tokens view: explanatory text noting the opaque secsy_pat_ secret is shown once and stored only as a one-way hash, a table listing siem-exporter (tenant scope, auditor role, active, expiring 2026-11-30) and deploy-robot (tenant scope, issuer role, active, expiring 2027-01-29) with their key prefixes and Revoke buttons, a "Create token" form with name, role checkboxes for issuer/signer/auditor/approver/admin, a scope selector, a tenant field, an expiry field and a description, and an empty "New token secret" panel.

Automation should not hold an operator's password, and it should not hold root. A token is a service account: a name, a set of roles, a tenant scope, an expiry.

Note what the two tokens above are not. Neither is an admin. The SIEM exporter holds auditor — it can read the audit log and nothing else, which is all a log shipper has ever needed. The deployer holds issuer — it can request certificates and cannot revoke them, rotate a CA or read a secret. Issue the narrowest role that works, because a token is a credential that will end up in a CI configuration, and the blast radius of one that leaks is exactly the roles you gave it.

The secret is shown once, at creation, and stored only as a one-way hash: there is no "show token" button anywhere, because there is nothing to show. Lose it and you mint another. The table keeps the prefix so you can tell which token a log line refers to without knowing the secret, and Last used tells you which ones are dead and can be revoked.

Set an expiry. auth.api_tokens.max_lifetime_days caps it by policy so "never" cannot be chosen by accident. API tokens.


20. Compliance evidence

The Compliance & lint summary view: a CA filter set to "all CAs", a green banner reading "Conformant — audit chain verified and no non-conformant certificate was issued", and five stat tiles — 23 issued, 23 lint pass, 0 lint warn, 0 blocked (gate held), audit chain OK. Below, a "Certificate authorities" table listing the root, three issuing CAs and the SSH CA with their subjects, key types, an HSM badge each and their issued counts, an empty "Top lint findings" panel, and a "Profile breakdown" table showing server 9, client 6, server-client 4, and one each of code-signing, server-delegation, server-muststaple and smime-sign.

This page exists to be read by someone who is not you: an auditor, a security review, a customer questionnaire. It states a single claim at the top — conformant — and then shows the evidence behind it.

The tiles are the argument. Issued and lint pass should be equal; lint warn counts certificates that were issued despite a lint finding; blocked (gate held) counts requests the pre-issuance gates refused, which is a number you want to be non-zero over time, because zero means either perfect inputs or a gate that is not actually enforcing. Audit chain OK is the same verification as the Audit page, restated as a compliance control.

The per-CA table carries an HSM badge per CA — the claim that this CA's key is on a hardware token rather than in a file, which is the single most-asked question in any CA audit. The profile breakdown shows what the deployment is actually used for, which is often different from what its owners believe.

The mapping from these controls to the CA/Browser Forum Baseline Requirements and the RFC 3647 CP/CPS structure is in the compliance control mapping.


21. Day-2 diagnostics

The Preflight diagnostics panel: a "deep checks" checkbox and a Run diagnostics button, a green banner reading "Verdict ok (exit code 0) — 22 pass · 0 warn · 0 fail · 19 skip", a line naming the config file, the check time, "shallow probes" and a 60s timeout, and a table of checks with status, name, detail and elapsed — config.parse, config.unknown_keys, keyprovider.ca/tsa/signing, pin.source (SKIP), auth.ldap (SKIP), auth.oidc.pkce (SKIP), db.connectivity, db.schema, pkcs11.uris, keys.ca, keys.tsa, keys.signing, keys.secret_kek, keys.ocsp_delegate (SKIP), audit.chain_head and db.integrity (SKIP), the list continuing below the crop.

Operations is the console half of secsy-ca doctor: a read-only sweep over configuration, storage, the key provider, revocation freshness, publishing and the audit chain. It changes nothing, and the verdict carries the CLI's exit code so the same check can gate a deployment.

It is worth running at three moments: after a config change, before a release, and first thing when something is wrong — because it checks the boring layers underneath the symptom. keyprovider.ca proves the token is reachable and the PIN is accepted; keys.ca goes further and actually signs and verifies with each CA key; audit.chain_head verifies the chain; certs.ca_expiry reports the tightest headroom any CA has left. A warn or fail carries the hint that resolves it rather than only the finding.

SKIP is not a failure — auth.ldap skips because LDAP is not configured — and deep checks adds the slower gates (a full store-integrity pass) that are not worth running on every click.

The rest of the page covers the disaster-recovery manifest, the restore drill and static-artifact publishing; all three are described in the operator runbook.

This page used to disturb the key provider (fixed)

Through the 1.0 release, loading this page could take the server's HSM keys away until it was restarted: the HSM key inventory reported zero keys and secret decryption failed with CKR_OBJECT_HANDLE_INVALID. GET /api/doctor builds a second PKCS#11 provider for the sweep and closes it at the end, and that teardown — C_Logout, C_Finalize, dlclose — is process-global rather than per-handle, so it deauthenticated and then finalized the module out from under the serving session pool. The token was healthy throughout, which is what made it look like an HSM fault.

Both the module and the login are now reference-counted across every holder in the process (internal/pki/pool.go), so a diagnostic sweep logs out and finalizes only if nothing else is using the token. If you are running an older build, prefer secsy-ca doctor from a terminal: it runs in its own process and cannot reach the server's sessions.


22. What is actually on the token

The PKCS#11 inspector: a Module panel listing the SoftHSM module path, Cryptoki version 2.40, manufacturer SoftHSM, library "Implementation of PKCS11 2.6", flags 0x00000000 and 2 slots; a Token panel for the resolved token reading slot 2057406631, CKA label secsy-shots, model SoftHSM v2, serial 7bdec139faa188a7, hardware/firmware 2.6, flags 0x0000042D decoded to CKF_RNG CKF_LOGIN_REQUIRED CKF_USER_PIN_INITIALIZED CKF_RESTORE_KEY_NOT_NEEDED CKF_TOKEN_INITIALIZED, a PIN length of 4..255 and the token clock; and a Slots table listing both slots the module presents — the first badged IN USE with 70 mechanisms, the second an unlabelled token whose flags include CKF_SO_PIN_LOCKED.

Section 3 asked the key provider what it holds. PKCS#11 asks the token, in Cryptoki's own words: C_GetInfo, every slot the module presents, the CK_TOKEN_INFO of the one this deployment resolved to, the mechanism list, and every object on it.

The two views answer different questions, and the difference is the point of this page. The key inventory lists the keys this PKI knows about. This lists everything there is — including the second slot above, whose token is unlabelled and whose SO PIN is locked, which no inventory would ever have mentioned. That is what you need when a token selector matches nothing, when a key appears that nobody provisioned, or when the module turns out not to offer a mechanism the configuration assumes.

The module half opens no session and performs no login, deliberately: an operator reaching for a slot list is very often one whose login does not work.

The Objects panel of the PKCS#11 inspector: class and label filters, a max-objects field set to 6, a "whole catalogue" checkbox, Load objects and Objects JSON buttons, a summary reading "token secsy-shots (serial 7bdec139faa188a7), slot 2057406631 — showing 4 of 6 object(s)" with class counts CKO_PRIVATE_KEY=4 and CKO_PUBLIC_KEY=2, an amber warning that the token holds more objects than were read, and the expanded attribute table of object 0x9 (CKO_PRIVATE_KEY northwind-root): CKA_CLASS, CKA_TOKEN, CKA_PRIVATE, CKA_LABEL, CKA_KEY_TYPE CKK_EC, CKA_SENSITIVE true, CKA_EXTRACTABLE false, CKA_NEVER_EXTRACTABLE true, CKA_EC_PARAMS decoded to "P-384 (secp384r1) [1.3.132.0.34]", two template rows badged NOT-READ, and the CKA_PRIVATE_EXPONENT, CKA_PRIME_1, CKA_PRIME_2, CKA_EXPONENT_1, CKA_EXPONENT_2, CKA_COEFFICIENT and CKA_VALUE rows badged WITHHELD with the reason "not requested: the value is private key material".

Objects loads on a click rather than with the page, because it costs a device round trip per object — and, on a force-audited YubiHSM, entries in the device's own log. Each row expands to the full attribute table, with every value decoded by its Cryptoki type: a curve OID becomes P-384 (secp384r1), a certificate's CKA_VALUE becomes its subject and expiry, a modulus its bit length.

Two kinds of row carry no value, and both are deliberate. WITHHELD marks an attribute whose value is private key material: the inspector does not ask for it. A correctly provisioned token would refuse anyway — and that refusal, CKR_ATTRIBUTE_SENSITIVE, is reported when it happens — but this page must not be the one path in the system that turns a mis-provisioned extractable key into a browser response. NOT-READ marks the CK_ATTRIBUTE-array attributes, which cannot be read without a caller-built template.

What this page is not is proof. CKA_EXTRACTABLE=false is the module's answer over a session; the device-signed version of the same claim is key attestation, one tab to the left. Use PKCS#11 to see what is there, and attestation to prove what it is. The full reference is the raw PKCS#11 inspector.


Where to go next

  • The web console reference — the same UI as a reference: every page, including the ones this walk skipped (ACME, CT inclusion, discovery, PKCS#12, approvals, tenants, DNS records, webhooks), and the endpoints behind each.
  • CLI ↔ console parity matrix — every page above against the secsy-ca command that does the same thing, and the commands that stay deliberately on the command line.
  • Operator runbook — the procedures this page only points at: CA key compromise, OCSP/CRL outages, rotation and the DR drill.
  • HSM configuration — what sits behind section 3, and how to move from SoftHSM to real hardware.
  • RBAC and audit — the role model the whole console is gated by.

↩ Back to day-2 operations · documentation map