尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证A simple, sexy PKI (experimental)
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗HSM-backed SSH and X.509 Certificate Authority with OIDC authentication, publicly auditable signing logs, and a web UI.
📖 Documentation · Quick start · Console walkthrough · Examples · Architecture
Every page of docs/ is published there — searchable and
cross-linked — by the documentation-site workflow;
make docs-serve renders the same tree locally.
See it first: a walkthrough of the operator console — 22 steps and 27 screenshots, from sign-in to the raw PKCS#11 view of the token.
Secsy PKI manages a public key infrastructure where CA private keys are stored on hardware security modules (HSMs) via PKCS#11. Users authenticate through OpenID Connect and request SSH or X.509 certificates signed by the HSM. Every signing operation is recorded in a cryptographically verifiable audit log backed by the YubiHSM's hardware hash chain.
The enterprise edition (this repository) extends the base CA into a full HSM-backed enterprise PKI and secret-management platform. On top of per-key SSH/X.509 signing it adds:
internal/keyprovider) routes every key operation to a PKCS#11 HSM (YubiHSM, network HSM), SoftHSM for dev/CI, or an on-disk software keystore. Keys are generated on the device and never exported.admin, issuer, auditor) layered over the existing per-CA permission matrix.See the architecture overview and the full guide index in docs/ for everything below.
Comprehensive deployment and operations guides live in docs/,
grouped into twelve sections. Each links to its own index; the
documentation map lists every page. The same pages are
published — with search and navigation — at
https://blechschmidt.github.io/secsy-pki/ (how).
| Section | Covers |
|---|---|
| HSM & key management | PKCS#11 HSMs, cloud KMS and Vault Transit backends, key ceremony and DR, key attestation, the remotely verifiable HSM audit log |
| Certificate authority | Root and intermediate CAs, the certificate lifecycle, key rotation, cross-signing, the SSH CA, chain validation |
| Issuance policy & gates | The fail-closed pre-issuance stack: BR linting, CAA, name constraints, weak-key checks, Certificate Transparency |
| Certificate types & profiles | S/MIME, smartcard logon / PKINIT, eIDAS qualified, SPIFFE SVIDs, delegated credentials, post-quantum |
| Enrollment protocols | ACME, SCEP/EST, BRSKI, Windows autoenrollment, the host agent, and the gRPC API |
| Signing & timestamping | Artifact/code signing, the RFC 3161 TSA, trusted time, RFC 4998 evidence records |
| Secret & password encryption | HSM-backed envelope encryption, escrow and recovery, tokenization, the crypto service |
| Security & governance | RBAC, operator authentication, four-eyes approvals, multi-tenancy, rate limiting, FIPS mode, SIEM export |
| Deployment & scaling |
The Architecture Decision Records explain why the load-bearing design decisions were made.
Worked examples for common deployments — an SSH PKI, keyless software signing from GitHub Actions through OIDC, automated TLS with ACME, and a private CA for service-to-service mTLS — live in examples/: a minimal, ready-to-adapt config plus the client-side glue for each use case.
The secsy-ca and secsy-secret CLIs drive the CA and secret features; see the guides above. The sections below document the base server, SSH workflow, and per-key signing API.
secsy-verify) proves the complete chain of trust.SIGN_CERTIFICATE, MANAGE_PERMISSIONS, and CONFIGURE_CA per key, assignable to users or groups with separate SSH and X.509 restriction set overrides.POST /api/keys/{id}/sign: max validity, allowed principals/cert types, forced email+reason key IDs, deny extensions/critical options. X.509 sets are stored but consulted by no issuance path — the equivalent X.509 ceiling is the certificate profile; see the console reference. Built-in "Permit all" and "Disallow all" defaults for both types; a new CA starts at "Disallow all".secsy-ca only, by design.graph TB
Browser["Browser (SPA)"]
Server["Go HTTP Server"]
SQLite["SQLite DB"]
HSM["YubiHSM (PKCS#11)"]
OIDC["OIDC Provider"]
Browser -->|"HTTPS + OIDC/Basic Auth"| Server
Server --> SQLite
Server -->|"PKCS#11 (yhusb://)"| HSM
Server -->|"ID token verification"| OIDC
HSM -->|"Ed25519/ECDSA/RSA signing"| Server
Secsy PKI provides cryptographic proof that a CA key has only signed the certificates listed in the audit log. The verification relies on three independent mechanisms that together form a complete chain of trust.
graph LR
YubicoRoot["Yubico Root CA"]
YubicoInt["Yubico Intermediate CA"]
DeviceCert["Device Certificate"]
AttestKey["Attestation Key (0x0001)"]
CAKey["CA Key (e.g. 0x50dd)"]
HashChain["HSM Hash Chain"]
Signature["Signature on last hash"]
Certs["Signed Certificates"]
YubicoRoot -->|"signs"| YubicoInt
YubicoInt -->|"signs"| DeviceCert
DeviceCert -->|"signs attestation cert for"| AttestKey
DeviceCert -->|"signs attestation cert for"| CAKey
AttestKey -->|"signs"| Signature
HashChain -->|"last hash"| Signature
CAKey -->|"signs"| Certs
secsy-verify verify-combined-log)graph TD
S1["Step 1: Verify Signed HSM Audit Log"]
S1a["Hash chain integrity"]
S1b["Signature on last hash (Ed25519)"]
S1c["Attestation cert ← Device cert ← Yubico CA"]
S2["Step 2: Combined Log Hash Chain"]
S2a["Independent hash chain verification"]
S2b["Consistency with signed log"]
S3["Step 3: CA Key Attestation"]
S3a["Match provided public key to attestation cert"]
S3b["Verify attestation cert signed by device"]
S4["Step 4: CA Key Properties"]
S4a["Generated on HSM (origin=generated)"]
S4b["Never exported"]
S4c["Sign-only capabilities (no decrypt/wrap/export)"]
S4d["Forced audit enabled before key generation"]
S4e["All sign commands force-audited before keygen"]
S5["Step 5: Cross-Reference"]
S5a["Every HSM sign op → combined log entry"]
S5b["Every combined log entry → HSM sign op"]
S6["Step 6: Certificate Verification"]
S6a["Certificate signature matches attested CA key"]
S6b["Parameters match log (key_id, principals, validity)"]
S7["Step 7: Bijection"]
S7a["All certificates unique (no duplicate hashes/serials)"]
S7b["N HSM ops ↔ N unique verified certificates"]
S1 --> S1a & S1b & S1c
S2 --> S2a & S2b
S3 --> S3a & S3b
S4 --> S4a & S4b & S4c & S4d & S4e
S5 --> S5a & S5b
S6 --> S6a & S6b
S7 --> S7a & S7b
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7
graph LR
subgraph "HSM Hardware"
HC["Hash Chain<br/>(computed inside HSM)"]
Sign["Sign operations<br/>(recorded in chain)"]
end
subgraph "Attestation"
DC["Device Cert<br/>(Yubico CA chain)"]
AK["Attestation Key<br/>(signs last hash)"]
CK["CA Key Attestation<br/>(generated, sign-only,<br/>unexportable)"]
end
subgraph "Provisioning Order"
PO["Factory Reset → Audit Provisioning<br/>→ Key Generation"]
end
subgraph "Bijection"
N_ops["N HSM sign ops"]
N_certs["N unique certificates"]
Verify["Each cert signed by<br/>attested CA key"]
end
HC --> AK
DC --> AK
DC --> CK
PO --> HC
Sign --> N_ops
N_ops -->|"1:1 mapping"| N_certs
CK --> Verify
N_certs --> Verify
The verification proves a strict provisioning order from the audit log:
Forced audit mode (irreversible) ensures the HSM refuses operations when the 62-entry log is full, preventing unlogged signing. The server consumes HSM audit entries before and after every HSM operation to keep the log from filling up.
Build the verifier:
cd server
go build -o secsy-verify ./cmd/verify
Verify a signed audit log:
secsy-verify verify-audit-log \
--audit-log signed-audit-log.json \
--yubico-ca yubico-root.pem \
--yubico-intermediate yubico-intermediate.pem
Verify the complete chain including certificate parameters:
secsy-verify verify-combined-log \
--signed-log signed-audit-log.json \
--combined-log combined-audit-log.json \
--ca-key ca-public-key.pub \
--yubico-ca yubico-root.pem \
--yubico-intermediate yubico-intermediate.pem
The Yubico CA certificates can be downloaded from:
cd server
# With SQLite support
go build -tags sqlite -o secsy-pki-server ./cmd/server
# Without SQLite (PostgreSQL only)
go build -o secsy-pki-server ./cmd/server
# Tools
go build -o secsy-verify ./cmd/verify
go build -o secsy-ssh ./cmd/secsy-ssh
mkdir -p certs
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout certs/server.key -out certs/server.crt -days 365 -nodes \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
Edit config.yaml:
server:
host: "0.0.0.0"
port: 8443
tls_cert: "certs/server.crt"
tls_key: "certs/server.key"
database:
driver: "sqlite"
dsn: "secsy-pki.db"
oidc:
issuer_url: "http://localhost:8080/realms/secsy-pki"
client_id: "secsy-pki"
root_user:
username: "root"
password: "change-me-in-production"
pkcs11:
module_path: "/usr/lib/pkcs11/yubihsm_pkcs11.so"
pin: "0001password"
token_label: "YubiHSM"
token_serial: "" # optional: match by serial number
token_manufacturer: "" # optional: match by manufacturer
yubihsm:
connector_url: "yhusb://"
auth_key_id: 1
password: "password"
suppress_audit_warning: false
# A YubiHSM's USB interface belongs to one claim, and two things here need the
# device: the PKCS#11 signing path and the native audit/attestation driver.
# The server takes the claim and relays for the module on loopback, so one
# process — and one container — does both. On by default for this pair.
embedded_connector:
enabled: true
For SoftHSM development, use module_path: "/usr/lib/softhsm/libsofthsm2.so". Run ./scripts/setup-softhsm.sh to initialize a token, and eval "$(./scripts/setup-softhsm.sh --export-env)" to wire the SECSY_* environment variables. See HSM / PKCS#11 configuration.
The enterprise blocks — key_provider, rbac, policy, profiles, and secret — are documented in docs/ and shown fully commented in server/config.yaml.
For PostgreSQL, change the database section:
database:
driver: "postgres"
dsn: "host=localhost user=secsy password=secret dbname=secsypki sslmode=disable"
./secsy-pki-server -config config.yaml
The server auto-generates yubihsm_pkcs11.conf from the yubihsm.connector_url config setting — or from the address of its own embedded connector when it is multiplexing the device, so the signing path and the audit driver cannot drift apart. You can also set YUBIHSM_PKCS11_CONF manually if needed.
Yubico's PKCS#11 module is not part of this repository — install the YubiHSM 2 SDK, or pull the -yubihsm container image, which has the module and the libyubihsm transports baked in for both architectures. See the container image.
Open https://localhost:8443 and log in as root.
secsy-ssh wraps the standard ssh command with automatic OIDC-based certificate authentication. It handles the entire flow: OIDC login via browser, certificate signing through the Secsy PKI API, and passing the certificate to ssh — all in a single command.
sequenceDiagram
participant User
participant secsy-ssh
participant Browser
participant OIDC as OIDC Provider
participant API as Secsy PKI API
participant HSM as YubiHSM
participant SSH as SSH Server
User->>secsy-ssh: secsy-ssh --ca "Root CA" user@host
secsy-ssh->>secsy-ssh: Find ~/.ssh/id_ed25519
secsy-ssh->>secsy-ssh: Check certificate cache
alt Cache hit (not expired)
secsy-ssh->>SSH: ssh -i key -o CertificateFile=cert user@host
else Cache miss
secsy-ssh->>API: GET /api/auth/config
API-->>secsy-ssh: OIDC issuer + client_id
secsy-ssh->>Browser: Open authorization URL
Browser->>OIDC: User logs in
OIDC->>secsy-ssh: Authorization code (localhost:18329/callback)
secsy-ssh->>OIDC: Exchange code for ID token (PKCE)
OIDC-->>secsy-ssh: ID token
secsy-ssh->>API: GET /api/keys/{id}/my-restrictions
API-->>secsy-ssh: Restriction set (require_reason, etc.)
opt Reason required
secsy-ssh->>User: Prompt for reason
User-->>secsy-ssh: "deployment"
end
secsy-ssh->>API: POST /api/keys/{id}/sign (pubkey, principal, reason)
API->>HSM: Sign certificate
HSM-->>API: Signed certificate
API-->>secsy-ssh: SSH certificate
secsy-ssh->>secsy-ssh: Cache certificate
secsy-ssh->>secsy-ssh: Write cert to memfd (in-memory only)
secsy-ssh->>SSH: ssh -i key -o CertificateFile=/proc/self/fd/3 user@host
end
SSH-->>User: Connected
cd server
go build -o secsy-ssh ./cmd/secsy-ssh
sudo cp secsy-ssh /usr/local/bin/
Create ~/.ssh/secsy.yaml:
api_url: "https://secsy-pki.example.com:8443"
# insecure_skip_verify: true # only for development with self-signed certs
# Basic usage — opens browser for OIDC login, signs key, connects
secsy-ssh --ca "Root CA" user@host
# With a specific port
secsy-ssh --ca "Root CA" user@host -p 2222
# Provide a reason (if the CA requires it)
secsy-ssh --ca "Root CA" --reason "deployment" user@host
# Skip the certificate cache
secsy-ssh --ca "Root CA" --nocache user@host
# All standard ssh options work
secsy-ssh --ca "Root CA" user@host -L 8080:localhost:80 -N
| Flag | Description |
|---|---|
--ca <name> | CA to sign with (required, matched by label or ID) |
--reason <text> | Reason for the certificate (prompted interactively if CA requires it) |
--nocache | Skip the certificate cache, always request a new one |
All other arguments are passed directly to ssh.
secsy-ssh searches ~/.ssh/ for the first available key with a corresponding .pub file. Preferred order:
id_ed25519id_ecdsaid_rsaid_* file (e.g., id_ed25519_work, id_ecdsa_deploy)The corresponding .pub file is sent to the API for signing.
Signed certificates are cached in $XDG_RUNTIME_DIR/secsy-ssh/ (or /tmp/secsy-ssh-<uid>/ if XDG_RUNTIME_DIR is not set). The cache key is a SHA-256 hash of all CLI arguments.
ValidBefore field)--nocache to bypass the cachememfd_create(2) and passed to ssh via /proc/self/fd/3. No certificate data touches the filesystem (except the cache, which uses 0600 permissions in the runtime directory).user@host or -l user) is automatically included as the certificate principal.require_reason, the user is prompted interactively. If force_key_id_email is set, the key ID is automatically set to the user's email from the OIDC token.The OIDC provider must be configured to allow the redirect URI http://localhost:18329/* for the secsy-ssh callback. For KeyCloak:
# Add redirect URI via admin API
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://keycloak:8080/admin/realms/secsy-pki/clients/$CLIENT_UUID" \
-H "Content-Type: application/json" \
-d '{"redirectUris": ["https://your-server:8443/*", "http://localhost:18329/*"]}'
# 1. Configure
cat > ~/.ssh/secsy.yaml << 'EOF'
api_url: "https://secsy-pki.example.com:8443"
EOF
# 2. Generate an SSH key (if you don't have one)
ssh-keygen -t ed25519
# 3. Connect — browser opens for login, cert is signed, SSH connects
secsy-ssh --ca "Production CA" [email protected]
# 4. Second connection — uses cached cert, no browser needed
secsy-ssh --ca "Production CA" [email protected]
Provision the root CA key on a YubiHSM:
cd terraform
cp terraform.tfvars.example terraform.tfvars # edit as needed
terraform init
terraform apply
The Ed25519 root key is generated on the YubiHSM via yubihsm-shell and imported into Terraform state:
terraform import 'pkcs11_key_pair.root_ca_ed25519[0]' 'key-label/key-id-hex'
yubihsm_pkcs11.conf:
# Direct USB (no connector daemon needed)
connector = yhusb://
All endpoints under /api/ require authentication (Bearer token or Basic auth for root).
| Method | Path | Description |
|---|---|---|
| GET | /api/health | Health check |
| GET | /api/auth/config | OIDC discovery config |
| GET | /api/me | Current user info |
| GET | /api/keys | List keys |
| POST | /api/keys | Create key (omit pkcs11_uri to generate on HSM) |
| GET | /api/keys/{id} | Get key |
| DELETE | /api/keys/{id} | Delete key |
| GET | /api/keys/{id}/public-key | Export public key (?format=pem default, ?format=ssh) |
| POST | /api/keys/{id}/sign | Sign an SSH certificate |
| POST | /api/keys/{id}/sign-x509 | Sign an X.509 certificate from CSR |
| POST | /api/parse-csr | Parse and display CSR contents |
| GET | /api/keys/{id}/my-restrictions | Get effective restriction set (?format=ssh|x509) |
| GET | /api/groups | List groups |
| POST | /api/groups | Create group |
| DELETE | /api/groups/{id} | Delete group |
| GET | /api/groups/{id}/members | List group members |
| POST | /api/groups/{id}/members | Add member |
| DELETE | /api/groups/{id}/members/{sub} | Remove member |
| GET | /api/keys/{id}/permissions | List permissions |
| POST | /api/keys/{id}/permissions | Grant permission (with SSH/X.509 restriction sets) |
| DELETE |
curl -sk -u root:password -X POST https://localhost:8443/api/keys/{key_id}/sign \
-H 'Content-Type: application/json' \
-d '{
"public_key": "ssh-ed25519 AAAA... user@host",
"cert_type": "user",
"principals": ["username"],
"valid_before": "+52w",
"key_id": "user@org",
"reason": "deployment"
}'
curl -sk -u root:password -X POST https://localhost:8443/api/keys/{key_id}/sign-x509 \
-H 'Content-Type: application/json' \
-d '{
"csr": "-----BEGIN CERTIFICATE REQUEST-----\n...\n-----END CERTIFICATE REQUEST-----",
"valid_before": "+365d"
}'
All certificate parameters (subject, SANs, extensions) are taken from the CSR.
cd server
go test -tags sqlite ./internal/...
When a YubiHSM is connected, additional tests cover PKCS#11 signing and HSM operations:
go test -tags "sqlite yubihsm" ./internal/...
These tests are automatically skipped in CI (which uses SoftHSM instead).
Run with Docker (KeyCloak + OpenSSH):
cd server
./scripts/run-integration-tests.sh
Validate certificate-based authentication:
cd test-ssh
docker build -t secsy-pki-test-sshd .
docker run -d -p 2222:22 secsy-pki-test-sshd
ssh -i id_test -o CertificateFile=id_test-cert.pub -p 2222 testuser@localhost
| Permission | Description |
|---|---|
SIGN_CERTIFICATE | Sign certificates through this CA (subject to restriction sets) |
MANAGE_PERMISSIONS | Grant/revoke permissions and assign restriction sets |
CONFIGURE_CA | Create/edit/delete restriction sets and set the CA default |
Permissions can be assigned to individual users (by OIDC subject) or to groups. The root user bypasses all permission checks.
Restriction sets enforce policies on certificate signing. SSH and X.509 restriction sets are separate types stored in dedicated database tables. Priority: user-specific > group-specific > key default.
Each key has separate default SSH and X.509 restriction sets. New keys default to "Disallow all signatures" for both types. Built-in restriction sets:
deny_all: true)Restriction sets can be global (not associated with any key) or key-specific.
| Field | Effect |
|---|---|
max_validity_secs | Maximum certificate lifetime |
deny_all | Block all SSH signing |
allowed_principals | Only these principals (* for any) |
allowed_cert_types | Only user, host, or both |
force_key_id_email | Key ID forced to user email |
require_reason | User must provide a reason (appended to key ID as email (reason)) |
deny_extensions | No custom extensions allowed |
allowed_extensions | Only these extensions (when not denied) |
deny_critical_options | No critical options allowed |
max_valid_after_offset | Max seconds into the future for valid-after |
| Field | Effect |
|---|---|
max_validity_secs | Maximum certificate lifetime |
deny_all | Block all X.509 signing |
allowed_key_usages | Only these key usages (e.g. digitalSignature, keyEncipherment) |
allowed_ext_key_usages | Only these extended key usages (e.g. serverAuth, clientAuth) |
allowed_san_types | Only these SAN types (dns, ip, email) |
allowed_san_patterns | Only SANs matching these patterns (e.g. *.example.com, 10.0.0.0/8) |
allowed_subject_fields | Only these subject fields (CN, O, OU, C, ST, L) |
max_path_length | Maximum CA path length (-1 = no CA certs) |
deny_ca | Cannot issue CA certificates |
MIT
| Container image and Helm chart, SQLite/PostgreSQL, multi-replica HA, self-issued serving TLS |
| Day-2 operations | The operator runbook, incident response, observability, backups, expiry monitoring, the illustrated console walkthrough |
| Compliance & audit readiness | The RFC 3647 CP/CPS and the CA/B Forum + WebTrust control mapping |
| Development & testing | Benchmarks, coverage, fuzzing, chaos testing, the authorization matrix, supply-chain security |
go:embed/console//api/keys/{id}/permissions| Revoke permission |
| GET | /api/restriction-sets | List all restriction sets |
| POST | /api/restriction-sets | Create global restriction set |
| GET | /api/keys/{id}/restriction-sets | List restriction sets for a key (includes global) |
| POST | /api/keys/{id}/restriction-sets | Create key-specific restriction set |
| PUT | /api/restriction-sets/{id} | Update restriction set |
| DELETE | /api/restriction-sets/{id} | Delete restriction set |
| PUT | /api/keys/{id}/default-restriction-set | Set key default (type: ssh|x509) |
| GET | /api/audit-log | Sign operations audit log (filterable, paginated) |
| GET | /api/access-log | API access log (paginated) |
| GET | /api/hsm/info | HSM device info and audit status |
| GET | /api/hsm/attestation | Export device attestation certificate (PEM) |
| GET | /api/hsm/audit-log | HSM audit log entries from database |
| GET | /api/hsm/signed-audit-log | Signed HSM audit log (signature over last hash) |
| GET | /api/hsm/combined-audit-log | Combined log with sign operations and key attestations |
| POST | /api/hsm/provision-audit | Enable forced audit logging (irreversible) |
| POST | /api/hsm/factory-reset | Factory reset the YubiHSM |