Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

kipuka (Hawaiian) — an area of older land surrounded by younger lava flows; an island of stability in a landscape of constant change.

kipuka is a Rust-based EST (Enrollment over Secure Transport) server that issues and renews X.509 certificates at scale. It targets environments where compliance, high availability, and hardware-backed key protection are non-negotiable: government enclaves, regulated enterprise networks, IoT device fleets, and zero-trust architectures.

What kipuka does

  • Certificate enrollment and renewal — full RFC 7030 EST implementation including /cacerts, /simpleenroll, /simplereenroll, /serverkeygen, /fullcmc, and /csrattrs.
  • Multi-CA high availability — route enrollment requests to different Certificate Authorities based on EST labels, with automatic failover.
  • HSM integration — PKCS #11 support via cryptoki for hardware-backed CA signing keys (Thales Luna, YubiHSM 2, SoftHSM, and others).
  • One-time password enrollment — generate and validate OTPs through the admin API for initial device bootstrapping.
  • Profile-based routing — EST labels map incoming requests to specific CA configurations, certificate profiles, and policy sets.
  • Audit logging — structured, tamper-evident logs suitable for NIAP and CA/Browser Forum audit requirements.
  • Dogtag PKI back-end — delegate signing to a Red Hat Certificate System / Dogtag PKI instance when full CA lifecycle management is needed.
  • CoAP transport — constrained-device enrollment over RFC 7252 (CoAP) for IoT environments with limited bandwidth.

What kipuka does not do

  • Full CA lifecycle management — kipuka is an enrollment front-end, not a complete CA. It delegates signing to local key material, an HSM, or a back-end CA such as Dogtag PKI. It does not manage CRL publication, OCSP responders, or CA key ceremonies.
  • ACME — kipuka implements EST, not ACME (RFC 8555). Use a dedicated ACME server if your clients speak that protocol.
  • Certificate transparency — kipuka does not submit pre-certificates to CT logs. Pair it with a CT-aware CA if your trust model requires it.
  • End-entity key management — private keys for enrolled devices are generated client-side (or via /serverkeygen). kipuka never stores end-entity private keys beyond the lifetime of a single request.

Technology stack

ComponentCrate / LibraryRole
LanguageRust (edition 2021)Memory safety, performance, fearless concurrency
HTTP frameworkaxumAsync request routing and middleware
TLSrustlsTLS 1.2/1.3 termination with certificate-based client auth
DatabasesqlxAsync database access (SQLite, PostgreSQL)
ASN.1 / X.509syntaDER/BER encoding, CSR parsing, certificate construction
PKCS #11cryptokiHSM integration for hardware-backed signing

Standards implemented

kipuka targets conformance with the following specifications:

StandardScope
RFC 7030Enrollment over Secure Transport (EST)
RFC 8951Clarifications and updates to EST
RFC 5272Certificate Management over CMS (Full CMC)
RFC 8739Short-term, automatically renewed certificates
RFC 7252Constrained Application Protocol (CoAP) transport
CA/Browser Forum Baseline RequirementsTLS certificate issuance policy
NIAP CA Protection Profile v2.0Common Criteria for Certificate Authorities
FIPS 140-3Cryptographic module validation (via HSM)

Who this documentation is for

This book is organized for three audiences:

  1. Operators — you deploy, configure, and maintain kipuka in production. Start with the Quick Start and then read the Operator Guide for the full configuration reference, HA setup, HSM integration, and audit logging.

  2. API integrators — you write client software that enrolls certificates through kipuka. The API Reference documents every endpoint, request format, and response code. The Your First Certificate walkthrough gives you a working example in five minutes.

  3. Contributors — you want to build kipuka from source, run the test suite, or submit patches. The Developer Guide covers the workspace layout, architecture decisions, database migrations, and contribution process.

Quick navigation

I want to …Start here
Run kipuka in a container in under two minutesInstallation
Issue my first certificateYour First Certificate
Understand every configuration knobConfiguration Reference
Connect an HSMHSM Integration
Set up multi-CA high availabilityHigh Availability
Integrate with Dogtag PKIDogtag PKI Integration
Review RFC conformance detailsRFC Support Reference
Prepare for a NIAP evaluationNIAP CA Protection Profile
Read the EST API specificationEST Endpoints
Build from source and run testsDevelopment Setup

Installation

This page covers every way to get kipuka running: pulling a pre-built container image, building from source, and installing as a systemd service.

Prerequisites

RequirementMinimum versionNotes
Rust toolchain1.88+Only needed when building from source
OpenSSL dev headers1.1.1+ or 3.xNeeded for the build; not linked at runtime (kipuka uses rustls)
SQLite or PostgreSQLSQLite 3.35+ / PG 14+Database for OTP state and audit records

Container (fastest)

Pre-built images are published to the kipuka container registry for both x86_64 and aarch64:

# x86_64 (default)
podman pull registry.kipuka.dev/kipuka:latest

# Apple Silicon / ARM servers
podman pull registry.kipuka.dev/kipuka:latest-arm64

Run the container with a bind-mounted configuration directory:

podman run -d \
  --name kipuka \
  -p 9443:9443 \
  -v /etc/kipuka:/etc/kipuka:ro \
  -v /var/lib/kipuka:/var/lib/kipuka:rw \
  registry.kipuka.dev/kipuka:latest \
  kipuka --config /etc/kipuka/kipuka.toml

The container image ships a minimal filesystem. All state lives in /var/lib/kipuka (database, OTP records) and all configuration is read from /etc/kipuka. TLS certificates and CA key material are expected under /etc/kipuka/tls/ and /etc/kipuka/ca/ respectively.

Tip: For Kubernetes or OpenShift deployments, mount the configuration as a ConfigMap and secrets (TLS keys, CA keys) as Secret volumes.

Building from source

Clone the repository and build in release mode:

git clone https://codeberg.org/czinda/kipuka.git
cd kipuka
cargo build --release

The workspace contains six crates:

CratePurpose
kipuka-estCore EST server, HTTP handlers, TLS, database
kipuka-hsmPKCS #11 / HSM integration via cryptoki
kipuka-otpOne-time password generation and validation
kipuka-utilShared utilities (ASN.1 helpers, configuration parsing)
kipuka-dogtagDogtag PKI back-end connector
kipuka-coapCoAP (RFC 7252) transport layer

The final binary is at target/release/kipuka.

OS-specific build dependencies

Fedora / RHEL / CentOS Stream

sudo dnf install openssl-devel clang cmake pkg-config

Debian / Ubuntu

sudo apt install libssl-dev clang cmake pkg-config

macOS

brew install openssl cmake
export OPENSSL_DIR=$(brew --prefix openssl)

Installing the binary

Copy the release binary to a location on $PATH:

sudo cp target/release/kipuka /usr/local/bin/
sudo chmod 755 /usr/local/bin/kipuka

Verify the installation:

kipuka --version

systemd service

Create a dedicated service account:

sudo useradd -r -s /sbin/nologin -d /var/lib/kipuka kipuka
sudo mkdir -p /var/lib/kipuka /var/log/kipuka /etc/kipuka
sudo chown kipuka:kipuka /var/lib/kipuka /var/log/kipuka

Install the unit file at /etc/systemd/system/kipuka.service:

[Unit]
Description=kipuka EST enrollment server
Documentation=https://codeberg.org/czinda/kipuka
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=kipuka
Group=kipuka
ExecStart=/usr/local/bin/kipuka --config /etc/kipuka/kipuka.toml
Restart=on-failure
RestartSec=5s

# Security hardening
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/lib/kipuka /var/log/kipuka
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=kipuka

[Install]
WantedBy=multi-user.target

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now kipuka
sudo systemctl status kipuka

Note: The CAP_NET_BIND_SERVICE capability allows kipuka to bind to port 443 without running as root. If you run on a high port (e.g., 9443) you can remove both CapabilityBoundingSet and AmbientCapabilities lines.

Running tests

The full test suite runs against an in-memory SQLite database and does not require any external services:

cargo test

To run tests for a specific crate:

cargo test -p kipuka-est
cargo test -p kipuka-hsm

Integration tests that require a running EST server are gated behind a feature flag:

cargo test --features integration

Next: First Run walks you through creating a minimal configuration and starting the server.

First Run

This guide takes you from a freshly installed kipuka binary to a running EST server responding to /cacerts requests. By the end you will have a working server with a self-signed CA suitable for development and testing.

Step 1: Create directories and service account

sudo mkdir -p /etc/kipuka/{tls,ca}
sudo mkdir -p /var/lib/kipuka
sudo mkdir -p /var/log/kipuka
sudo useradd -r -s /sbin/nologin -d /var/lib/kipuka kipuka
sudo chown kipuka:kipuka /var/lib/kipuka /var/log/kipuka
PathPurpose
/etc/kipuka/tls/Server TLS certificate and private key
/etc/kipuka/ca/CA certificate and signing key
/var/lib/kipuka/Database files, OTP state
/var/log/kipuka/Audit logs (when file-based logging is enabled)

Step 2: Generate test certificates

The repository includes a helper script that creates a complete test PKI:

./contrib/local-dev/setup-ca.sh

This generates a root CA, a server TLS certificate, and a client certificate under contrib/local-dev/pki/. Copy the relevant files:

sudo cp contrib/local-dev/pki/ca.pem /etc/kipuka/ca/ca.pem
sudo cp contrib/local-dev/pki/ca-key.pem /etc/kipuka/ca/ca-key.pem
sudo cp contrib/local-dev/pki/server.pem /etc/kipuka/tls/server.pem
sudo cp contrib/local-dev/pki/server-key.pem /etc/kipuka/tls/server-key.pem

Manual certificate generation

If you prefer to create certificates by hand:

# Root CA
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout /etc/kipuka/ca/ca-key.pem \
  -out /etc/kipuka/ca/ca.pem \
  -days 3650 -nodes \
  -subj "/CN=kipuka Test CA"

# Server TLS certificate
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout /etc/kipuka/tls/server-key.pem \
  -out /tmp/server.csr -nodes \
  -subj "/CN=localhost"

openssl x509 -req -in /tmp/server.csr \
  -CA /etc/kipuka/ca/ca.pem \
  -CAkey /etc/kipuka/ca/ca-key.pem \
  -CAcreateserial \
  -out /etc/kipuka/tls/server.pem \
  -days 365 \
  -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1")

rm /tmp/server.csr

Set restrictive permissions on key material:

sudo chmod 600 /etc/kipuka/ca/ca-key.pem /etc/kipuka/tls/server-key.pem
sudo chown kipuka:kipuka /etc/kipuka/ca/* /etc/kipuka/tls/*

Step 3: Write a minimal configuration

Create /etc/kipuka/kipuka.toml:

[server]
listen = "0.0.0.0:9443"

[tls]
cert = "/etc/kipuka/tls/server.pem"
key = "/etc/kipuka/tls/server-key.pem"

[tls.client_auth]
# Trust anchor for EST client certificate authentication.
# Clients presenting a certificate signed by this CA are
# permitted to re-enroll without an OTP.
trust_anchors = ["/etc/kipuka/ca/ca.pem"]

[db]
# SQLite for development.  Use a PostgreSQL URL for production.
url = "sqlite:///var/lib/kipuka/kipuka.db?mode=rwc"

[[ca]]
id = "default"
cert = "/etc/kipuka/ca/ca.pem"
key = "/etc/kipuka/ca/ca-key.pem"

# Optional: restrict which subject names this CA will sign.
# allowed_subjects = ["CN=*"]

# Optional: set a default certificate validity period.
# validity_days = 365

Production note: For production deployments, replace the [db] URL with a PostgreSQL connection string and store CA keys in an HSM. See the Configuration Reference for the full set of options.

Step 4: Run database migrations

kipuka manages its own schema. Run the migration command before the first start:

sudo -u kipuka kipuka migrate --config /etc/kipuka/kipuka.toml

Expected output:

Applied 3 migrations to sqlite:///var/lib/kipuka/kipuka.db

Step 5: Start the server

Start kipuka in the foreground to verify everything works:

sudo -u kipuka kipuka --config /etc/kipuka/kipuka.toml

You should see log output similar to:

2026-06-24T12:00:00.000Z  INFO kipuka::server: kipuka v0.1.0 starting
2026-06-24T12:00:00.001Z  INFO kipuka::tls: TLS configured, client auth enabled
2026-06-24T12:00:00.002Z  INFO kipuka::ca: Loaded CA "default" (CN=kipuka Test CA)
2026-06-24T12:00:00.003Z  INFO kipuka::db: Database connected (sqlite)
2026-06-24T12:00:00.004Z  INFO kipuka::server: Listening on 0.0.0.0:9443

Press Ctrl+C to stop. For long-running deployments, use the systemd service instead.

Step 6: Verify the EST endpoint

The /cacerts endpoint returns the CA certificate(s) without requiring client authentication. Use it to confirm the server is responding:

curl -sk https://localhost:9443/.well-known/est/cacerts \
  | base64 -d \
  | openssl pkcs7 -inform DER -print_certs

You should see the PEM-encoded CA certificate:

subject=CN = kipuka Test CA
issuer=CN = kipuka Test CA
-----BEGIN CERTIFICATE-----
MIIBkTCB+...
-----END CERTIFICATE-----

If this works, your server is ready to issue certificates.

Logging

kipuka uses Rust’s tracing framework. Control verbosity with the RUST_LOG environment variable:

LevelWhat you see
RUST_LOG=errorOnly errors
RUST_LOG=warnErrors and warnings
RUST_LOG=infoStartup, shutdown, enrollment events (default)
RUST_LOG=debugRequest/response details, TLS handshake info
RUST_LOG=traceFull ASN.1 dumps, raw bytes, internal state

To set the log level when running directly:

RUST_LOG=debug kipuka --config /etc/kipuka/kipuka.toml

For the systemd service, add an environment override:

sudo systemctl edit kipuka
[Service]
Environment="RUST_LOG=debug"

Next: Your First Certificate walks through the complete enrollment flow — generating an OTP, submitting a CSR, and extracting the signed certificate.

Your First Certificate

This guide walks through a complete EST enrollment cycle: generating a one-time password, creating a certificate signing request, enrolling through the EST endpoint, and verifying the result. By the end you will have a signed X.509 certificate issued by your kipuka server.

Prerequisites: A running kipuka instance from the First Run guide and the CA certificate saved as ca.pem.

Step 1: Generate a one-time password

kipuka authenticates initial enrollment requests with a one-time password (OTP). Generate one through the admin API:

curl -sk -X POST https://localhost:9443/admin/otp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "test-device"}'

Response:

{
  "entity_id": "test-device",
  "otp": "a1b2c3d4e5f6",
  "expires_at": "2026-06-25T12:00:00Z"
}

Save the OTP value:

OTP="a1b2c3d4e5f6"

Note: The admin API bearer token is configured in kipuka.toml under [admin]. See the Admin API Reference for details on token management.

Step 2: Generate a CSR

Create an EC P-256 key pair and a certificate signing request:

openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout client.key \
  -out client.csr \
  -nodes \
  -subj "/CN=test-device"

This produces two files:

FileContents
client.keyPrivate key (stays on the device, never sent to the server)
client.csrCertificate signing request (sent to kipuka)

Tip: For production IoT devices, generate the key pair in a secure element or TPM and export only the CSR.

Step 3: Enroll with OTP

Submit the CSR to the EST /simpleenroll endpoint, authenticating with the entity ID and OTP as HTTP Basic credentials:

curl -sk \
  --cacert ca.pem \
  -u "test-device:${OTP}" \
  --data-binary @client.csr \
  -H "Content-Type: application/pkcs10" \
  -o cert.p7 \
  https://localhost:9443/.well-known/est/simpleenroll

The server returns a PKCS #7 (CMS) envelope containing the signed certificate in DER format, saved here as cert.p7.

If enrollment succeeds, the HTTP response code is 200. Common error codes:

CodeMeaning
401Invalid or expired OTP
400Malformed CSR
403Subject name not permitted by CA policy
503Back-end CA unavailable

Step 4: Extract the certificate

Convert the PKCS #7 envelope to PEM:

openssl pkcs7 -inform DER -in cert.p7 -print_certs -out client.pem

You now have the signed certificate in client.pem.

Step 5: Verify the certificate

Inspect the certificate details:

openssl x509 -in client.pem -text -noout

Expected output (abbreviated):

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: ...
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: CN = kipuka Test CA
        Validity
            Not Before: Jun 24 12:00:00 2026 GMT
            Not After : Jun 24 12:00:00 2027 GMT
        Subject: CN = test-device
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                ...

Verify the certificate chains back to your CA:

openssl verify -CAfile ca.pem client.pem
client.pem: OK

Re-enrollment

Once a device holds a valid certificate, it can renew without an OTP by authenticating with TLS client certificate authentication. The EST /simplereenroll endpoint accepts the same CSR format:

# Generate a new CSR (optionally with a new key pair)
openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout client-new.key \
  -out client-new.csr \
  -nodes \
  -subj "/CN=test-device"

# Re-enroll using the existing certificate for authentication
curl -sk \
  --cacert ca.pem \
  --cert client.pem \
  --key client.key \
  --data-binary @client-new.csr \
  -H "Content-Type: application/pkcs10" \
  -o cert-new.p7 \
  https://localhost:9443/.well-known/est/simplereenroll

Extract the renewed certificate:

openssl pkcs7 -inform DER -in cert-new.p7 -print_certs -out client-new.pem

Key rotation: The example above generates a fresh key pair during re-enrollment. This is recommended practice. If your use case requires keeping the same key, omit -newkey and pass the existing key with -key client.key.

EST labels: profile-based routing

kipuka supports EST labels (also called path segments) to route enrollment requests to different CA configurations or certificate profiles. The label appears in the URL path between /.well-known/est/ and the operation name.

For example, if you configure a label called iot-devices that maps to a dedicated CA and profile:

[[ca]]
id = "iot"
cert = "/etc/kipuka/ca/iot-ca.pem"
key = "/etc/kipuka/ca/iot-ca-key.pem"
label = "iot-devices"
validity_days = 90

Clients enroll against the labeled path:

curl -sk \
  --cacert iot-ca.pem \
  -u "sensor-001:${OTP}" \
  --data-binary @sensor.csr \
  -H "Content-Type: application/pkcs10" \
  -o sensor-cert.p7 \
  https://localhost:9443/.well-known/est/iot-devices/simpleenroll

The /cacerts endpoint also respects labels, returning only the CA certificate(s) for that label:

curl -sk https://localhost:9443/.well-known/est/iot-devices/cacerts \
  | base64 -d \
  | openssl pkcs7 -inform DER -print_certs

Labels are a powerful mechanism for multi-tenant and multi-profile deployments. See EST Labels in the Operator Guide for the complete configuration reference.

Summary

You have completed a full EST enrollment lifecycle:

  1. Generated a one-time password via the admin API
  2. Created a CSR with an EC P-256 key pair
  3. Enrolled through /simpleenroll with OTP authentication
  4. Extracted and verified the signed certificate
  5. Learned how to re-enroll with certificate-based authentication
  6. Explored label-based routing for multi-CA deployments

Next steps:

Configuration Reference

This document provides a complete reference for kipuka.toml, the main configuration file for the kipuka EST server.

Configuration Sections

[server]

Core server settings for HTTP/HTTPS listeners and runtime behavior.

KeyTypeDefaultDescription
listenstring"0.0.0.0:8443"Address and port for the main EST endpoint (HTTPS).
admin_listenstring"127.0.0.1:9443"Address and port for the administrative API. Bind to localhost by default for security.
workersintegernum_cpusNumber of worker threads. Defaults to the number of CPU cores.
max_body_sizestring"1MB"Maximum allowed request body size. Accepts suffixes: B, KB, MB, GB.
shutdown_timeoutstring"30s"Grace period for connection draining during shutdown. Accepts suffixes: s, m, h.

[tls]

TLS configuration for the main EST endpoint.

KeyTypeDefaultDescription
certstringrequiredPath to PEM-encoded server certificate.
keystringrequiredPath to PEM-encoded private key for the server certificate.
min_versionstring"1.2"Minimum TLS protocol version. Valid values: "1.2", "1.3".
cipher_suitesarray of strings(TLS library defaults)Explicit list of allowed cipher suites. Omit to use secure defaults.

[tls.client_auth]

Mutual TLS (mTLS) client authentication settings.

KeyTypeDefaultDescription
trust_anchorsstringrequiredPath to PEM file containing trusted CA certificates for client authentication.
modestring"required"Client certificate verification mode: required, optional, or none.

[db]

Database connection settings.

KeyTypeDefaultDescription
urlstring"sqlite://kipuka.db"Database connection URL. Supports sqlite://, postgres://, and mysql:// (MariaDB) schemes.
max_connectionsinteger5Maximum number of database connections in the pool.
connect_timeoutstring"5s"Timeout for establishing database connections.
auto_migratebooleantrueAutomatically apply schema migrations on startup.

[[ca]]

Certificate Authority (CA) definitions. Multiple [[ca]] sections define multiple CAs.

KeyTypeDefaultDescription
idstringrequiredUnique identifier for this CA. Referenced by EST labels.
namestringrequiredHuman-readable CA name.
certstringrequiredPath to PEM-encoded CA certificate.
keystringrequiredPath to PEM-encoded CA private key (or HSM key reference).
chainstringoptionalPath to PEM-encoded intermediate chain (if applicable).
validity_daysinteger365Default validity period for issued certificates (days).
max_validity_daysinteger398Maximum allowed validity period. RFC 5280 and NIAP recommend 398 days max.
default_key_usagearray of strings["digitalSignature", "keyEncipherment"]Default X.509 Key Usage extensions.
default_ext_key_usagearray of strings["serverAuth"]Default Extended Key Usage OIDs.
hsm_slotintegeroptionalHSM slot number if this CA key is stored in an HSM.

[est]

EST protocol feature flags and global settings.

KeyTypeDefaultDescription
base_pathstring"/.well-known/est"Base URL path for EST endpoints.
cacertsbooleantrueEnable /cacerts endpoint (RFC 7030 section 4.1).
simpleenrollbooleantrueEnable /simpleenroll endpoint (initial certificate enrollment).
simplereenrollbooleantrueEnable /simplereenroll endpoint (certificate renewal).
fullcmcbooleanfalseEnable /fullcmc endpoint (full CMC protocol).
serverkeygenbooleanfalseEnable /serverkeygen endpoint (server-generated key pairs).
csrattrsbooleantrueEnable /csrattrs endpoint (CSR attribute hints).
csr_attributesarray of strings[]OIDs to return in /csrattrs response. Empty means no specific attributes.
retry_afterinteger60Seconds to return in Retry-After header for pending requests.

[[est.label]]

EST label definitions for CA-specific endpoints. Multiple [[est.label]] sections define multiple labels.

KeyTypeDefaultDescription
namestringrequiredLabel name. Appears in URL as /.well-known/est/{name}/simpleenroll.
ca_idstringrequiredID of the CA to use for this label (references [[ca]] section).
allowed_key_typesarray of strings[]Restrict key types (e.g., ["rsa-2048", "ecdsa-p256"]). Empty allows all.
required_ext_key_usagearray of strings[]Require specific EKU OIDs in CSR.
max_validity_daysinteger(inherited from CA)Override CA’s max validity for this label.
require_sanbooleantrueRequire Subject Alternative Name extension in CSR.
subject_patternstringoptionalRegex pattern for validating CSR subject DN.

[hsm]

Hardware Security Module (HSM) configuration.

KeyTypeDefaultDescription
librarystringrequiredPath to PKCS#11 library (e.g., /usr/lib/softhsm/libsofthsm2.so).
slotintegeroptionalDefault HSM slot number. Can be overridden per CA.
token_labelstringoptionalToken label for slot selection.
pinstringoptionalHSM PIN. NOT RECOMMENDED (use pin_env or pin_file instead).
pin_envstringoptionalEnvironment variable containing the HSM PIN.
pin_filestringoptionalPath to file containing the HSM PIN (first line, newline stripped).

[otp]

One-Time Password (OTP) authentication for initial enrollment.

KeyTypeDefaultDescription
enabledbooleanfalseEnable OTP authentication.
token_lengthinteger20Length of generated OTP tokens. NIAP requires >= 16 characters.
default_ttlstring"24h"Default OTP validity period.
max_usesinteger1Maximum times an OTP can be used before expiration.
hash_algorithmstring"argon2id"Password hashing algorithm: argon2id, bcrypt, or sha256-hmac.
max_failuresinteger5Maximum failed OTP attempts before lockout.
failure_windowstring"15m"Time window for counting failed attempts.
lockout_durationstring"30m"Duration of account lockout after exceeding max_failures.

[audit]

Audit logging configuration.

KeyTypeDefaultDescription
filestringoptionalPath to audit log file. JSON Lines format.
syslogstringoptionalSyslog server URL (e.g., tcp+tls://syslog.example.com:6514).
syslog_facilitystring"local0"Syslog facility for audit events.
eventsarray of strings(all events)Filter events to log. Omit to log all. Examples: enroll, renew, reject.
include_cert_databooleanfalseInclude full certificate PEM in audit logs. Increases log volume.

[ha]

High Availability configuration for CA failover.

KeyTypeDefaultDescription
enabledbooleanfalseEnable HA mode.
strategystring"active-passive"Global failover strategy: active-passive, round-robin, weighted, latency-based.
check_intervalstring"10s"Health check interval for CAs.
failure_thresholdinteger3Consecutive failures before marking CA unhealthy.
recovery_timeoutstring"60s"Time to wait before retrying a failed CA.
check_timeoutstring"5s"Timeout for individual health checks.

[[ha.group]]

HA groups for label-specific CA failover. Multiple [[ha.group]] sections define multiple groups.

KeyTypeDefaultDescription
namestringrequiredGroup name.
ca_idsarray of stringsrequiredList of CA IDs in priority order.
strategystring(inherited from [ha])Override global HA strategy for this group.

[admin]

Administrative API configuration.

KeyTypeDefaultDescription
enabledbooleanfalseEnable the admin API.
authstring"mtls"Authentication method: mtls, bearer, or both.
trust_anchorsstringoptionalPath to PEM file with trusted CAs for admin mTLS. Required if auth includes mtls.
bearer_token_envstringoptionalEnvironment variable containing bearer token. Required if auth includes bearer.

Configuration Examples

Minimal Configuration

A basic single-CA deployment with one EST label.

[server]
listen = "0.0.0.0:8443"

[tls]
cert = "/etc/kipuka/server.pem"
key = "/etc/kipuka/server-key.pem"

[tls.client_auth]
trust_anchors = "/etc/kipuka/client-ca.pem"
mode = "required"

[db]
url = "sqlite:///var/lib/kipuka/kipuka.db"

[[ca]]
id = "main-ca"
name = "Main CA"
cert = "/etc/kipuka/ca.pem"
key = "/etc/kipuka/ca-key.pem"
validity_days = 365
max_validity_days = 398

[est]
base_path = "/.well-known/est"

[[est.label]]
name = "default"
ca_id = "main-ca"
require_san = true

Production Configuration

A realistic production deployment with HSM, HA, audit logging, admin API, OTP, and multiple CAs.

[server]
listen = "0.0.0.0:8443"
admin_listen = "127.0.0.1:9443"
workers = 8
max_body_size = "2MB"
shutdown_timeout = "60s"

[tls]
cert = "/etc/kipuka/certs/server.pem"
key = "/etc/kipuka/certs/server-key.pem"
min_version = "1.3"

[tls.client_auth]
trust_anchors = "/etc/kipuka/certs/client-ca-bundle.pem"
mode = "required"

[db]
url = "postgres://kipuka:[email protected]/kipuka?sslmode=require"
max_connections = 20
connect_timeout = "10s"
auto_migrate = true

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
slot = 0
token_label = "kipuka-prod"
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "root-ca"
name = "Production Root CA"
cert = "/etc/kipuka/ca/root-ca.pem"
key = "pkcs11:object=root-ca-key"
chain = "/etc/kipuka/ca/root-chain.pem"
validity_days = 365
max_validity_days = 398
default_key_usage = ["digitalSignature", "keyEncipherment"]
default_ext_key_usage = ["serverAuth", "clientAuth"]
hsm_slot = 0

[[ca]]
id = "backup-ca"
name = "Production Backup CA"
cert = "/etc/kipuka/ca/backup-ca.pem"
key = "pkcs11:object=backup-ca-key"
chain = "/etc/kipuka/ca/backup-chain.pem"
validity_days = 365
max_validity_days = 398
default_key_usage = ["digitalSignature", "keyEncipherment"]
default_ext_key_usage = ["serverAuth", "clientAuth"]
hsm_slot = 1

[[ca]]
id = "iot-ca"
name = "IoT Device CA"
cert = "/etc/kipuka/ca/iot-ca.pem"
key = "pkcs11:object=iot-ca-key"
validity_days = 180
max_validity_days = 180
default_key_usage = ["digitalSignature"]
default_ext_key_usage = ["clientAuth"]
hsm_slot = 2

[est]
base_path = "/.well-known/est"
cacerts = true
simpleenroll = true
simplereenroll = true
fullcmc = false
serverkeygen = false
csrattrs = true
csr_attributes = ["2.5.4.3", "2.5.4.11"]
retry_after = 120

[[est.label]]
name = "servers"
ca_id = "root-ca"
allowed_key_types = ["rsa-2048", "rsa-4096", "ecdsa-p256"]
required_ext_key_usage = ["serverAuth"]
max_validity_days = 398
require_san = true
subject_pattern = "^CN=.*\\.example\\.com$"

[[est.label]]
name = "clients"
ca_id = "root-ca"
allowed_key_types = ["ecdsa-p256", "ecdsa-p384"]
required_ext_key_usage = ["clientAuth"]
max_validity_days = 365
require_san = true

[[est.label]]
name = "iot"
ca_id = "iot-ca"
allowed_key_types = ["ecdsa-p256"]
required_ext_key_usage = ["clientAuth"]
max_validity_days = 180
require_san = false

[otp]
enabled = true
token_length = 20
default_ttl = "24h"
max_uses = 1
hash_algorithm = "argon2id"
max_failures = 5
failure_window = "15m"
lockout_duration = "30m"

[audit]
file = "/var/log/kipuka/audit.jsonl"
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local0"
events = ["enroll", "renew", "reject", "revoke"]
include_cert_data = false

[ha]
enabled = true
strategy = "active-passive"
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

[[ha.group]]
name = "production"
ca_ids = ["root-ca", "backup-ca"]
strategy = "active-passive"

[[ha.group]]
name = "iot"
ca_ids = ["iot-ca"]
strategy = "active-passive"

[admin]
enabled = true
auth = "both"
trust_anchors = "/etc/kipuka/certs/admin-ca.pem"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Notes

  • Security: Never commit kipuka.toml with plaintext secrets to version control. Use environment variables or HSM-backed keys.
  • Validation: kipuka validates the configuration at startup and reports errors for missing required fields or invalid values.
  • Reloading: Configuration changes require a server restart. Plan maintenance windows for production deployments.
  • HSM Keys: When using HSM-backed CA keys, specify key as a PKCS#11 URI (e.g., pkcs11:object=my-key) and ensure hsm_slot matches the token slot.
  • NIAP Compliance: For NIAP Common Criteria compliance, ensure otp.token_length >= 16, tls.min_version = "1.3", and ca.max_validity_days <= 398.

Certificate Authorities

kipuka supports multiple Certificate Authorities (CAs) through its [[ca]] array configuration. This architecture enables operators to serve different trust domains, key algorithms, and PKI hierarchies from a single EST server instance.

Multi-CA Architecture

Each CA in kipuka is defined by a unique id field used to bind EST labels to specific certificate authorities. Multiple CAs are configured in kipuka.toml:

[[ca]]
id = "internal-rsa"
cert = "/etc/kipuka/ca/internal-rsa-ca.crt"
key = "/etc/kipuka/ca/internal-rsa-ca.key"
chain = "/etc/kipuka/ca/internal-rsa-chain.pem"
max_validity_days = 90

[[ca]]
id = "external-ecdsa"
cert = "/etc/kipuka/ca/external-ecdsa-ca.crt"
key = "/etc/kipuka/ca/external-ecdsa-ca.key"
chain = "/etc/kipuka/ca/external-ecdsa-chain.pem"
max_validity_days = 90

Use Cases for Multiple CAs

Separate Trust Domains: Internal infrastructure certificates from one CA, external-facing services from another. This isolation prevents compromise of one domain from affecting the other.

Algorithm Migration: Run both RSA and ECDSA CAs simultaneously to support legacy clients while transitioning to modern algorithms.

Tiered Assurance Levels: Different CAs for different assurance requirements (standard validation vs. extended validation, different key sizes).

Geographic or Organizational Boundaries: Separate CAs for different regions or business units within an organization.

Development vs. Production: Dedicated CAs for testing environments prevent accidental trust of development certificates in production.

CA Lifecycle

Creating a Root CA

Generate a self-signed root CA private key and certificate:

# RSA 4096-bit root CA
openssl genrsa -out root-ca.key 4096
openssl req -new -x509 -days 7300 -key root-ca.key -out root-ca.crt \
  -subj "/C=US/O=Example Corp/CN=Example Root CA"

# ECDSA P-384 root CA
openssl ecparam -name secp384r1 -genkey -noout -out root-ca.key
openssl req -new -x509 -days 7300 -key root-ca.key -out root-ca.crt \
  -subj "/C=US/O=Example Corp/CN=Example Root CA"

Root CAs typically have 10-20 year validity periods and are kept offline. The root private key should be stored in secure offline storage (HSM, encrypted media in a safe).

Creating an Intermediate CA

Generate an intermediate CA signed by the root:

# Generate intermediate CA private key
openssl ecparam -name prime256v1 -genkey -noout -out intermediate-ca.key

# Create CSR for intermediate CA
openssl req -new -key intermediate-ca.key -out intermediate-ca.csr \
  -subj "/C=US/O=Example Corp/CN=Example Issuing CA"

# Sign intermediate CSR with root CA (5 year validity)
openssl x509 -req -in intermediate-ca.csr -CA root-ca.crt -CAkey root-ca.key \
  -CAcreateserial -out intermediate-ca.crt -days 1825 \
  -extensions v3_ca -extfile openssl-ca.cnf

The openssl-ca.cnf must include intermediate CA extensions:

[v3_ca]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always

Building the Certificate Chain

The chain file contains certificates from the issuing CA up to (but typically not including) the root:

# Create chain file (intermediate only, root excluded)
cat intermediate-ca.crt > chain.pem

# If there are multiple intermediates, order from issuing CA to root:
# cat issuing-intermediate.crt sub-intermediate.crt > chain.pem

Chain ordering is critical: the first certificate in the chain must be the issuer of the end-entity certificate, and each subsequent certificate must be the issuer of the previous one.

Configuring kipuka to Use the Intermediate

[[ca]]
id = "prod-issuing-ca"
cert = "/etc/kipuka/ca/intermediate-ca.crt"
key = "/etc/kipuka/ca/intermediate-ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 90

The /cacerts endpoint returns the intermediate certificate plus the chain (but not the root). Clients must have the root CA in their trust store independently.

Key Types and Recommendations

RSA Keys

# RSA 2048-bit (legacy compatibility)
openssl genrsa -out ca-rsa2048.key 2048

# RSA 3072-bit (transitional strength)
openssl genrsa -out ca-rsa3072.key 3072

# RSA 4096-bit (high assurance, slower)
openssl genrsa -out ca-rsa4096.key 4096

RSA 2048: Minimum for legacy compatibility. Acceptable for short-lived certificates (under 100 days). Avoid for new deployments.

RSA 3072: Equivalent security to AES-128. Reasonable choice for transitional deployments.

RSA 4096: Equivalent security to AES-256. Use for long-lived root CAs. Performance penalty for signing and verification operations.

ECDSA Keys

# NIST P-256 (secp256r1, prime256v1)
openssl ecparam -name prime256v1 -genkey -noout -out ca-p256.key

# NIST P-384 (secp384r1)
openssl ecparam -name secp384r1 -genkey -noout -out ca-p384.key

# NIST P-521 (secp521r1)
openssl ecparam -name secp521r1 -genkey -noout -out ca-p521.key

P-256 (prime256v1): Recommended for general use. Equivalent security to AES-128. Wide client support, fast operations, small certificate size.

P-384: Equivalent security to AES-192. Use for high-assurance environments. Moderate performance impact.

P-521: Equivalent security to AES-256. Use for maximum security requirements. Larger certificates and slower operations than P-256.

Algorithm Selection Guidance

For new deployments, prefer ECDSA P-256 unless specific requirements dictate otherwise:

  • Smaller key and certificate sizes reduce bandwidth
  • Faster signing and verification operations
  • Equivalent or better security than RSA 2048
  • Supported by all modern clients (TLS 1.2+, most TLS 1.3 implementations)

Use RSA 2048 only when legacy client compatibility is required (older embedded devices, Windows XP, ancient Java versions).

Use P-384 or RSA 4096 for root CAs in high-assurance environments where the performance penalty is acceptable.

HSM-Backed vs File-Based Keys

File-Based Keys

File-based keys are stored as PEM-encoded files on the filesystem:

[[ca]]
id = "file-based-ca"
cert = "/etc/kipuka/ca/ca.crt"
key = "/etc/kipuka/ca/ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 90

Advantages:

  • Simple configuration and operation
  • No additional hardware or drivers required
  • Easy backup and disaster recovery
  • Standard OpenSSL tooling for key generation

Disadvantages:

  • Private key exists in cleartext on disk (even if file permissions are restrictive)
  • Key compromise if filesystem is breached
  • No audit trail for key usage
  • Key can be copied without detection

Security Measures for File-Based Keys:

  • Store keys on encrypted filesystems
  • Use file permissions to restrict access (mode 0400, root-owned)
  • Consider encrypted key files with passphrase protection (requires manual unlock on service start)
  • Implement filesystem integrity monitoring (AIDE, Tripwire)
  • Regular key rotation

HSM-Backed Keys

HSM-backed keys reference a PKCS#11 token and slot instead of filesystem paths:

[hsm]
module = "/usr/lib64/libsofthsm2.so"
pin = "1234"

[[ca]]
id = "hsm-backed-ca"
cert = "/etc/kipuka/ca/ca.crt"
hsm_slot = 0
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 90

Advantages:

  • Private key never leaves the HSM hardware
  • Tamper-resistant hardware with physical security controls
  • Audit logging of all key usage operations
  • FIPS 140-2/140-3 compliance available
  • Key backup and recovery through HSM vendor mechanisms

Disadvantages:

  • Additional hardware cost and complexity
  • PKCS#11 driver setup and maintenance
  • Potential performance bottleneck for high-volume signing
  • Vendor lock-in for key backup/recovery
  • Operational complexity (HSM initialization, token management, PIN management)

HSM Selection Considerations:

Network HSMs (Thales Luna, Entrust nShield) provide centralized key management and high availability but introduce network dependencies.

USB HSMs (YubiHSM, Nitrokey HSM) offer lower cost and simpler deployment but limited performance and single points of failure.

Software HSMs (SoftHSM) provide PKCS#11 API compatibility for testing but offer no security advantage over file-based keys.

For production issuing CAs, network HSMs are recommended. For offline root CAs, USB HSMs stored in physically secure locations are acceptable.

Certificate Chain Configuration

The chain parameter provides the certificate chain from the issuing CA to the root (root typically excluded). This chain is returned via the /cacerts endpoint.

Chain File Format

Chain files are PEM-encoded, containing one or more certificates. Order matters:

-----BEGIN CERTIFICATE-----
[Issuing Intermediate CA Certificate]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Sub-Intermediate CA Certificate, if any]
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
[Root CA Certificate - optional, often excluded]
-----END CERTIFICATE-----

Chain Ordering Rules

  1. First certificate is the issuer of the end-entity certificates that the CA signs
  2. Each subsequent certificate is the issuer of the previous certificate
  3. Last certificate is either the root CA or the certificate directly issued by the root
  4. Root CA is often excluded from the chain (clients must have it in their trust store)

Why Exclude the Root CA

Most TLS/PKI implementations expect clients to have the root CA in their local trust store. Including the root in the chain:

  • Increases bandwidth unnecessarily
  • May cause validation issues with strict clients
  • Violates some certificate profile specifications

However, some closed environments include the root in the chain for convenience. kipuka supports both approaches.

Verifying Chain Correctness

# Verify the intermediate can be validated against the root
openssl verify -CAfile root-ca.crt intermediate-ca.crt

# Verify the complete chain
openssl verify -CAfile root-ca.crt -untrusted chain.pem end-entity.crt

Multiple Intermediate Levels

For deep hierarchies (root -> policy CA -> issuing CA):

# Build chain with two intermediates
cat issuing-ca.crt policy-ca.crt > chain.pem

# Root CA is still excluded

kipuka returns the issuing CA certificate concatenated with the chain file contents via /cacerts.

CA/B Forum Validity Limits

The CA/Browser Forum mandates maximum validity periods for publicly-trusted certificates. These limits progressively shorten:

Effective DateMaximum Validity
Current398 days
March 15, 2026200 days
March 15, 2027100 days
March 15, 202947 days

Configuring max_validity_days

The max_validity_days parameter in each [[ca]] section enforces these limits:

[[ca]]
id = "public-ca-2026"
cert = "/etc/kipuka/ca/ca.crt"
key = "/etc/kipuka/ca/ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 200  # Ready for March 2026 limit

kipuka rejects CSRs requesting validity beyond max_validity_days. Clients must request shorter lifetimes or accept the CA’s default.

Planning for Shorter Lifetimes

March 2026 (200 days): Transition to quarterly certificate renewal cycles. Most organizations can adapt existing manual processes.

March 2027 (100 days): Manual renewal becomes impractical. EST-based automation is essential. Plan for 90-day certificate lifetimes to allow renewal buffer.

March 2029 (47 days): Monthly or bi-weekly renewal cycles. Full automation required. No manual certificate issuance workflows can scale.

Why Shorter Lifetimes Drive EST Adoption

Reduced Blast Radius: Compromised keys have limited validity windows. Stolen certificates expire quickly.

Faster Cryptographic Agility: Transitioning to new algorithms (post-quantum cryptography) becomes operationally feasible when certificates renew frequently.

Improved Revocation Handling: Short lifetimes reduce reliance on CRL and OCSP infrastructure. Expired certificates are automatically untrusted.

Forcing Automation: Organizations must build robust certificate lifecycle management. This operational maturity pays dividends in incident response and compliance.

EST as the Solution: Manual CSR generation, approval workflows, and certificate installation cannot scale to 47-day lifetimes. EST provides:

  • Automated enrollment via /simpleenroll
  • Automated renewal via /simplereenroll
  • Automated trust anchor updates via /cacerts
  • Standard protocol implemented by network equipment, IoT devices, operating systems

Operational Recommendations

Set max_validity_days to 90 days for new CAs. This provides:

  • Compliance with current and near-term CA/B Forum limits
  • 30-day renewal buffer (renew at 60 days remaining)
  • Operational experience with short-lived certificates before 47-day limit

For existing CAs, plan a staged reduction:

  • 2026: 180 days
  • 2027: 90 days
  • 2029: 45 days

Use monitoring to track certificate expiration across your fleet. Alert when certificates are not renewed on schedule. EST’s /simplereenroll should be triggered automatically when 1/3 of the validity period remains.

Internal vs. Publicly-Trusted CAs

CA/B Forum limits apply only to publicly-trusted CAs (trust anchors in browser and OS trust stores). Internal CAs can use longer lifetimes:

[[ca]]
id = "internal-ca"
cert = "/etc/kipuka/ca/internal-ca.crt"
key = "/etc/kipuka/ca/internal-ca.key"
chain = "/etc/kipuka/ca/chain.pem"
max_validity_days = 365  # Internal CA, not subject to CA/B Forum

However, adopting short lifetimes even for internal CAs provides operational benefits (automation, reduced compromise windows) and prepares infrastructure for future regulatory requirements.

EST Labels

EST labels provide a powerful mechanism for operating multiple certificate profiles from a single EST server instance. This chapter covers label configuration, policy enforcement, and practical deployment patterns.

What Are EST Labels

RFC 7030 Section 3.2.2 defines EST path segments that enable certificate profile selection. The URL pattern for enrollment operations follows this structure:

/.well-known/est/<label>/simpleenroll
/.well-known/est/<label>/simplereenroll

When a client makes a request without a label (e.g., /.well-known/est/simpleenroll), the EST server applies its default certificate profile. Labels allow a single EST server to issue different types of certificates with different policies, CA issuers, and validation rules.

Common use cases include:

  • Certificate purpose separation: Different labels for TLS server certificates, client authentication certificates, and code signing certificates
  • Policy enforcement: Distinct labels enforcing different validity periods, key requirements, or subject naming constraints
  • Multi-tenant operation: Separate labels for different organizational units or trust boundaries
  • CA hierarchy management: Different labels issuing from different intermediate CAs within the same PKI

Label Configuration

Each [[est.label]] section in kipuka.toml defines a named profile. Labels are bound to specific CAs and enforce policy constraints on certificate issuance.

Full Label Syntax

[[est.label]]
# Unique label name used in EST URLs
name = "server-tls"

# CA ID reference (must match a [[ca]] entry)
ca_id = "intermediate-tls"

# Allowed CSR key types (optional)
# If omitted, all key types are accepted
allowed_key_types = ["ecdsaP256", "ecdsaP384", "rsa2048", "rsa3072"]

# Required Extended Key Usage OIDs (optional)
# Forces these EKUs into all issued certificates
required_ext_key_usage = ["serverAuth"]

# Maximum certificate validity in days (optional)
# Overrides CA default if set
max_validity_days = 398

# Require Subject Alternative Names (default: true)
require_san = true

# Subject DN validation regex (optional)
# CSRs must match this pattern to be accepted
subject_pattern = "^CN=[a-z0-9-]+\\.example\\.com(,.*)?$"

All fields except name and ca_id are optional. Without constraints, the label inherits behavior from the referenced CA.

CA Binding

Every label must reference a CA through ca_id. This CA identifier must match the id field of a [[ca]] entry defined elsewhere in kipuka.toml.

Multiple Labels, Same CA

Different labels can reference the same CA to provide different policy enforcement while using the same issuer:

[[ca]]
id = "corp-intermediate"
cert_path = "/etc/kipuka/pki/intermediate.crt"
key_path = "/etc/kipuka/pki/intermediate.key"
default_validity_days = 365

[[est.label]]
name = "server"
ca_id = "corp-intermediate"
required_ext_key_usage = ["serverAuth"]
max_validity_days = 398

[[est.label]]
name = "client"
ca_id = "corp-intermediate"
required_ext_key_usage = ["clientAuth"]
max_validity_days = 90
require_san = false

Both labels issue certificates from the same intermediate CA but enforce different EKUs, validity periods, and SAN requirements.

Multiple Labels, Different CAs

Labels can reference different CAs to support complex PKI hierarchies:

[[ca]]
id = "public-tls-ca"
cert_path = "/etc/kipuka/pki/public-tls-intermediate.crt"
key_path = "/etc/kipuka/pki/public-tls-intermediate.key"

[[ca]]
id = "internal-device-ca"
cert_path = "/etc/kipuka/pki/internal-device-intermediate.crt"
key_path = "/etc/kipuka/pki/internal-device-intermediate.key"

[[est.label]]
name = "public-server"
ca_id = "public-tls-ca"
required_ext_key_usage = ["serverAuth"]

[[est.label]]
name = "iot-device"
ca_id = "internal-device-ca"
required_ext_key_usage = ["clientAuth"]
max_validity_days = 30

This configuration issues public TLS certificates from one CA and private IoT device certificates from another.

Key Type Restrictions

The allowed_key_types field restricts which cryptographic key algorithms are accepted in certificate signing requests. This prevents weak key types or enforces organizational cryptographic policies.

Supported Key Type Values

allowed_key_types = [
    "rsa2048",    # RSA with 2048-bit modulus
    "rsa3072",    # RSA with 3072-bit modulus
    "rsa4096",    # RSA with 4096-bit modulus
    "ecdsaP256",  # ECDSA with NIST P-256 curve
    "ecdsaP384",  # ECDSA with NIST P-384 curve
    "ecdsaP521",  # ECDSA with NIST P-521 curve
]

If allowed_key_types is omitted, the label accepts any supported key type. An empty array [] rejects all requests.

Example: ECDSA-Only Label

[[est.label]]
name = "modern-tls"
ca_id = "tls-ca"
allowed_key_types = ["ecdsaP256", "ecdsaP384"]

This configuration rejects all RSA CSRs and only accepts P-256 or P-384 ECDSA keys.

EKU Constraints

The required_ext_key_usage field forces specific Extended Key Usage OIDs into all certificates issued under this label. This ensures certificates are only used for their intended purpose.

Common EKU Values

required_ext_key_usage = [
    "serverAuth",        # TLS server authentication (1.3.6.1.5.5.7.3.1)
    "clientAuth",        # TLS client authentication (1.3.6.1.5.5.7.3.2)
    "emailProtection",   # S/MIME email (1.3.6.1.5.5.7.3.4)
    "codeSigning",       # Code signing (1.3.6.1.5.5.7.3.3)
]

You can specify multiple EKUs for dual-purpose certificates. If the field is omitted, the label does not enforce EKU requirements (though the CA may still add EKUs based on its own policy).

Example: Mutual TLS Label

[[est.label]]
name = "mtls"
ca_id = "corp-ca"
required_ext_key_usage = ["serverAuth", "clientAuth"]

Certificates issued under this label can be used for both server and client authentication.

SAN Requirements

The require_san field enforces the presence of Subject Alternative Names in certificate signing requests. This follows CA/Browser Forum Baseline Requirements, which mandate SAN extensions for publicly trusted TLS certificates.

require_san = true   # Default: reject CSRs without SAN
require_san = false  # Accept CSRs without SAN

When require_san = true, the EST server rejects any CSR that does not contain at least one SAN entry (DNS name, IP address, email, or URI).

When to Disable SAN Requirements

Set require_san = false for:

  • Client authentication certificates: User certificates often have meaningful Subject DNs but no SAN
  • Legacy device certificates: Older systems may not support SAN extension generation
  • Internal PKI: Private infrastructures may rely solely on Subject DN for identification
[[est.label]]
name = "user-cert"
ca_id = "user-ca"
required_ext_key_usage = ["clientAuth", "emailProtection"]
require_san = false  # Allow subject-only certificates

Subject DN Patterns

The subject_pattern field validates the Subject Distinguished Name of incoming CSRs using regular expressions. This prevents clients from requesting certificates with arbitrary subject fields.

Pattern Syntax

# Exact CN match
subject_pattern = "^CN=server\\.example\\.com$"

# Domain restriction with variable hostname
subject_pattern = "^CN=[a-z0-9-]+\\.example\\.com$"

# Multiple RDN components
subject_pattern = "^CN=[^,]+,O=Example Corp,C=US$"

# Flexible ordering (less restrictive)
subject_pattern = "CN=[a-z0-9-]+\\.example\\.com"

The regex engine is Rust’s regex crate, which supports standard PCRE-like syntax. Patterns are case-sensitive by default.

Example: Locked-Down Production Label

[[est.label]]
name = "prod-server"
ca_id = "prod-ca"
subject_pattern = "^CN=(www|api|mail)\\.example\\.com(,O=Example Corp)?(,C=US)?$"
required_ext_key_usage = ["serverAuth"]
allowed_key_types = ["ecdsaP256", "ecdsaP384"]

This pattern only allows CSRs with CN values of www.example.com, api.example.com, or mail.example.com, with optional Organization and Country fields.

Practical Examples

Server TLS Label

Configuration for public-facing TLS server certificates following CA/B Forum requirements:

[[ca]]
id = "public-tls-ca"
cert_path = "/etc/kipuka/pki/tls-intermediate.crt"
key_path = "/etc/kipuka/pki/tls-intermediate.key"
default_validity_days = 365

[[est.label]]
name = "server-tls"
ca_id = "public-tls-ca"
allowed_key_types = ["ecdsaP256", "ecdsaP384", "rsa2048", "rsa3072"]
required_ext_key_usage = ["serverAuth"]
max_validity_days = 398  # CA/B Forum 398-day limit
require_san = true
subject_pattern = "^CN=[a-z0-9-]+\\.example\\.com$"

Client enrollment:

# Generate CSR with SAN
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -nodes -keyout server-key.pem -out server-csr.pem \
  -subj "/CN=web.example.com" \
  -addext "subjectAltName=DNS:web.example.com,DNS:www.example.com"

# Enroll via EST
curl --cacert ca.pem --cert client.pem --key client-key.pem \
  --data-binary @server-csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o server-cert.p7 \
  https://est.example.com/.well-known/est/server-tls/simpleenroll

# Extract certificate from PKCS#7
openssl pkcs7 -in server-cert.p7 -inform DER -print_certs -out server.pem

Client Authentication Label

Configuration for mutual TLS client certificates with shorter validity and relaxed SAN requirements:

[[ca]]
id = "client-ca"
cert_path = "/etc/kipuka/pki/client-intermediate.crt"
key_path = "/etc/kipuka/pki/client-intermediate.key"

[[est.label]]
name = "client-auth"
ca_id = "client-ca"
allowed_key_types = ["ecdsaP256", "rsa2048"]
required_ext_key_usage = ["clientAuth"]
max_validity_days = 90
require_san = false
subject_pattern = "^CN=[a-z]+\\.[a-z]+@example\\.com$"

Client enrollment:

# Generate CSR (no SAN required)
openssl req -new -newkey rsa:2048 -nodes \
  -keyout client-key.pem -out client-csr.pem \
  -subj "/[email protected]"

# Enroll via EST
curl --cacert ca.pem --cert bootstrap.pem --key bootstrap-key.pem \
  --data-binary @client-csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o client-cert.p7 \
  https://est.example.com/.well-known/est/client-auth/simpleenroll

# Extract certificate
openssl pkcs7 -in client-cert.p7 -inform DER -print_certs -out client.pem

Device Identity Label

Configuration for IoT device certificates with strict key type enforcement and short validity:

[[ca]]
id = "device-ca"
cert_path = "/etc/kipuka/pki/device-intermediate.crt"
key_path = "/etc/kipuka/pki/device-intermediate.key"

[[est.label]]
name = "device"
ca_id = "device-ca"
allowed_key_types = ["ecdsaP256"]  # P-256 only for embedded devices
required_ext_key_usage = ["clientAuth"]
max_validity_days = 30
require_san = true
subject_pattern = "^CN=device-[0-9a-f]{8}$"

Device enrollment:

# Generate CSR with device-specific CN and SAN
DEVICE_ID="device-a1b2c3d4"
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -nodes -keyout device-key.pem -out device-csr.pem \
  -subj "/CN=${DEVICE_ID}" \
  -addext "subjectAltName=URI:urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890"

# Enroll via EST
curl --cacert ca.pem --cert device-bootstrap.pem --key device-bootstrap-key.pem \
  --data-binary @device-csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o device-cert.p7 \
  https://est.example.com/.well-known/est/device/simpleenroll

# Extract certificate
openssl pkcs7 -in device-cert.p7 -inform DER -print_certs -out device.pem

Label Selection Strategy

When designing your EST label architecture, consider:

  1. Certificate Purpose Separation: Create distinct labels for server authentication, client authentication, email protection, and code signing. Never mix purposes in a single label.

  2. Security Posture: Use restrictive labels for production workloads (subject_pattern, strict allowed_key_types) and permissive labels for development environments.

  3. Validity Period: Match max_validity_days to certificate rotation cadence. Short-lived certificates (30-90 days) for automated systems, longer validity for manually managed certificates.

  4. CA Hierarchy Mapping: Map labels to CAs based on trust requirements. Public-facing services may require certificates from a different CA than internal infrastructure.

  5. Client Capabilities: Consider what key types and extensions your clients can generate. Legacy systems may require require_san = false and broader allowed_key_types.

Label Discovery

Clients can discover available labels through the EST /csrattrs endpoint:

curl --cacert ca.pem --cert client.pem --key client-key.pem \
  https://est.example.com/.well-known/est/server-tls/csrattrs

However, RFC 7030 does not define a standard mechanism for enumerating all labels. Operators should document available labels and their intended purposes in client onboarding documentation.

Validation Behavior

When a client submits a CSR to a labeled endpoint, kipuka performs validation in this order:

  1. Key type validation: If allowed_key_types is set, reject CSRs with non-matching key algorithms
  2. SAN validation: If require_san = true, reject CSRs without SAN extension
  3. Subject DN validation: If subject_pattern is set, reject CSRs with non-matching Subject DN
  4. EKU injection: Add required_ext_key_usage OIDs to the issued certificate (does not validate CSR EKUs)
  5. Validity enforcement: Apply max_validity_days limit, overriding CA default if stricter

If any validation step fails, the EST server returns HTTP 400 Bad Request with an error message indicating which constraint was violated.

Performance Considerations

Label lookups occur on every EST request. For deployments with hundreds of labels, ensure kipuka.toml is stored on fast local storage. Label matching is O(n) in the number of configured labels.

If you anticipate very large numbers of labels (>100), consider deploying multiple kipuka instances, each serving a subset of labels, behind a reverse proxy that routes based on URL path.

Authentication

kipuka supports multiple authentication methods to accommodate different enrollment scenarios and enterprise environments. This chapter covers the three primary authentication mechanisms available for EST endpoints.

Authentication Methods

mTLS (Mutual TLS) Authentication

Mutual TLS is the primary authentication method for EST as defined in RFC 7030. The client presents a certificate during the TLS handshake, which the server validates against a configured trust store.

Configuration

mTLS is configured in the [tls.client_auth] section:

[tls]
# ... other TLS settings ...

[tls.client_auth]
# Path to CA bundle containing trusted client certificate issuers
trust_anchors = "/etc/kipuka/client-ca-bundle.pem"

# Client certificate verification mode
mode = "required"  # required | optional | none

Mode Options:

  • required: All EST endpoints require a valid client certificate. The TLS handshake fails if no certificate is presented or if the certificate is not signed by a trusted CA.
  • optional: Client certificate is verified if presented, but clients without certificates can still connect. This mode allows fallback to OTP or GSSAPI for initial enrollment.
  • required_for_reenroll: Enrollment optionally requires a client certificate, but reenrollment is only possible using an existing client certificate.
  • none: No client certificate verification. Not recommended for production environments.

Trust Anchors:

The trust_anchors file should contain one or more PEM-encoded CA certificates. These CAs define which client certificates are accepted for authentication.

# Example: concatenate multiple CA certificates
cat corporate-ca.pem partner-ca.pem > /etc/kipuka/client-ca-bundle.pem

Re-enrollment Workflow

For re-enrollment (/simplereenroll), the client typically uses its previously-issued certificate as the client certificate:

  1. Client initiates TLS connection with its current certificate
  2. Server validates certificate against trust anchors
  3. Client submits CSR for new certificate
  4. Server issues new certificate

Example: Initial Enrollment with mTLS

For clients that already have a certificate (e.g., issued by a different CA or manually installed):

curl --cert client.crt --key client.key \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @request.csr \
     https://est.example.com:8443/.well-known/est/simpleenroll

Example: Re-enrollment

# Use the previously-issued certificate for authentication
curl --cert current.crt --key current.key \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @renewal.csr \
     https://est.example.com:8443/.well-known/est/simplereenroll

OTP (One-Time Password) Authentication

OTP authentication is designed for initial enrollment scenarios where the client does not yet have a certificate. The administrator generates a token via the Admin API, delivers it to the client through a secure out-of-band channel, and the client uses it for authentication.

Configuration

[otp]
# Enable OTP authentication
enabled = true

# Hash algorithm for token storage
# Options: argon2id (recommended), bcrypt, sha256-hmac
hash_algorithm = "argon2id"

# Minimum token length (NIAP PP requires >= 16)
token_length = 20

# Single-use or multi-use tokens
max_uses = 1

# Rate limiting configuration
max_failures = 5
failure_window = 300  # 5 minutes
lockout_duration = 900  # 15 minutes

Hash Algorithms:

  • argon2id (recommended): NIAP-compliant, memory-hard algorithm resistant to GPU attacks
  • bcrypt: Industry-standard password hashing
  • sha256-hmac: Fast but less secure; use only for low-security environments

All implementations use timing-safe comparison to prevent timing attacks.

Rate Limiting:

  • max_failures: Maximum authentication failures before lockout
  • failure_window: Time window (seconds) for counting failures
  • lockout_duration: How long (seconds) the client is locked out after exceeding max_failures

OTP Workflow

  1. Token Generation: Administrator calls the Admin API to generate an OTP token
  2. Out-of-Band Delivery: Token is securely delivered to the client (email, SMS, secure portal, etc.)
  3. Client Authentication: Client uses the token in HTTP Basic authentication
  4. Certificate Issuance: Server validates token and issues certificate
  5. Future Renewals: Client uses mTLS with the issued certificate

Generating OTP Tokens

Tokens are generated via the Admin API:

# Generate a single-use OTP token for a client
curl -X POST https://est.example.com:8443/admin/otp \
     -H "Authorization: Bearer <admin-token>" \
     -H "Content-Type: application/json" \
     -d '{
       "client_id": "workstation-42",
       "max_uses": 1,
       "expires_at": "2026-06-30T23:59:59Z"
     }'

Response:

{
  "token": "xK9mP2vL8qR5wN3jT7fY",
  "client_id": "workstation-42",
  "created_at": "2026-06-24T10:15:00Z",
  "expires_at": "2026-06-30T23:59:59Z",
  "max_uses": 1
}

Using OTP for Enrollment

The client sends the OTP token as the password in HTTP Basic authentication. The username is ignored but should be provided for RFC compliance:

# Enroll using OTP authentication
curl -u ":xK9mP2vL8qR5wN3jT7fY" \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @request.csr \
     https://est.example.com:8443/.well-known/est/simpleenroll

After successful enrollment, the client stores the issued certificate and uses mTLS for all future operations, including re-enrollment.

NIAP PP Compliance

For NIAP Protection Profile compliance:

  • Set token_length >= 16
  • Use hash_algorithm = "argon2id"
  • Configure appropriate rate limiting
  • Enable audit logging for OTP generation and usage

GSSAPI/Kerberos Authentication

GSSAPI authentication provides enterprise Single Sign-On (SSO) integration using Kerberos tickets. This is particularly useful in Active Directory and FreeIPA environments.

Configuration

[gssapi]
# Enable GSSAPI/Kerberos authentication
enabled = true

# Path to server's keytab file
keytab = "/etc/kipuka/kipuka.keytab"

# Service principal name
# Format: HTTP/hostname@REALM
service_principal = "HTTP/[email protected]"

# Principal to certificate subject mapping
# Maps Kerberos principal to X.509 subject DN
[gssapi.principal_mapping]
"[email protected]" = "CN=User,OU=People,DC=example,DC=com"
"[email protected]" = "CN=Admin,OU=Admins,DC=example,DC=com"

# Default mapping template (optional)
# {principal} is replaced with the authenticated Kerberos principal
default_template = "CN={principal},OU=Users,DC=example,DC=com"

Server Setup

The EST server must have a keytab containing credentials for the HTTP service principal:

# On Active Directory or FreeIPA KDC, create the service principal
# and export the keytab

# FreeIPA example:
ipa service-add HTTP/est.example.com
ipa-getkeytab -s ipaserver.example.com \
              -p HTTP/est.example.com \
              -k /etc/kipuka/kipuka.keytab

# Set appropriate permissions
chown kipuka:kipuka /etc/kipuka/kipuka.keytab
chmod 600 /etc/kipuka/kipuka.keytab

Client Authentication

Clients use SPNEGO (RFC 4559) to send Kerberos tickets via the Negotiate HTTP authentication scheme:

# Obtain Kerberos ticket
kinit [email protected]

# Enroll using GSSAPI authentication
curl --negotiate -u : \
     --cacert est-server-ca.pem \
     -H "Content-Type: application/pkcs10" \
     --data-binary @request.csr \
     https://est.example.com:8443/.well-known/est/simpleenroll

The server:

  1. Receives the Negotiate header with Kerberos ticket
  2. Validates the ticket against the KDC
  3. Extracts the authenticated principal name
  4. Maps the principal to a certificate subject DN using the configured mapping rules
  5. Issues a certificate with the mapped subject

Principal Mapping

The principal_mapping table defines explicit mappings from Kerberos principals to certificate subject DNs. If no explicit mapping exists, the default_template is used (if configured).

Example: Principal [email protected] with default template "CN={principal},OU=Users,DC=example,DC=com" results in subject [email protected],OU=Users,DC=example,DC=com.

Authentication per Endpoint

Different EST endpoints have different authentication requirements:

EndpointAuthenticationNotes
/cacertsNonePublic endpoint; returns CA certificate chain
/csrattrsNonePublic endpoint; returns CSR attributes
/simpleenrollmTLS OR OTP OR GSSAPIInitial enrollment; at least one method must succeed
/simplereenrollmTLS requiredRe-enrollment requires an existing valid certificate
/serverkeygenmTLS OR OTP OR GSSAPIServer-side key generation; same as simpleenroll
/fullcmcmTLS requiredFull CMC protocol requires client certificate
/admin/*Admin authSeparate authentication via [admin] section

Admin Endpoint Authentication

Admin endpoints (OTP generation, certificate revocation, etc.) use separate authentication configured in the [admin] section:

[admin]
# mTLS for admin endpoints
[admin.tls]
trust_anchors = "/etc/kipuka/admin-ca-bundle.pem"
mode = "required"

# Bearer token authentication (alternative or in addition to mTLS)
[admin.bearer_token]
enabled = true
tokens = [
    { token_hash = "sha256:...", description = "CI/CD automation" },
    { token_hash = "sha256:...", description = "Admin portal" }
]

Admin authentication is independent of EST endpoint authentication. You can require mTLS for admin operations even if EST endpoints accept OTP or GSSAPI.

Authentication Precedence

When multiple authentication methods are enabled, kipuka evaluates them in the following order:

  1. mTLS: If a client certificate is presented (and mode != "none"), it is validated first
  2. GSSAPI: If no valid client certificate and Negotiate header is present, GSSAPI is attempted
  3. OTP: If no valid client certificate and Authorization: Basic header is present, OTP is checked

A request succeeds if any enabled method authenticates successfully. For endpoints requiring mTLS (e.g., /simplereenroll), only mTLS is evaluated.

Security Best Practices

  • Production environments: Use mode = "required" for mTLS and disable OTP after initial enrollment
  • Initial enrollment: Use mode = "optional" with OTP enabled, then migrate to mode = "required" after all devices are enrolled
  • Token delivery: Never send OTP tokens over the same channel as enrollment (e.g., do not email a token to an address that auto-forwards to the EST client)
  • Token entropy: Use token_length >= 20 for high-security environments
  • Keytab protection: Store GSSAPI keytabs with restrictive permissions (600) and limit access to the kipuka service account
  • Audit logging: Enable audit logs for all authentication events to detect suspicious activity

Troubleshooting

mTLS Issues

Error: “client certificate required”

  • Verify mode is set to optional or none if client does not have a certificate
  • Check that the client is sending a certificate (--cert and --key in curl)

Error: “certificate signed by unknown authority”

  • Ensure the client certificate is signed by a CA in trust_anchors
  • Verify the CA bundle is readable and contains valid PEM data

OTP Issues

Error: “invalid OTP token”

  • Check token expiration (expires_at)
  • Verify token has not exceeded max_uses
  • Ensure token is sent as password in Basic auth (username can be empty or any value)

Error: “too many failures, locked out”

  • Wait for lockout_duration to expire
  • Review rate limiting configuration (max_failures, failure_window)

GSSAPI Issues

Error: “GSSAPI authentication failed”

  • Verify client has a valid Kerberos ticket (klist)
  • Check server’s keytab is readable and contains the correct principal
  • Ensure clocks are synchronized between client, server, and KDC (Kerberos requires time sync within 5 minutes)

Error: “principal not mapped”

  • Add an explicit mapping in [gssapi.principal_mapping] or configure default_template
  • Check that the authenticated principal name matches the expected format

TLS Configuration

This guide covers TLS configuration for the kipuka EST server, including certificate requirements, protocol versions, cipher suites, client authentication modes, and production deployment scenarios.

Server Certificate Requirements

RFC 7030 Section 3.3.2 mandates that EST server certificates include the id-kp-cmcRA Extended Key Usage (EKU) with OID 1.3.6.1.5.5.7.3.28. This EKU identifies the server as an authorized Registration Authority (RA) for certificate enrollment operations.

Without this EKU, compliant EST clients may reject the server certificate, even if it is otherwise valid. In production deployments, include both:

  • id-kp-cmcRA (1.3.6.1.5.5.7.3.28) - Required for EST protocol compliance
  • id-kp-serverAuth (1.3.6.1.5.5.7.3.1) - Standard server authentication for web browser compatibility

Generating a Server Certificate with cmcRA EKU

Create an OpenSSL configuration file (est-server.cnf) with the required extensions:

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = v3_req
prompt             = no

[ req_distinguished_name ]
CN = est.example.com
O  = Example Organization
C  = US

[ v3_req ]
keyUsage           = critical, digitalSignature, keyEncipherment
extendedKeyUsage   = serverAuth, 1.3.6.1.5.5.7.3.28
subjectAltName     = @alt_names

[ alt_names ]
DNS.1 = est.example.com
DNS.2 = kipuka.example.com

[ v3_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical, CA:true
keyUsage               = critical, keyCertSign, cRLSign

Generate the server private key and CSR:

# Generate private key
openssl genrsa -out est-server-key.pem 2048

# Generate certificate signing request
openssl req -new -key est-server-key.pem \
    -out est-server.csr \
    -config est-server.cnf

# Sign with your CA (replace ca-cert.pem and ca-key.pem)
openssl x509 -req -in est-server.csr \
    -CA ca-cert.pem -CAkey ca-key.pem \
    -CAcreateserial -out est-server-cert.pem \
    -days 365 -sha256 \
    -extensions v3_req -extfile est-server.cnf

# Verify the cmcRA EKU is present
openssl x509 -in est-server-cert.pem -text -noout | grep -A1 "Extended Key Usage"

Expected output should include:

X509v3 Extended Key Usage:
    TLS Web Server Authentication, 1.3.6.1.5.5.7.3.28

Self-Signed Certificate for Development

For development and testing only:

# Generate self-signed certificate with cmcRA EKU
openssl req -x509 -newkey rsa:2048 -nodes \
    -keyout est-server-key.pem \
    -out est-server-cert.pem \
    -days 365 -sha256 \
    -config est-server.cnf \
    -extensions v3_req

Warning: Self-signed certificates should never be used in production. Always obtain certificates from a trusted CA.

TLS Version Configuration

The min_version parameter in the [tls] section controls the minimum TLS protocol version accepted by the server.

[tls]
min_version = "1.2"  # or "1.3"

Supported Values

  • "1.2" (default): Accept TLS 1.2 and TLS 1.3 connections. This is the minimum version required for RFC 7030 compliance and maintains compatibility with older EST clients.
  • "1.3": Accept only TLS 1.3 connections. Recommended for new deployments where all clients support TLS 1.3, as it provides improved security and performance.

TLS 1.0 and TLS 1.1 are not supported, as they have been deprecated by RFC 8996 and are considered insecure.

Recommendation

For production environments deployed after 2024, set min_version = "1.3" unless you have specific legacy client requirements. TLS 1.3 removes obsolete cryptographic algorithms, reduces handshake latency, and encrypts more of the handshake metadata.

Cipher Suite Configuration

The cipher_suites array in the [tls] section specifies the allowed cipher suites. If omitted, kipuka uses a secure default set that excludes weak ciphers.

[tls]
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

TLS 1.2 Cipher Suites

The default cipher suite list for TLS 1.2 connections includes:

cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
]

All suites provide:

  • Forward Secrecy via ECDHE key exchange
  • AEAD encryption (GCM or ChaCha20-Poly1305)
  • Strong hash functions (SHA-256 or SHA-384)

TLS 1.3 Cipher Suites

TLS 1.3 defines only three mandatory cipher suites, which are always enabled when min_version = "1.3":

  • TLS_AES_256_GCM_SHA384
  • TLS_AES_128_GCM_SHA256
  • TLS_CHACHA20_POLY1305_SHA256

The cipher_suites configuration parameter does not affect TLS 1.3, as the protocol specification mandates these suites.

NIAP and FIPS Compliance

For systems requiring NIAP Common Criteria or FIPS 140-2/3 compliance, disable ChaCha20-Poly1305 cipher suites, as they are not FIPS-approved algorithms:

[tls]
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

Consult your organization’s security policy for approved cipher suites.

Client Authentication Modes

The mode parameter in the [tls.client_auth] section controls whether and how client certificates are verified.

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/client-ca.pem"

Available Modes

mode = "required"

All TLS connections must present a valid client certificate. The certificate must:

  • Chain to a CA in the trust_anchors file
  • Not be expired or revoked
  • Have valid signatures

Use this mode when:

  • All clients have existing certificates (e.g., re-enrollment only)
  • Initial enrollment is performed via a separate out-of-band mechanism
  • Maximum security is required

Note: With mode = "required", clients cannot perform initial enrollment unless they already possess a certificate. You must provision initial certificates through another channel (e.g., manual issuance, SCEP, or ACME).

mode = "optional"

Client certificates are verified if presented, but connections are allowed without a certificate. This mode supports:

  • mTLS re-enrollment: Clients with existing certificates use them for authentication
  • OTP-based enrollment: Clients without certificates authenticate with a one-time password (OTP) during initial enrollment

This is the recommended mode for production EST deployments, as it supports the full enrollment lifecycle.

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/client-ca.pem"

[enrollment]
require_proof_of_possession = true  # Enforce PoP for OTP enrollments

mode = "none"

No client certificate verification is performed. Connections are accepted without client authentication.

This mode is for development and testing only. Do not use in production, as it disables a critical security layer.

Trust Anchors

The trust_anchors parameter specifies a PEM file containing the CA certificates trusted for client authentication. This file may contain multiple concatenated certificates.

# Example trust_anchors file with two CAs
cat ca1-cert.pem ca2-cert.pem > /etc/kipuka/client-ca.pem

kipuka validates client certificates against this trust anchor bundle. Ensure the file is readable by the kipuka process user.

Separate Admin API TLS Configuration

The administrative API runs on a separate TCP port (default 9443) and can use independent TLS settings. This allows you to:

  • Bind the admin API to a management network interface
  • Use separate client authentication for administrative operations
  • Restrict admin access to authorized systems only

Basic Admin API Configuration

[server]
listen = "0.0.0.0:8443"        # EST API (client-facing)
admin_listen = "127.0.0.1:9443" # Admin API (localhost only)

[tls]
cert = "/etc/kipuka/est-server-cert.pem"
key = "/etc/kipuka/est-server-key.pem"

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/client-ca.pem"

[admin.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/admin-ca.pem"  # Separate CA for admin certs

In this example:

  • EST API on port 8443 accepts optional client certificates from end-user devices
  • Admin API on localhost port 9443 requires client certificates from administrator systems
  • Admin certificates are issued by a separate CA (admin-ca.pem)

Binding to Management Networks

For multi-homed systems, bind the admin API to a dedicated management network interface:

[server]
listen = "0.0.0.0:8443"           # EST API on all interfaces
admin_listen = "10.0.1.100:9443"  # Admin API on management VLAN

Configure firewall rules to restrict access to the management network. For example, using iptables:

# Allow admin API only from management subnet
iptables -A INPUT -p tcp --dport 9443 -s 10.0.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 9443 -j DROP

Localhost-Only Admin Access

For single-host deployments or when using a reverse proxy, bind the admin API to localhost:

[server]
admin_listen = "127.0.0.1:9443"

Administrators can access the API via SSH port forwarding:

ssh -L 9443:localhost:9443 [email protected]
curl --cert admin-cert.pem --key admin-key.pem \
     https://localhost:9443/admin/health

Practical Configuration Examples

Development Setup with Self-Signed Certificate

For local testing and development:

[server]
listen = "127.0.0.1:8443"
admin_listen = "127.0.0.1:9443"

[tls]
cert = "/home/dev/kipuka/dev-cert.pem"
key = "/home/dev/kipuka/dev-key.pem"
min_version = "1.2"

[tls.client_auth]
mode = "none"  # Accept all connections for testing

[enrollment]
require_proof_of_possession = false  # Simplified for dev

Generate the development certificate:

openssl req -x509 -newkey rsa:2048 -nodes \
    -keyout dev-key.pem -out dev-cert.pem \
    -days 365 -sha256 \
    -subj "/CN=localhost" \
    -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
    -addext "extendedKeyUsage=serverAuth,1.3.6.1.5.5.7.3.28"

Production Setup with Internal CA

For enterprise deployments with an internal PKI:

[server]
listen = "0.0.0.0:8443"
admin_listen = "10.0.1.100:9443"

[tls]
cert = "/etc/kipuka/certs/est-server-cert.pem"
key = "/etc/kipuka/private/est-server-key.pem"
min_version = "1.2"
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

[tls.client_auth]
mode = "optional"
trust_anchors = "/etc/kipuka/ca/device-ca-bundle.pem"

[admin.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/ca/admin-ca.pem"

[enrollment]
require_proof_of_possession = true
allowed_key_types = ["rsa2048", "rsa4096", "ecdsa256", "ecdsa384"]

Certificate generation workflow:

# 1. Generate server private key
openssl genrsa -out est-server-key.pem 2048
chmod 600 est-server-key.pem

# 2. Create CSR with cmcRA EKU
openssl req -new -key est-server-key.pem \
    -out est-server.csr \
    -config est-server.cnf

# 3. Submit CSR to internal CA for signing
# (Process varies by CA implementation)

# 4. Install signed certificate
install -m 644 est-server-cert.pem /etc/kipuka/certs/
install -m 600 est-server-key.pem /etc/kipuka/private/
chown kipuka:kipuka /etc/kipuka/certs/est-server-cert.pem
chown kipuka:kipuka /etc/kipuka/private/est-server-key.pem

# 5. Verify cmcRA EKU
openssl x509 -in /etc/kipuka/certs/est-server-cert.pem -text -noout \
    | grep -A1 "Extended Key Usage"

High-Security Setup with TLS 1.3 Only

For environments requiring maximum security (e.g., classified networks, financial services):

[server]
listen = "10.0.1.50:8443"
admin_listen = "127.0.0.1:9443"

[tls]
cert = "/etc/kipuka/certs/est-server-cert.pem"
key = "/etc/kipuka/private/est-server-key.pem"
min_version = "1.3"  # TLS 1.3 only

# Note: cipher_suites parameter has no effect in TLS 1.3
# The following suites are always enabled:
# - TLS_AES_256_GCM_SHA384
# - TLS_AES_128_GCM_SHA256
# - TLS_CHACHA20_POLY1305_SHA256

[tls.client_auth]
mode = "required"  # All connections must present client cert
trust_anchors = "/etc/kipuka/ca/device-ca-bundle.pem"

[admin.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/ca/admin-ca.pem"

[enrollment]
require_proof_of_possession = true
allowed_key_types = ["ecdsa384"]  # P-384 only
max_validity_days = 365
require_san = true

Additional hardening:

# Restrict file permissions
chmod 700 /etc/kipuka/private
chmod 600 /etc/kipuka/private/*.pem
chmod 644 /etc/kipuka/certs/*.pem

# Run as unprivileged user
useradd -r -s /bin/false -d /var/lib/kipuka kipuka
chown -R kipuka:kipuka /etc/kipuka

# Enable mandatory access control (SELinux/AppArmor)
# Example SELinux policy (adjust for your distribution)
semanage fcontext -a -t cert_t "/etc/kipuka/certs(/.*)?"
semanage fcontext -a -t cert_t "/etc/kipuka/ca(/.*)?"
semanage fcontext -a -t cert_t "/etc/kipuka/private(/.*)?"
restorecon -R /etc/kipuka

FIPS-Compliant Configuration

For systems requiring FIPS 140-2/3 compliance:

[server]
listen = "0.0.0.0:8443"
admin_listen = "10.0.1.100:9443"

[tls]
cert = "/etc/kipuka/certs/est-server-cert.pem"
key = "/etc/kipuka/private/est-server-key.pem"
min_version = "1.2"

# Disable ChaCha20 (not FIPS-approved)
cipher_suites = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]

[tls.client_auth]
mode = "required"
trust_anchors = "/etc/kipuka/ca/device-ca-bundle.pem"

[enrollment]
allowed_key_types = ["rsa2048", "rsa4096", "ecdsa256", "ecdsa384"]
allowed_signature_algorithms = ["sha256", "sha384", "sha512"]

Ensure the underlying operating system is running in FIPS mode:

# RHEL/CentOS
fips-mode-setup --enable
reboot

# Verify FIPS mode
cat /proc/sys/crypto/fips_enabled  # Should output: 1

Troubleshooting

Client Rejects Server Certificate

Symptom: EST client fails with “server certificate verification failed” or similar.

Cause: Server certificate missing the id-kp-cmcRA EKU.

Solution: Verify the EKU is present:

openssl x509 -in est-server-cert.pem -text -noout | grep -A1 "Extended Key Usage"

Expected output:

X509v3 Extended Key Usage:
    TLS Web Server Authentication, 1.3.6.1.5.5.7.3.28

If 1.3.6.1.5.5.7.3.28 is missing, regenerate the certificate using the instructions in “Server Certificate Requirements.”

TLS Handshake Failure

Symptom: Connection fails during TLS handshake with “no shared cipher” or “protocol version mismatch.”

Cause: Client and server have no common cipher suites or TLS versions.

Solution: Enable TLS 1.2 support if clients do not support TLS 1.3:

[tls]
min_version = "1.2"

Check client cipher suite support and add compatible suites to cipher_suites.

Admin API Unreachable

Symptom: Cannot connect to admin API on port 9443.

Cause: Admin API bound to wrong interface or blocked by firewall.

Solution: Verify admin_listen binding:

[server]
admin_listen = "0.0.0.0:9443"  # Listen on all interfaces

Check firewall rules:

# List firewall rules
iptables -L -n -v | grep 9443

# Allow admin API port
iptables -A INPUT -p tcp --dport 9443 -j ACCEPT

For localhost-only access, use SSH port forwarding:

ssh -L 9443:localhost:9443 [email protected]

Security Best Practices

  1. Use TLS 1.3 for new deployments: Set min_version = "1.3" to eliminate legacy protocol weaknesses.
  2. Restrict cipher suites: Use the default list or a more restrictive set. Avoid CBC-mode ciphers and weak hashes.
  3. Require client authentication for re-enrollment: Set mode = "required" if all clients have certificates.
  4. Separate admin and client CAs: Use distinct trust anchors for administrative and end-user certificates.
  5. Bind admin API to management networks: Limit exposure of administrative endpoints.
  6. Rotate certificates before expiration: Monitor certificate expiry and renew at least 30 days in advance.
  7. Protect private keys: Store keys on encrypted filesystems or HSMs. Use chmod 600 and restrict access to the kipuka process user.
  8. Enable OCSP or CRL checking: Configure revocation checking for client certificates (see enrollment configuration documentation).
  9. Audit TLS configuration regularly: Use tools like testssl.sh or sslyze to verify cipher suite and protocol configuration.
  10. Follow your organization’s security policy: Consult internal PKI and cryptography standards before deployment.

References

  • RFC 7030: Enrollment over Secure Transport (EST)
  • RFC 8996: Deprecating TLS 1.0 and TLS 1.1
  • RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
  • NIST SP 800-52 Rev. 2: Guidelines for the Selection, Configuration, and Use of TLS
  • NIAP Protection Profile for Network Devices: Common Criteria requirements for TLS configuration

HSM Integration

kipuka supports Hardware Security Modules (HSMs) and software HSMs through the PKCS#11 (Cryptoki) standard. HSM integration ensures that CA private keys never leave the secure hardware boundary, with all signing operations delegated to the HSM.

PKCS#11 Overview

kipuka uses the PKCS#11 API to interface with HSMs. The integration model follows these principles:

  • The [hsm] section in kipuka.toml configures the PKCS#11 provider library and authentication
  • CA definitions reference HSM-stored keys using the hsm_slot parameter in the [[ca]] section
  • Private keys remain on the HSM at all times—signing operations are performed by the HSM
  • When hsm_slot is set for a CA, the key parameter is ignored

This architecture ensures that sensitive key material never exists in kipuka’s process memory or on the filesystem.

Supported Vendors and Library Paths

kipuka works with any PKCS#11-compliant HSM. The following table lists common vendors and their typical library paths:

VendorProductLibrary Path (Linux)Library Path (macOS)
EntrustnShield/opt/nfast/toolkits/pkcs11/libcknfast.soN/A
UtimacoCryptoServer/opt/utimaco/lib/libcs_pkcs11_R3.soN/A
ThalesLuna/usr/safenet/lunaclient/lib/libCryptoki2_64.so/usr/safenet/lunaclient/lib/libCryptoki2.dylib
KryopticSoftHSM-compatible/usr/lib/pkcs11/libkryoptic_pkcs11.sotarget/release/libkryoptic_pkcs11.dylib
SoftHSM2SoftHSM2/usr/lib/softhsm/libsofthsm2.so/usr/local/lib/softhsm/libsofthsm2.so
AWSCloudHSM/opt/cloudhsm/lib/libcloudhsm_pkcs11.soN/A
YubiHSMYubiHSM2/usr/lib/x86_64-linux-gnu/pkcs11/yubihsm_pkcs11.so/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib

Consult your HSM vendor documentation for the exact library path on your system.

PIN Management

kipuka supports three methods for providing the HSM PIN, evaluated in the following priority order:

Set pin_env to the name of an environment variable containing the PIN:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin_env = "KIPUKA_HSM_PIN"

Then start kipuka with the PIN in the environment:

export KIPUKA_HSM_PIN="your-pin-here"
kipuka

For systemd services, use EnvironmentFile:

[Service]
EnvironmentFile=/etc/kipuka/hsm.env
ExecStart=/usr/local/bin/kipuka

Where /etc/kipuka/hsm.env contains:

KIPUKA_HSM_PIN=your-pin-here

Set restrictive permissions on the environment file:

chmod 0400 /etc/kipuka/hsm.env
chown kipuka:kipuka /etc/kipuka/hsm.env

PIN File

Set pin_file to a path containing only the PIN:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin_file = "/etc/kipuka/hsm.pin"

Secure the PIN file:

echo "your-pin-here" > /etc/kipuka/hsm.pin
chmod 0400 /etc/kipuka/hsm.pin
chown kipuka:kipuka /etc/kipuka/hsm.pin

For development only, the PIN can be stored directly in the configuration:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin = "1234"

Never use this method in production. The PIN will be visible in the configuration file and process listings.

Slot Configuration

PKCS#11 tokens are identified by either slot number or token label.

By Slot Number

Reference the HSM token by its numeric slot identifier:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
slot = 0
pin_env = "KIPUKA_HSM_PIN"

By Token Label

Reference the HSM token by its label:

[hsm]
library = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "kipuka"
pin_env = "KIPUKA_HSM_PIN"

Using token_label is generally more portable across HSM reconfigurations.

Discovering Slots

Use pkcs11-tool to list available slots:

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --list-slots

Example output:

Available slots:
Slot 0 (0x1d6d28f6): SoftHSM slot ID 0x1d6d28f6
  token label        : kipuka
  token manufacturer : SoftHSM project
  token model        : SoftHSM v2
  token flags        : login required, rng, token initialized, PIN initialized
  hardware version   : 2.6
  firmware version   : 2.6
  serial num         : 4c8e0a766d6d28f6
  pin min/max        : 4/255

The token label from this output can be used as the token_label value.

Key Generation Examples

Before configuring kipuka to use HSM-stored keys, you must generate key pairs on the HSM. The following examples use pkcs11-tool from the OpenSC package.

Generate RSA 2048-bit Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type rsa:2048 \
  --id 01 \
  --label "kipuka-ca-rsa-2048"

Generate RSA 4096-bit Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type rsa:4096 \
  --id 02 \
  --label "kipuka-ca-rsa-4096"

Generate ECDSA P-256 Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type EC:prime256v1 \
  --id 03 \
  --label "kipuka-ca-ec-p256"

Generate ECDSA P-384 Key Pair

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type EC:secp384r1 \
  --id 04 \
  --label "kipuka-ca-ec-p384"

List HSM Objects

Verify key generation by listing objects on the token:

pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so \
  --login \
  --pin 1234 \
  --list-objects

Example output:

Public Key Object; RSA 2048 bits
  label:      kipuka-ca-rsa-2048
  ID:         01
  Usage:      encrypt, verify, wrap
Private Key Object; RSA
  label:      kipuka-ca-rsa-2048
  ID:         01
  Usage:      decrypt, sign, unwrap

The ID field value is used as the hsm_slot parameter in kipuka’s CA configuration.

Development Setup with Kryoptic

Kryoptic is a Rust-based software PKCS#11 implementation suitable for development and testing. It provides a lightweight alternative to hardware HSMs.

Installation

Install Kryoptic from crates.io:

cargo install kryoptic

Or build from source:

git clone https://github.com/latchset/kryoptic.git
cd kryoptic
cargo build --release

The PKCS#11 library will be at target/release/libkryoptic_pkcs11.so (Linux) or target/release/libkryoptic_pkcs11.dylib (macOS).

Initialize a Token

Create a Kryoptic configuration directory:

mkdir -p ~/.config/kryoptic

Initialize a token using pkcs11-tool:

pkcs11-tool --module target/release/libkryoptic_pkcs11.so \
  --init-token \
  --label "kipuka-dev" \
  --so-pin 12345678

Set the user PIN:

pkcs11-tool --module target/release/libkryoptic_pkcs11.so \
  --init-pin \
  --so-pin 12345678 \
  --pin 1234

Generate Development Keys

Generate an RSA 2048-bit key pair:

pkcs11-tool --module target/release/libkryoptic_pkcs11.so \
  --login \
  --pin 1234 \
  --keypairgen \
  --key-type rsa:2048 \
  --id 01 \
  --label "kipuka-dev-ca"

Configure kipuka

Update kipuka.toml to use the Kryoptic library:

[hsm]
library = "/home/user/kryoptic/target/release/libkryoptic_pkcs11.so"
token_label = "kipuka-dev"
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "dev-ca"
name = "Development CA"
cert = "/etc/kipuka/ca/dev-ca.pem"
hsm_slot = 1
validity_days = 365

Generate the CA certificate (the private key remains on Kryoptic):

export KIPUKA_HSM_PIN=1234

# Generate a self-signed CA certificate using the HSM key
openssl req -new -x509 \
  -engine pkcs11 \
  -keyform engine \
  -key "pkcs11:token=kipuka-dev;id=%01;type=private" \
  -out /etc/kipuka/ca/dev-ca.pem \
  -days 3650 \
  -subj "/CN=kipuka Development CA"

Start kipuka:

kipuka

kipuka will now use the Kryoptic HSM for all signing operations for the dev-ca CA.

Full Configuration Example

The following example shows a complete HSM configuration with multiple CAs:

# HSM configuration
[hsm]
library = "/usr/lib/pkcs11/libkryoptic_pkcs11.so"
token_label = "kipuka"
pin_env = "KIPUKA_HSM_PIN"

# Server configuration
[server]
listen = "0.0.0.0:8443"
tls_cert = "/etc/kipuka/server.pem"
tls_key = "/etc/kipuka/server-key.pem"

# RSA CA using HSM key
[[ca]]
id = "hsm-rsa-ca"
name = "HSM-Protected RSA CA"
cert = "/etc/kipuka/ca/hsm-rsa-ca.pem"
hsm_slot = 1
validity_days = 365
key_usage = ["digitalSignature", "keyCertSign", "cRLSign"]

# ECDSA CA using HSM key
[[ca]]
id = "hsm-ec-ca"
name = "HSM-Protected ECDSA CA"
cert = "/etc/kipuka/ca/hsm-ec-ca.pem"
hsm_slot = 3
validity_days = 365
key_usage = ["digitalSignature", "keyCertSign", "cRLSign"]

Key points:

  • The hsm_slot parameter references the object ID assigned during key generation
  • When hsm_slot is set, the key parameter must be omitted—the private key exists only on the HSM
  • Multiple CAs can share the same HSM by using different slot identifiers
  • The CA certificate in the cert parameter is the public certificate; the private key remains on the HSM

Troubleshooting

Library Not Found

If kipuka reports that the PKCS#11 library cannot be loaded, verify:

  • The library path is correct for your system
  • The library file has appropriate read and execute permissions
  • Required dependencies are installed (consult HSM vendor documentation)

Authentication Failures

If kipuka reports authentication failures:

  • Verify the PIN is correct using pkcs11-tool --login
  • Check that the token is not locked due to failed login attempts
  • Ensure the environment variable or PIN file is readable by the kipuka process user

Key Not Found

If kipuka reports that the key cannot be found in the HSM:

  • List objects with pkcs11-tool --list-objects and verify the key exists
  • Ensure the hsm_slot value matches the key’s ID field
  • Check that the token label or slot number is correct

Performance Considerations

HSM signing operations have higher latency than software signing. For high-throughput deployments:

  • Use hardware HSMs with dedicated crypto acceleration
  • Consider load balancing across multiple kipuka instances
  • Monitor HSM session limits and adjust max_sessions if supported by your HSM

High Availability

kipuka provides multi-CA high availability at the application layer. When a Certificate Authority backend becomes unavailable—whether due to HSM failure, Dogtag server downtime, or certificate expiration—kipuka can automatically failover to an alternate CA. This ensures continuous certificate enrollment even when individual CA components fail.

High availability is configured through [ha] and [[ha.group]] sections in the configuration file.

Overview

Traditional PKI deployments often rely on infrastructure-level HA (load balancers, database replication) for a single CA. kipuka takes a different approach: it treats each CA as an independent backend and implements application-layer failover logic. This allows:

  • Heterogeneous CA backends: Mix HSM-backed CAs with file-based CAs, or Dogtag with other implementations
  • Gradual migration: Route a percentage of traffic to a new CA while maintaining the old one
  • Geographic distribution: Route requests to the nearest or fastest CA
  • Independent failure domains: HSM failure doesn’t take down file-based backup CAs

When a CA fails, kipuka’s circuit breaker immediately stops routing requests to it and redistributes traffic to healthy CAs in the same group. When the failed CA recovers, kipuka automatically reintegrates it based on the configured strategy.

Failover Strategies

kipuka supports four failover strategies, selectable globally via [ha] or per-group via [[ha.group]]:

active-passive

The primary CA handles all requests. If it fails, the secondary takes over. When the primary recovers, traffic automatically returns to it.

Use when:

  • You have a clear primary CA (e.g., HSM-backed) and want a hot standby
  • You need predictable CA assignment for audit or compliance
  • Your CAs have different trust levels or security characteristics

Behavior:

  • All requests routed to the first healthy CA in the group
  • On failure, immediately switch to the next CA
  • On recovery, immediately switch back to the primary

round-robin

Requests are distributed evenly across all healthy CAs in the group. Any failure removes that CA from rotation until it recovers.

Use when:

  • All CAs in the group have equivalent capacity and trust
  • You want to distribute load across multiple CAs
  • You need to maximize utilization of all CA backends

Behavior:

  • Each request goes to the next CA in a circular list
  • Failed CAs are skipped in the rotation
  • Load is balanced evenly across all healthy CAs

weighted

Requests are distributed according to configured weights (e.g., 80% to CA1, 20% to CA2). Allows proportional load distribution.

Use when:

  • CAs have different capacities (e.g., HSM vs. software)
  • You’re migrating from one CA to another gradually
  • You want to test a new CA with a small percentage of traffic

Behavior:

  • Each CA receives traffic proportional to its weight
  • Weights are specified per-CA in the group configuration
  • If a CA fails, its weight is redistributed to remaining CAs

Configuration:

[[ha.group]]
name = "primary"
ca_ids = ["hsm-ca", "file-ca"]
strategy = "weighted"
weights = { "hsm-ca" = 80, "file-ca" = 20 }

latency-based

Requests are routed to the CA with the lowest recent response time. Self-optimizing for geographically distributed CAs.

Use when:

  • CAs are in different geographic locations
  • Network latency varies significantly between CAs
  • You want automatic optimization without manual tuning

Behavior:

  • kipuka tracks rolling average latency for each CA
  • Each request goes to the CA with the lowest average latency
  • Latency is measured from health checks and actual signing operations
  • Failed CAs are assigned infinite latency

Circuit Breaker Pattern

kipuka implements a circuit breaker to prevent cascading failures and automatically recover from transient issues. The circuit breaker prevents the system from repeatedly attempting to use a failing CA, which could delay client requests or exhaust resources.

States

The circuit breaker transitions through five states:

Healthy: CA is responding normally. All requests are routed to it according to the failover strategy.

Degraded: CA has experienced some failures but remains below the failure threshold. Requests continue to be routed, but the CA is monitored more closely. This state provides early warning of potential issues.

Unhealthy: Failure count exceeds failure_threshold within the check window. The CA is immediately removed from rotation to prevent client impact.

CircuitOpen: After transitioning to Unhealthy, the circuit opens. No requests are sent to this CA. A timer starts for recovery_timeout seconds to allow the CA time to recover.

Recovering: After recovery_timeout expires, a single probe request is sent. If it succeeds, the CA transitions back to Healthy and rejoins rotation. If it fails, the circuit returns to CircuitOpen with an extended timeout (exponential backoff).

State Transitions

Healthy --> Degraded --> Unhealthy --> CircuitOpen --> Recovering --> Healthy
   ^                                      |               |
   |                                      +-------<-------+
   +------------------<-------------------+
  • Healthy → Degraded: First failure detected
  • Degraded → Unhealthy: Failure count exceeds threshold
  • Unhealthy → CircuitOpen: Immediate transition; timer starts
  • CircuitOpen → Recovering: After recovery_timeout expires
  • Recovering → Healthy: Probe succeeds; CA rejoins rotation
  • Recovering → CircuitOpen: Probe fails; extended timeout begins
  • Degraded → Healthy: CA recovers before hitting threshold
  • CircuitOpen → Healthy: Manual operator override (health check passes)

Configuration

Circuit breaker behavior is tuned via [ha]:

[ha]
check_interval = "10s"        # How often to probe each CA
failure_threshold = 3          # Consecutive failures before marking unhealthy
recovery_timeout = "60s"       # Wait time before probing a failed CA
check_timeout = "5s"           # Max time to wait for health check response
  • check_interval: Frequency of active health checks. Shorter intervals detect failures faster but increase CA load.
  • failure_threshold: Number of consecutive failures before removing a CA from rotation. Lower values improve responsiveness but may cause flapping; higher values increase tolerance for transient failures.
  • recovery_timeout: How long to wait before attempting recovery. This gives the CA time to fully restart or for transient issues to resolve. kipuka uses exponential backoff: if the first probe fails, the next timeout is doubled.
  • check_timeout: Maximum time to wait for a health check response. Should be shorter than check_interval to avoid overlapping checks.

HA Groups

HA groups define sets of CAs that provide redundancy for each other. All CAs in a group should issue from the same root (or cross-certified roots) so clients trust all issuers.

Configuration

[[ha.group]]
name = "primary"               # Unique group name
ca_ids = ["hsm-ca", "file-ca"] # Array of [[ca]] id values
strategy = "active-passive"    # Optional: override global strategy
  • name: Unique identifier for this group. Used in logs and metrics.
  • ca_ids: Array of CA identifiers. Must reference valid [[ca]] sections. Order matters for active-passive strategy.
  • strategy: Optional override of the global [ha] strategy. Allows different strategies for different groups.

EST Label Integration

EST labels reference individual CAs via their id. When the primary CA in a group fails, the HA system automatically routes requests to the next healthy CA in the group. From the client’s perspective, the EST label remains the same—failover is transparent.

Example:

[[ca]]
id = "hsm-ca"
backend = "dogtag"
# ... HSM configuration ...

[[ca]]
id = "file-ca"
backend = "file"
# ... file configuration ...

[[ha.group]]
name = "production"
ca_ids = ["hsm-ca", "file-ca"]
strategy = "active-passive"

[[est.label]]
name = "device-cert"
ca_id = "hsm-ca"  # References the group leader
profile = "deviceCert"

If hsm-ca fails, requests to the device-cert label automatically use file-ca until hsm-ca recovers.

Health Check Configuration

kipupa performs active health checks to detect CA failures and recoveries. Health checks are lightweight signing operations that verify the CA is fully functional—not just network-reachable.

Health Check Behavior

  1. Every check_interval seconds, kipuka sends a test signing request to each CA
  2. If the CA responds successfully within check_timeout, the check passes
  3. If the CA fails to respond or returns an error, the check fails
  4. After failure_threshold consecutive failures, the CA is marked Unhealthy
  5. After recovery_timeout seconds, kipuka sends a single probe to the failed CA
  6. If the probe succeeds, the CA returns to Healthy; if it fails, the timeout doubles

Tuning Recommendations

Low-latency environment (local CAs):

[ha]
check_interval = "5s"
failure_threshold = 2
recovery_timeout = "30s"
check_timeout = "2s"

High-latency environment (geographically distributed CAs):

[ha]
check_interval = "30s"
failure_threshold = 5
recovery_timeout = "120s"
check_timeout = "10s"

Production (balanced):

[ha]
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

Example Configurations

Two-CA Active-Passive with HSM Primary

A production deployment with an HSM-backed primary CA and a file-based backup. Normal traffic uses the HSM; if it fails, the file-based CA provides continuity.

[ha]
strategy = "active-passive"
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

[[ca]]
id = "hsm-ca"
backend = "dogtag"
[ca.dogtag]
url = "https://pki.example.com:8443"
ca_cert = "/etc/kipuka/pki-ca.pem"
auth_cert = "/etc/kipuka/ra-agent.pem"
auth_key = "pkcs11:token=HSM;object=ra-key"

[[ca]]
id = "backup-ca"
backend = "file"
[ca.file]
ca_cert = "/etc/kipuka/backup-ca.pem"
ca_key = "/etc/kipuka/backup-ca-key.pem"

[[ha.group]]
name = "production"
ca_ids = ["hsm-ca", "backup-ca"]

[[est.label]]
name = "device"
ca_id = "hsm-ca"  # HA group leader
profile = "deviceCert"

Expected behavior:

  • All requests use hsm-ca under normal conditions
  • If the HSM or Dogtag server fails, traffic immediately switches to backup-ca
  • When hsm-ca recovers, traffic returns to it within one check_interval
  • Clients see no difference—the device label works throughout

Three-CA Round-Robin for Load Distribution

Three identical CAs in different datacenters. Traffic is distributed evenly to maximize utilization and provide geographic redundancy.

[ha]
strategy = "round-robin"
check_interval = "15s"
failure_threshold = 3
recovery_timeout = "90s"
check_timeout = "7s"

[[ca]]
id = "ca-east"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-east.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-east.pem"
auth_key = "/etc/kipuka/ra-east-key.pem"

[[ca]]
id = "ca-west"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-west.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-west.pem"
auth_key = "/etc/kipuka/ra-west-key.pem"

[[ca]]
id = "ca-central"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-central.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-central.pem"
auth_key = "/etc/kipuka/ra-central-key.pem"

[[ha.group]]
name = "global"
ca_ids = ["ca-east", "ca-west", "ca-central"]

[[est.label]]
name = "iot"
ca_id = "ca-east"  # Any CA in the group; round-robin applies
profile = "iotDevice"

Expected behavior:

  • Requests are distributed 33/33/33 across the three CAs
  • If ca-east fails, traffic is redistributed 50/50 to ca-west and ca-central
  • When ca-east recovers, it rejoins the rotation
  • Each CA operates independently; no shared state required

Geographic HA with Latency-Based Routing

Two CAs in different regions. kipuka automatically routes requests to the CA with the lowest latency, optimizing performance for geographically distributed clients.

[ha]
strategy = "latency-based"
check_interval = "20s"
failure_threshold = 4
recovery_timeout = "120s"
check_timeout = "10s"

[[ca]]
id = "ca-us"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-us.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-us.pem"
auth_key = "/etc/kipuka/ra-us-key.pem"

[[ca]]
id = "ca-eu"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-eu.example.com:8443"
ca_cert = "/etc/kipuka/ca.pem"
auth_cert = "/etc/kipuka/ra-eu.pem"
auth_key = "/etc/kipuka/ra-eu-key.pem"

[[ha.group]]
name = "global"
ca_ids = ["ca-us", "ca-eu"]

[[est.label]]
name = "vpn"
ca_id = "ca-us"  # HA group leader; latency-based routing applies
profile = "vpnCert"

Expected behavior:

  • kipuka measures latency to both CAs during health checks
  • Requests are automatically routed to the faster CA (e.g., ca-us for US clients, ca-eu for EU clients)
  • If latency increases for one CA (network congestion, overload), traffic shifts to the other
  • If one CA fails completely, all traffic uses the remaining CA
  • No manual configuration required—self-optimizing based on network conditions

Weighted Migration from Old to New CA

Gradual migration from an existing CA to a new one. Start with 90% traffic on the old CA, gradually shift to 100% on the new CA, then decommission the old CA.

[ha]
strategy = "weighted"
check_interval = "10s"
failure_threshold = 3
recovery_timeout = "60s"
check_timeout = "5s"

[[ca]]
id = "old-ca"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-old.example.com:8443"
ca_cert = "/etc/kipuka/old-ca.pem"
auth_cert = "/etc/kipuka/ra-old.pem"
auth_key = "/etc/kipuka/ra-old-key.pem"

[[ca]]
id = "new-ca"
backend = "dogtag"
[ca.dogtag]
url = "https://pki-new.example.com:8443"
ca_cert = "/etc/kipuka/new-ca.pem"
auth_cert = "/etc/kipuka/ra-new.pem"
auth_key = "/etc/kipuka/ra-new-key.pem"

[[ha.group]]
name = "migration"
ca_ids = ["old-ca", "new-ca"]
strategy = "weighted"
weights = { "old-ca" = 90, "new-ca" = 10 }

[[est.label]]
name = "server"
ca_id = "old-ca"
profile = "serverCert"

Migration procedure:

  1. Start with weights = { "old-ca" = 100, "new-ca" = 0 } (new CA online but unused)
  2. Update to weights = { "old-ca" = 90, "new-ca" = 10 } (10% canary traffic)
  3. Monitor logs and metrics; if no issues, increase to 80/20, 50/50, 20/80
  4. Finish with weights = { "old-ca" = 0, "new-ca" = 100 } (full cutover)
  5. Remove old-ca from the configuration once fully decommissioned

No client reconfiguration required—weights can be adjusted without restarting kipuka.

Database Backends

kipuka uses a database to store issued certificates, audit logs, OTP tokens, and certificate authority state. Three backend options are supported: SQLite, PostgreSQL, and MariaDB. The database backend is configured via the [db] section in kipuka.toml.

SQLite

SQLite is the default backend and requires zero configuration. It is ideal for single-node deployments, development environments, and small to medium workloads.

URL Format

[db]
url = "sqlite:///var/lib/kipuka/kipuka.db"
auto_migrate = true

For in-memory testing:

[db]
url = "sqlite://:memory:"

Features and Characteristics

  • Write-Ahead Logging (WAL): kipuka enables WAL mode by default, allowing concurrent reads while a write transaction is in progress.
  • File Permissions: The database file should be owned by the kipuka service user with mode 0600 to prevent unauthorized access.
  • Single-Writer: SQLite supports only one concurrent writer. This is not a limitation for single-node deployments but makes it unsuitable for high-availability configurations with shared state.
  • No Network Access: The database must be on the local filesystem. Network-mounted filesystems (NFS, SMB) are not recommended due to locking and performance issues.

Backup

SQLite databases can be backed up using:

  • File copy: While the WAL is checkpointed (kipuka handles this automatically), the database file can be copied. Use a consistent snapshot method to avoid corruption.

  • sqlite3 .backup: The SQLite command-line tool provides a .backup command for online backups:

    sqlite3 /var/lib/kipuka/kipuka.db ".backup /backup/kipuka-$(date +%Y%m%d).db"
    

Best For

  • Single-node deployments
  • Development and testing
  • Small to medium workloads (< 1000 requests/minute)
  • Scenarios where operational simplicity is prioritized

Limitations

  • Single writer (not suitable for multi-node HA with shared state)
  • No network access (cannot be shared across nodes)
  • Limited to local filesystem performance

PostgreSQL

PostgreSQL is the recommended backend for production multi-node deployments. It provides robust support for concurrent writes, network access, replication, and point-in-time recovery.

URL Format

[db]
url = "postgres://kipuka:[email protected]:5432/kipuka?sslmode=require"
max_connections = 20
connect_timeout = "5s"
auto_migrate = true

Features and Characteristics

  • Concurrent Writes: Multiple kipuka nodes can write to the database simultaneously.
  • Network Access: The database can be accessed over the network, enabling multi-node deployments.
  • Replication: PostgreSQL supports streaming replication for read replicas and high availability.
  • Point-in-Time Recovery (PITR): WAL archiving enables recovery to any point in time.
  • Connection Pooling: kipuka maintains a connection pool sized by max_connections.

Connection Pool Tuning

The max_connections setting controls the size of kipuka’s connection pool to PostgreSQL:

  • Default: 5 (suitable for low-load scenarios)
  • Formula: Start with workers * 2 and adjust based on monitoring.
  • Too Low: Connection pool exhaustion under load, causing requests to queue.
  • Too High: Unnecessary resource consumption; may exceed PostgreSQL’s max_connections limit (default 100).

Monitor pool utilization via the Admin API endpoint /admin/health, which reports connection pool statistics.

PostgreSQL Configuration

Ensure max_connections in postgresql.conf is set higher than the sum of all kipuka nodes’ max_connections. Leave headroom for other applications and administrative connections.

# postgresql.conf
max_connections = 100

SSL Connections

Append ?sslmode=require to the URL to enforce SSL/TLS connections:

url = "postgres://kipuka:${DB_PASSWORD}@db.example.com:5432/kipuka?sslmode=require"

Best For

  • Production environments
  • High-availability deployments (with replication)
  • High-throughput scenarios (> 1000 requests/minute)
  • Multi-node clusters with shared state

MariaDB

MariaDB is an alternative to PostgreSQL and supports Galera Cluster for synchronous multi-master replication. It is well-suited for organizations already standardized on MariaDB or requiring Galera-based high availability.

URL Format

[db]
url = "mysql://kipuka:[email protected]:3306/kipuka"
max_connections = 20
connect_timeout = "5s"
auto_migrate = true

Features and Characteristics

  • Galera Cluster: Provides synchronous multi-master replication, allowing writes to any node in the cluster.
  • Concurrent Writes: Multiple kipuka nodes can write to the database simultaneously.
  • Network Access: The database can be accessed over the network.

Galera Considerations

  • Replication Format: Use ROW-based replication (binlog_format=ROW) for deterministic replication.
  • Synchronous Reads: Set wsrep_sync_wait=1 to ensure reads reflect writes from all nodes (reads-after-writes consistency).
# MariaDB Galera configuration
wsrep_sync_wait=1
binlog_format=ROW

Best For

  • Organizations standardized on MariaDB
  • Galera Cluster-based high availability
  • Multi-master replication requirements

Connection URL Formats Summary

BackendURL FormatDefault Port
SQLitesqlite://path/to/dbN/A
PostgreSQLpostgres://user:pass@host:port/db5432
MariaDBmysql://user:pass@host:port/db3306

Migrations

kipuka uses versioned, idempotent migrations to manage database schema changes. Migrations can be applied automatically on startup or run manually as a separate step.

Automatic Migrations

Set auto_migrate = true (the default) to apply pending migrations on startup:

[db]
auto_migrate = true

This is convenient for development and single-node deployments, but may not be suitable for production environments where migrations should be reviewed and tested before deployment.

Manual Migrations

Set auto_migrate = false to disable automatic migrations:

[db]
auto_migrate = false

Then run migrations manually using the kipuka migrate command:

# Show pending migrations
kipuka migrate status

# Apply pending migrations
kipuka migrate run

Best Practices

  • Production: Run migrations as a separate step before starting the server. This allows you to review migrations, test them in a staging environment, and coordinate downtime if necessary.
  • Development: Use auto_migrate = true for convenience.
  • Idempotency: Migrations are idempotent and can be re-run safely. If a migration is interrupted, re-running it will complete the operation.

max_connections Tuning

The max_connections setting controls the size of kipuka’s database connection pool. Tuning this parameter is critical for balancing resource utilization and performance.

Default

The default is 5, which is suitable for SQLite and small deployments with low concurrency.

Tuning Formula

Start with the formula:

max_connections = workers * 2

Adjust based on monitoring and observed load. For example, if kipuka is configured with 4 workers, start with max_connections = 8.

Symptoms of Incorrect Settings

  • Too Low: Connection pool exhaustion under load. Requests will queue waiting for an available connection, increasing latency.
  • Too High: Unnecessary resource consumption (memory, file descriptors). May exceed the database’s max_connections limit, causing connection failures.

Monitoring

Use the Admin API endpoint /admin/health to monitor connection pool utilization:

curl http://localhost:8080/admin/health

The response includes metrics such as active connections, idle connections, and pool size.

PostgreSQL Configuration

Ensure PostgreSQL’s max_connections setting is higher than the sum of all kipuka nodes’ max_connections. For example:

  • 3 kipuka nodes, each with max_connections = 20 → 60 total
  • PostgreSQL max_connections = 100 → 40 connections available for other applications and administrative tasks

Credential Security

Database connection URLs often contain sensitive credentials (usernames and passwords). Follow these best practices to protect credentials:

Environment Variable Substitution

kipuka supports environment variable substitution in the url field using the ${ENV_VAR} syntax:

[db]
url = "postgres://kipuka:${DB_PASSWORD}@db.example.com:5432/kipuka"

Set the environment variable before starting kipuka:

export DB_PASSWORD="your-secure-password"
kipuka run

Or use a systemd service file with EnvironmentFile:

[Service]
EnvironmentFile=/etc/kipuka/db.env
ExecStart=/usr/bin/kipuka run

Never Commit Passwords

Never commit plaintext passwords to version control. Use:

  • Environment variables (as shown above)
  • Secret management systems (e.g., HashiCorp Vault, AWS Secrets Manager)
  • Encrypted configuration files with restricted file permissions

Example: Secrets Manager Integration

For production deployments, consider integrating with a secrets manager:

# Fetch password from secrets manager
export DB_PASSWORD=$(vault kv get -field=password secret/kipuka/db)

# Start kipuka
kipuka run

This ensures credentials are never written to disk in plaintext and can be rotated without modifying configuration files.

Audit Logging

kipuka implements comprehensive audit logging designed to meet NIAP Protection Profile for Certification Authorities requirements, specifically FAU_GEN.1 (Audit Data Generation). All security-relevant events are recorded with sufficient detail for forensic analysis and compliance verification.

NIAP FAU_GEN.1 Compliance

kipuka’s audit logging implementation satisfies the following FAU_GEN.1 requirements:

  • Completeness: All security-relevant events are recorded, including certificate issuance, revocation, authentication attempts, and administrative actions.
  • Sufficient Detail: Each audit record contains timestamp (UTC), event type, outcome (success/failure), subject identity, source IP address, and event-specific data necessary for forensic analysis.
  • Tamper Evidence: Audit records are append-only. The database audit tables permit only INSERT operations; UPDATE and DELETE operations are prohibited at the schema level.
  • Reliability: Audit records are written synchronously to the database, with optional asynchronous replication to file and syslog destinations.

Audit Event Types

kipuka records 22 distinct event types across four categories:

Event NameCategoryDescription
cert.issuedCertificateCertificate successfully issued
cert.deniedCertificateCertificate request denied (policy violation)
cert.revokedCertificateCertificate revoked
cert.renewedCertificateCertificate renewed via simplereenroll
cert.expiredCertificateCertificate reached expiration
enroll.requestEnrollmentsimpleenroll request received
enroll.successEnrollmentEnrollment completed successfully
enroll.failureEnrollmentEnrollment failed
reenroll.requestEnrollmentsimplereenroll request received
reenroll.successEnrollmentRe-enrollment completed successfully
reenroll.failureEnrollmentRe-enrollment failed
auth.mtls.successAuthenticationmTLS authentication succeeded
auth.mtls.failureAuthenticationmTLS authentication failed
auth.otp.successAuthenticationOTP authentication succeeded
auth.otp.failureAuthenticationOTP authentication failed
auth.otp.lockoutAuthenticationOTP account locked due to excessive failures
auth.gssapi.successAuthenticationGSSAPI/Kerberos authentication succeeded
auth.gssapi.failureAuthenticationGSSAPI/Kerberos authentication failed
admin.accessAdminAdmin API endpoint accessed
otp.createdAdminOTP token provisioned
ca.health.changedSystemCA health state changed (HA)
server.startupSystemkipuka server started
server.shutdownSystemkipuka server stopped

Audit Destinations

kipuka supports three audit destinations that operate independently. The database destination is always active; file and syslog destinations are optional.

Database (Always Active)

All audit events are written to the audit_log table in the kipuka database. This destination is always active and cannot be disabled.

Characteristics:

  • INSERT-only: The database schema enforces that audit records can only be inserted, never updated or deleted.
  • Queryable: Audit records can be retrieved via the Admin API for compliance reporting and forensic analysis.
  • Persistent: Records remain in the database until explicitly purged by an administrator through documented procedures.

File (Append-Only JSON)

When configured, kipuka writes audit events to a file in JSON Lines format (one JSON object per line). The file is opened with the O_APPEND flag to ensure records cannot be overwritten.

Configuration:

[audit]
file = "/var/log/kipuka/audit.json"

Rotation: Use logrotate with the copytruncate option or equivalent log rotation tools. Example logrotate configuration:

/var/log/kipuka/audit.json {
    daily
    rotate 90
    compress
    delaycompress
    copytruncate
    missingok
    notifempty
}

Example JSON Record:

{
  "timestamp": "2026-06-24T14:32:10.123456Z",
  "event_type": "cert.issued",
  "outcome": "success",
  "subject": "CN=server.example.com",
  "source_ip": "192.0.2.45",
  "serial": "4A:3F:21:8C:09:77:34:2B",
  "issuer": "CN=Example CA",
  "not_before": "2026-06-24T14:32:00Z",
  "not_after": "2027-06-24T14:32:00Z"
}

Syslog (RFC 5424, Optional TLS)

kipuka can forward audit events to a remote syslog server using RFC 5424 format. UDP, TCP, and TLS-encrypted TCP are supported.

Configuration:

[audit]
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local0"

Supported URL Schemes:

  • udp://host:port - Unencrypted UDP (not recommended for production)
  • tcp://host:port - Unencrypted TCP
  • tcp+tls://host:port - TLS-encrypted TCP (recommended for NIAP compliance)

Syslog Facilities: The syslog_facility option accepts local0 through local7. Default is local0.

Structured Data: Audit events are encoded as RFC 5424 structured data elements. Example syslog message:

<134>1 2026-06-24T14:32:10.123456Z server1 kipuka 12345 cert.issued [audit event_type="cert.issued" outcome="success" subject="CN=server.example.com" source_ip="192.0.2.45" serial="4A:3F:21:8C:09:77:34:2B"]

Event Filtering

The events array in the [audit] section controls which events are forwarded to file and syslog destinations. The database always records all events regardless of this filter.

Default Behavior: If the events array is omitted or empty, all event types are forwarded to file and syslog.

Filtering Example: To log only security-critical events to file/syslog:

[audit]
file = "/var/log/kipuka/audit.json"
events = [
    "cert.issued",
    "cert.denied",
    "cert.revoked",
    "auth.otp.failure",
    "auth.otp.lockout",
    "auth.mtls.failure",
    "admin.access",
]

In this configuration, successful routine operations (e.g., enroll.success, auth.otp.success) are recorded only in the database, reducing file and syslog volume while maintaining complete audit trail in the database for compliance queries.

Certificate Data in Audit Records

By default, audit records for certificate lifecycle events include metadata (serial number, subject, issuer, validity dates) but not the full certificate. The include_cert_data option controls whether the complete PEM-encoded certificate is included.

Default (include_cert_data = false):

{
  "timestamp": "2026-06-24T14:32:10.123456Z",
  "event_type": "cert.issued",
  "serial": "4A:3F:21:8C:09:77:34:2B",
  "subject": "CN=server.example.com",
  "issuer": "CN=Example CA",
  "not_before": "2026-06-24T14:32:00Z",
  "not_after": "2027-06-24T14:32:00Z"
}

With Full Certificate Data (include_cert_data = true):

{
  "timestamp": "2026-06-24T14:32:10.123456Z",
  "event_type": "cert.issued",
  "serial": "4A:3F:21:8C:09:77:34:2B",
  "subject": "CN=server.example.com",
  "issuer": "CN=Example CA",
  "not_before": "2026-06-24T14:32:00Z",
  "not_after": "2027-06-24T14:32:00Z",
  "certificate": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAEo/IYwJdzQr...\n-----END CERTIFICATE-----"
}

Trade-offs:

  • Enabling include_cert_data significantly increases audit log size (typical certificate is 1-2 KB).
  • Required by some compliance frameworks (e.g., certain government PKI policies) for complete certificate lifecycle tracking.
  • Useful for forensic analysis when the original certificate is no longer available in the database.

Configuration Examples

Minimal Configuration (File Only)

[audit]
file = "/var/log/kipuka/audit.json"

This configuration writes all audit events to a local file with default settings.

Production Configuration (File + Syslog over TLS)

[audit]
file = "/var/log/kipuka/audit.json"
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local1"
events = [
    "cert.issued",
    "cert.denied",
    "cert.revoked",
    "auth.otp.failure",
    "auth.otp.lockout",
    "auth.mtls.failure",
    "admin.access",
    "otp.created",
    "server.startup",
    "server.shutdown",
]

This configuration:

  • Logs all events to the database (always active).
  • Logs security-critical events to both file and remote syslog over TLS.
  • Reduces file and syslog volume by filtering routine successful operations.

NIAP-Compliant Configuration

[audit]
file = "/var/log/kipuka/audit.json"
syslog = "tcp+tls://syslog.example.com:6514"
syslog_facility = "local0"
include_cert_data = true
# events array omitted: all events forwarded to file and syslog

This configuration:

  • Logs all 22 event types to database, file, and syslog.
  • Uses TLS-encrypted syslog for confidentiality and integrity.
  • Includes full certificate data in audit records for complete lifecycle tracking.
  • Meets NIAP FAU_GEN.1 requirements for completeness, detail, and tamper evidence.

Note: Enabling include_cert_data will substantially increase storage requirements. Plan for approximately 2-5 KB per certificate lifecycle event (issuance, renewal, revocation).

Admin API

The Admin API provides privileged endpoints for server management, monitoring, OTP provisioning, and certificate lifecycle operations. It runs on a separate port from the EST service to enable network-level access control and is disabled by default for security.

Enabling the Admin API

The Admin API is disabled by default. To enable it, configure both the [admin] section and the admin_listen address in [server].

The Admin API runs on a separate port (default 9443) to allow network-level access control. Bind to 127.0.0.1 for localhost-only access, or to a management VLAN IP for remote administration:

[server]
admin_listen = "127.0.0.1:9443"

[admin]
enabled = true
auth = "mtls"
trust_anchors = "/etc/kipuka/admin-ca.pem"

Authentication Methods

The Admin API supports three authentication modes configured via the auth parameter:

mTLS Authentication

Admin clients must present a certificate signed by the admin trust anchors CA. This is the most secure option and recommended for production environments.

[admin]
enabled = true
auth = "mtls"
trust_anchors = "/etc/kipuka/admin-ca.pem"

Bearer Token Authentication

Admin clients provide a bearer token in the Authorization header. The token value is loaded from the environment variable specified by bearer_token_env.

[admin]
enabled = true
auth = "bearer"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Clients authenticate using:

curl -H "Authorization: Bearer $KIPUKA_ADMIN_TOKEN" \
  https://localhost:9443/admin/health

Both Authentication

Either mTLS or bearer token accepted. Useful during migration periods or for mixed tooling environments.

[admin]
enabled = true
auth = "both"
trust_anchors = "/etc/kipuka/admin-ca.pem"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Endpoints Overview

MethodPathDescription
GET/admin/healthServer health and component status
GET/admin/health/readyReadiness probe (for Kubernetes)
GET/admin/health/liveLiveness probe (for Kubernetes)
GET/admin/caList all configured CAs with status
GET/admin/ca/{id}Get specific CA details and health
GET/admin/ca/{id}/chainGet CA certificate chain (PEM)
POST/admin/otpGenerate a new OTP token
GET/admin/otpList active (unused) OTP tokens
DELETE/admin/otp/{token_id}Revoke an unused OTP token
GET/admin/certsList issued certificates (paginated)
GET/admin/certs/{serial}Get certificate details by serial
POST/admin/certs/{serial}/revokeRevoke a certificate
GET/admin/auditQuery audit log (paginated, filterable)
GET/admin/metricsPrometheus-format metrics

OTP Provisioning Workflow

The OTP provisioning workflow enables secure initial enrollment for devices that do not yet have certificates.

Step 1: Admin Generates OTP

The administrator creates a new OTP token with specified constraints:

curl -s --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  -X POST https://localhost:9443/admin/otp \
  -H "Content-Type: application/json" \
  -d '{"label": "device-42", "ttl": "24h", "max_uses": 1}'

Response:

{
  "token_id": "01234567-89ab-cdef-0123-456789abcdef",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires": "2026-06-25T14:30:00Z",
  "label": "device-42",
  "max_uses": 1
}

Parameters:

  • label: Human-readable identifier for the token (for audit logs)
  • ttl: Time-to-live (e.g., “24h”, “7d”, “1h30m”)
  • max_uses: Maximum number of times the token can be used (typically 1)

Step 2: Admin Delivers Token to Client Operator

The token value must be delivered out-of-band through a secure channel:

  • Encrypted email
  • Ticketing system
  • Secure messaging platform
  • In-person handoff

Never transmit OTP tokens over unencrypted channels.

Step 3: Client Uses OTP for Initial Enrollment

The client operator uses the OTP token for initial EST enrollment:

curl --cacert ca.pem \
  -u ":eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  --data-binary @csr.pem \
  -H "Content-Type: application/pkcs10" \
  -o cert.p7 \
  https://est.example.com/.well-known/est/simpleenroll

Note the colon prefix in the -u flag: HTTP Basic Auth uses username:password format, and OTP tokens are submitted as the password with an empty username.

Step 4: OTP Consumed After Successful Enrollment

After successful enrollment, the OTP is marked as consumed if max_uses=1. Subsequent attempts to use the same token will be rejected.

Step 5: Future Renewals Use Certificate-Based mTLS

Once the device has a certificate, all future operations (reenrollment, renewal) use the issued certificate for mTLS authentication. The OTP is no longer required.

CA Status Monitoring

List All CAs

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/ca

Response:

[
  {
    "id": "primary-ca",
    "name": "Primary Issuing CA",
    "status": "healthy",
    "last_check": "2026-06-24T12:34:56Z",
    "cert_not_after": "2028-12-31T23:59:59Z",
    "certs_issued_today": 42,
    "certs_issued_total": 15234
  },
  {
    "id": "backup-ca",
    "name": "Backup Issuing CA",
    "status": "healthy",
    "last_check": "2026-06-24T12:34:56Z",
    "cert_not_after": "2029-06-30T23:59:59Z",
    "certs_issued_today": 0,
    "certs_issued_total": 0
  }
]

Status values:

  • healthy: CA is operational and passing all health checks
  • degraded: CA is operational but experiencing issues (e.g., approaching expiration)
  • unhealthy: CA is not operational (e.g., expired, HSM unreachable)

Get Specific CA Details

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/ca/primary-ca

This endpoint provides detailed status for a single CA, including backend-specific health information.

Get CA Certificate Chain

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/ca/primary-ca/chain

Returns the CA certificate chain in PEM format. This is useful for distributing trust anchors to clients.

Aggregate Health Status

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/health

Response:

{
  "status": "healthy",
  "components": {
    "database": "healthy",
    "cas": {
      "primary-ca": "healthy",
      "backup-ca": "healthy"
    },
    "hsm": "healthy"
  },
  "uptime": "3d 14h 22m"
}

Use this endpoint for monitoring dashboards and alerting. Aggregate status reflects the worst status of any component:

  • All components healthy: healthy
  • Any component degraded: degraded
  • Any component unhealthy: unhealthy

Kubernetes Probes

For Kubernetes deployments, use the dedicated probe endpoints:

Readiness probe (server is ready to accept traffic):

curl https://localhost:9443/admin/health/ready

Liveness probe (server is running):

curl https://localhost:9443/admin/health/live

Configure in your Deployment manifest:

livenessProbe:
  httpGet:
    path: /admin/health/live
    port: 9443
    scheme: HTTPS
  initialDelaySeconds: 10
  periodSeconds: 30

readinessProbe:
  httpGet:
    path: /admin/health/ready
    port: 9443
    scheme: HTTPS
  initialDelaySeconds: 5
  periodSeconds: 10

Certificate Management

List Issued Certificates

List certificates with optional filtering and pagination:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  "https://localhost:9443/admin/certs?status=active&ca=primary-ca&page=1&per_page=50"

Query parameters:

  • status: Filter by certificate status (active, revoked, expired)
  • ca: Filter by issuing CA ID
  • subject: Filter by subject DN (substring match)
  • page: Page number (1-indexed)
  • per_page: Results per page (default 50, max 1000)

Response:

{
  "certs": [
    {
      "serial": "1a2b3c4d5e6f7890",
      "subject": "CN=device-42.example.com",
      "issuer": "CN=Primary Issuing CA",
      "not_before": "2026-06-24T00:00:00Z",
      "not_after": "2027-06-24T23:59:59Z",
      "status": "active",
      "ca_id": "primary-ca"
    }
  ],
  "total": 15234,
  "page": 1,
  "per_page": 50
}

Get Certificate Details

Retrieve detailed information for a specific certificate by serial number:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/certs/1a2b3c4d5e6f7890

Response includes full certificate details, issuance metadata, and audit trail.

Revoke a Certificate

Revoke a certificate by serial number with a reason code:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  -X POST https://localhost:9443/admin/certs/1a2b3c4d5e6f7890/revoke \
  -H "Content-Type: application/json" \
  -d '{"reason": "keyCompromise"}'

Valid reason codes (per RFC 5280):

  • unspecified: Default reason
  • keyCompromise: Private key compromise
  • caCompromise: CA key compromise
  • affiliationChanged: Subject changed affiliation
  • superseded: Certificate replaced
  • cessationOfOperation: Certificate no longer needed
  • privilegeWithdrawn: Certificate privileges revoked

Response:

{
  "serial": "1a2b3c4d5e6f7890",
  "status": "revoked",
  "revocation_time": "2026-06-24T14:30:00Z",
  "revocation_reason": "keyCompromise"
}

The certificate is immediately marked as revoked in the database and will appear in the next CRL update.

Audit Log Queries

Query the audit log for security analysis and compliance:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  "https://localhost:9443/admin/audit?action=enroll&start=2026-06-01&end=2026-06-24&page=1&per_page=100"

Query parameters:

  • action: Filter by action type (enroll, reenroll, serverkeygen, revoke, otp_generate, otp_use)
  • subject: Filter by certificate subject DN
  • ca: Filter by CA ID
  • start: Start timestamp (ISO 8601)
  • end: End timestamp (ISO 8601)
  • page: Page number (1-indexed)
  • per_page: Results per page (default 100, max 1000)

Response:

{
  "events": [
    {
      "timestamp": "2026-06-24T10:15:30Z",
      "action": "enroll",
      "subject": "CN=device-42.example.com",
      "ca_id": "primary-ca",
      "result": "success",
      "client_ip": "10.0.1.42",
      "auth_method": "otp",
      "serial": "1a2b3c4d5e6f7890"
    }
  ],
  "total": 1523,
  "page": 1,
  "per_page": 100
}

Metrics

The Admin API exposes Prometheus-format metrics for monitoring:

curl --cert admin.pem --key admin-key.pem \
  --cacert admin-ca.pem \
  https://localhost:9443/admin/metrics

Key metrics include:

  • kipuka_enrollments_total: Counter of enrollment operations by CA and result
  • kipuka_reenrollments_total: Counter of reenrollment operations by CA and result
  • kipuka_revocations_total: Counter of revocation operations by CA and reason
  • kipuka_otp_generated_total: Counter of OTP tokens generated
  • kipuka_otp_consumed_total: Counter of OTP tokens consumed
  • kipuka_request_duration_seconds: Histogram of request durations by endpoint
  • kipuka_active_certificates: Gauge of currently active certificates by CA
  • kipuka_ca_health: Gauge of CA health status (1=healthy, 0=unhealthy)

Configure Prometheus to scrape the Admin API endpoint with appropriate mTLS credentials.

Dogtag PKI Integration

Overview

kipuka can use Red Hat Certificate System (Dogtag PKI) as a backend CA instead of, or alongside, file-based or HSM-backed local CAs. This integration enables organizations already running Dogtag/RHCS to add EST enrollment capabilities without modifying their existing PKI infrastructure.

Architecture:

  • Dogtag provides a full-featured PKI with REST API, comprehensive audit logging, HSM support, and complete certificate lifecycle management
  • kipuka acts as an EST frontend (Registration Authority) to Dogtag’s CA subsystem
  • The integration preserves all Dogtag policies, approval workflows, and certificate profiles while exposing EST protocol endpoints

Benefits:

  • Leverage existing Dogtag infrastructure and operational expertise
  • Maintain centralized certificate policy enforcement in Dogtag profiles
  • Benefit from Dogtag’s enterprise features: HSM integration, audit trails, approval workflows, Key Recovery Authority (KRA)
  • Add EST enrollment to IoT devices, network equipment, and mobile endpoints without deploying new CA infrastructure

REST API Client

kipuka communicates with Dogtag exclusively via its REST API, typically exposed at https://dogtag.example.com:8443/ca/rest/certrequests.

Authentication

kipuka authenticates to Dogtag using a client certificate (agent cert) that has enrollment privileges. This certificate must be issued by the Dogtag CA itself and associated with an agent user in the Dogtag database.

Obtaining an agent certificate:

  1. On the Dogtag server, create an agent user (if not already present):

    pki -d /var/lib/pki/pki-tomcat/alias -c <password> \
        ca-user-add kipuka-agent --fullName "kipuka EST RA"
    
  2. Generate a certificate request and submit it for the agent certificate:

    pki -d /var/lib/pki/pki-tomcat/alias -c <password> \
        client-cert-request "CN=kipuka EST RA,O=Example Corp" \
        --profile caAgentServerCert
    
  3. Approve and issue the certificate via the Dogtag web UI or CLI

  4. Export the agent certificate and key for use by kipuka

Configuration

Configure Dogtag as a CA entry with type = "dogtag":

[[ca]]
id = "dogtag-ca"
name = "Dogtag Enterprise CA"
type = "dogtag"

# Dogtag instance URL (without /ca/rest suffix)
url = "https://dogtag.example.com:8443"

# Agent certificate for authenticating to Dogtag
agent_cert = "/etc/kipuka/dogtag/agent.pem"
agent_key = "/etc/kipuka/dogtag/agent-key.pem"

# Dogtag CA certificate chain for TLS verification
ca_cert = "/etc/kipuka/dogtag/ca-chain.pem"

# Default certificate profile for this CA
profile = "caServerCert"

# Optional: Request timeout (default: 30s)
timeout = "30s"

# Optional: Enable certificate caching to reduce Dogtag load
cache_certificates = true
cache_ttl = "1h"

File formats:

  • agent_cert and ca_cert: PEM-encoded X.509 certificates
  • agent_key: PEM-encoded RSA or EC private key (can be encrypted; kipuka will prompt for passphrase)

Enrollment

When kipuka receives an EST /simpleenroll request, it translates the PKCS#10 CSR into a Dogtag certificate enrollment request and submits it to the Dogtag REST API.

Profile Mapping

Dogtag uses certificate profiles (e.g., caServerCert, caUserCert, caTPSCert) to define certificate content, extensions, and policy constraints. kipuka maps EST labels to Dogtag profiles:

[[est.label]]
name = "server-tls"
ca_id = "dogtag-ca"
dogtag_profile = "caServerCert"

[[est.label]]
name = "client-auth"
ca_id = "dogtag-ca"
dogtag_profile = "caUserCert"

[[est.label]]
name = "token-signing"
ca_id = "dogtag-ca"
dogtag_profile = "caTPSCert"

Default profile: If no label is provided in the EST request, the profile specified in the [[ca]] entry is used.

Enrollment Flow

  1. Client sends EST /simpleenroll request with PKCS#10 CSR
  2. kipuka validates the CSR and authenticates the client (via HTTP Basic or TLS client cert)
  3. kipuka determines the Dogtag profile based on the EST label
  4. kipuka submits the CSR to Dogtag: POST /ca/rest/certrequests
  5. Dogtag processes the request according to the profile policy:
    • If auto-approval is enabled in the profile, the certificate is issued immediately
    • If approval is required, the request enters pending state
  6. kipuka polls Dogtag until the certificate is issued (or timeout expires)
  7. kipuka returns the issued certificate to the EST client in PKCS#7 format

Manual Approval Workflow

If the Dogtag profile requires manual approval (common for high-assurance certificates):

  1. The enrollment request remains in Dogtag’s pending queue
  2. An agent approves the request via Dogtag web UI or CLI:
    pki ca-cert-request-review <request_id> --action approve
    
  3. kipuka’s next poll retrieves the issued certificate
  4. The EST client receives the certificate (this may take seconds to minutes depending on poll interval)

Polling configuration:

[[ca]]
id = "dogtag-ca"
type = "dogtag"
# ... other config ...

# Poll interval for pending certificate requests (default: 5s)
poll_interval = "5s"

# Maximum time to wait for approval (default: 5m)
approval_timeout = "5m"

If approval_timeout expires before the certificate is issued, kipuka returns an HTTP 202 response to the client with a retry-after header.

Revocation

kipuka’s Admin API revocation calls are forwarded to Dogtag’s revocation endpoint.

Revoke via Admin API

curl -X POST https://kipuka.example.com/admin/revoke \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "serial": "0x1A2B3C4D",
    "reason": "keyCompromise"
  }'

Revocation Flow

  1. kipuka receives the revocation request via Admin API
  2. kipuka maps the serial number to the issuing CA (in this case, dogtag-ca)
  3. kipuka submits a revocation request to Dogtag: POST /ca/rest/agent/certs/<serial>/revoke
  4. Dogtag revokes the certificate and updates its internal database
  5. Dogtag automatically updates CRLs and OCSP responder (if enabled)
  6. kipuka returns success to the caller

Revocation Reason Mapping

kipuka maps its revocation reason codes to Dogtag’s reason codes per RFC 5280:

kipuka ReasonDogtag Reason CodeRFC 5280 Code
unspecified00
keyCompromise11
caCompromise22
affiliationChanged33
superseded44
cessationOfOperation55
certificateHold66
removeFromCRL88
privilegeWithdrawn99
aaCompromise1010

Note: Dogtag does not support removeFromCRL (un-hold) via the REST API for certificates revoked with certificateHold. Use the Dogtag CLI for un-hold operations.

KRA Server-Side Key Generation

EST’s /serverkeygen endpoint can use Dogtag’s Key Recovery Authority (KRA) subsystem for server-side key generation and archival.

Overview

When server-side key generation is requested:

  1. kipuka forwards the request to Dogtag with the KRA archival flag enabled
  2. Dogtag’s KRA subsystem generates the key pair server-side
  3. The private key is encrypted and archived in the KRA database
  4. Dogtag issues the certificate using the generated public key
  5. kipuka receives both the certificate and the encrypted private key
  6. The private key is returned to the EST client as a PKCS#8 EncryptedPrivateKeyInfo wrapped in the EST response

Prerequisites

  • Dogtag KRA subsystem must be installed and configured
  • The KRA must be configured to communicate with the CA subsystem
  • The agent certificate used by kipuka must have key archival privileges

Configuration

[[ca]]
id = "dogtag-ca"
type = "dogtag"
url = "https://dogtag.example.com:8443"

# KRA endpoint (often same host as CA)
kra_url = "https://dogtag.example.com:8443"

# KRA agent credentials (may be same as CA agent or separate)
kra_agent_cert = "/etc/kipuka/dogtag/kra-agent.pem"
kra_agent_key = "/etc/kipuka/dogtag/kra-agent-key.pem"

# ... other CA config ...

If kra_url is not specified, server-side key generation is disabled for this CA.

EST Request

curl -X POST https://kipuka.example.com/.well-known/est/serverkeygen \
  --user client:password \
  --cacert ca.pem \
  -H "Content-Type: application/pkcs10" \
  --data-binary @csr.p10 \
  -o response.p7

The response is a PKCS#7 structure containing:

  • The issued certificate
  • The encrypted private key (PKCS#8 EncryptedPrivateKeyInfo)
  • Optional additional CA certificates

Client-side decryption: The client must decrypt the private key using the password provided during enrollment (passed in the CSR’s challenge password attribute, or via out-of-band key).

Key Recovery

If a client loses their private key, authorized users can recover it from the KRA:

pki kra-key-recover --keyID <key_id> --output recovered-key.p12

This operation requires dual approval from KRA agents (depending on KRA policy).

Multi-CA Pool with Circuit Breaker

Multiple Dogtag instances can be configured as separate CA entries to provide high availability and load distribution.

Configuration

[[ca]]
id = "dogtag-primary"
type = "dogtag"
url = "https://dogtag1.example.com:8443"
agent_cert = "/etc/kipuka/dogtag/agent.pem"
agent_key = "/etc/kipuka/dogtag/agent-key.pem"
ca_cert = "/etc/kipuka/dogtag/ca-chain.pem"
profile = "caServerCert"

[[ca]]
id = "dogtag-secondary"
type = "dogtag"
url = "https://dogtag2.example.com:8443"
agent_cert = "/etc/kipuka/dogtag/agent.pem"
agent_key = "/etc/kipuka/dogtag/agent-key.pem"
ca_cert = "/etc/kipuka/dogtag/ca-chain.pem"
profile = "caServerCert"

[ha]
enabled = true
strategy = "active-passive"

[[ha.group]]
name = "dogtag-pool"
ca_ids = ["dogtag-primary", "dogtag-secondary"]

# Health check interval
health_check_interval = "30s"

# Circuit breaker thresholds
failure_threshold = 3
success_threshold = 2
timeout = "10s"

Failover Behavior

Active-Passive Strategy:

  • All requests are routed to dogtag-primary
  • If dogtag-primary fails health checks (3 consecutive failures), the circuit breaker trips
  • Traffic is routed to dogtag-secondary while dogtag-primary is unavailable
  • Once dogtag-primary passes health checks (2 consecutive successes), traffic is restored to primary

Active-Active Strategy (future):

[ha]
enabled = true
strategy = "active-active"

[[ha.group]]
name = "dogtag-pool"
ca_ids = ["dogtag-primary", "dogtag-secondary"]
load_balancing = "round-robin"  # or "least-connections"

Health Checks

kipuka performs health checks against each Dogtag instance:

GET /ca/rest/certs/ca HTTP/1.1
Host: dogtag1.example.com:8443

A successful response (HTTP 200 with the CA certificate) indicates the instance is healthy.

Monitoring endpoint:

curl https://kipuka.example.com/health/ca

Response:

{
  "dogtag-primary": {
    "status": "healthy",
    "last_check": "2026-06-24T10:30:00Z",
    "consecutive_failures": 0
  },
  "dogtag-secondary": {
    "status": "circuit_open",
    "last_check": "2026-06-24T10:30:00Z",
    "consecutive_failures": 5
  }
}

Full CMC Passthrough (RFC 5272)

When fullcmc = true in the [est] configuration, kipuka can pass Full CMC (Certificate Management over CMS) requests directly to Dogtag.

Overview

Full CMC (RFC 5272/5273/5274) supports complex certificate management operations beyond simple enrollment:

  • Batch enrollment (multiple certificates in a single request)
  • Certificate update/renewal
  • Key recovery requests
  • Revocation requests
  • Get certificate/CRL operations

Dogtag is one of the few CAs that fully implements RFC 5272/5273/5274, making it ideal for enterprise CMC deployments.

Configuration

[est]
enabled = true
fullcmc = true

# Optional: CMC-specific settings
[est.cmc]
# Maximum CMC message size (default: 10MB)
max_message_size = "10MB"

# Allow unsigned CMC requests (default: false)
allow_unsigned = false

CMC Request Flow

  1. Client constructs a Full CMC request (typically using CMCRequest tool or smart card management software)
  2. Client submits the CMC request to kipuka’s EST endpoint: POST /.well-known/est/fullcmc
  3. kipuka validates the CMC request structure and authenticates the client
  4. kipuka forwards the CMC payload to Dogtag: POST /ca/rest/certrequests/cmc
  5. Dogtag processes the CMC request according to its internal policies
  6. Dogtag returns a CMC Full PKI Response
  7. kipuka returns the CMC response to the EST client

Example: Batch Enrollment via CMC

# Create a CMC request with multiple CSRs
CMCRequest \
  -d /path/to/nssdb \
  -i csr1.pem \
  -i csr2.pem \
  -i csr3.pem \
  -o batch-request.cmc

# Submit to kipuka
curl -X POST https://kipuka.example.com/.well-known/est/fullcmc \
  --user agent:password \
  --cacert ca.pem \
  -H "Content-Type: application/pkcs7-mime" \
  --data-binary @batch-request.cmc \
  -o batch-response.cmc

# Parse the response
CMCResponse -d /path/to/nssdb -i batch-response.cmc

Token Processing System (TPS) Integration

Dogtag’s Token Processing System (TPS) uses Full CMC for smart card lifecycle management:

  • Format and enroll smart cards
  • Update certificates on existing tokens
  • Recover keys from KRA to re-provision tokens

kipuka acts as a CMC gateway, enabling TPS to operate over EST endpoints. This is particularly useful for remote TPS operations where direct Dogtag access is restricted by firewall policy.

TPS configuration example:

[[ca]]
id = "dogtag-ca"
type = "dogtag"
url = "https://dogtag.example.com:8443"
kra_url = "https://dogtag.example.com:8443"
tps_url = "https://dogtag.example.com:8443"
# ... agent certs ...

[est.cmc]
# TPS may send large CMC messages with multiple key recovery requests
max_message_size = "50MB"

# TPS operations require signed CMC requests
allow_unsigned = false

Prerequisites

Before configuring kipuka with Dogtag integration, ensure:

  1. Dogtag PKI Installation:

    • Dogtag PKI 10.x or 11.x (RHEL 7/8/9: Red Hat Certificate System 9.x or 10.x)
    • Fedora: Dogtag PKI 11.x available via dnf
    • CA subsystem installed and operational
  2. Agent Certificate:

    • Agent certificate issued by the Dogtag CA with enrollment privileges
    • For KRA integration: separate agent cert with key archival/recovery privileges
    • Agent user configured in Dogtag database and added to appropriate groups
  3. Network Connectivity:

    • kipuka must reach Dogtag REST API port (default: 8443)
    • If using Dogtag behind a load balancer, ensure session affinity for approval workflows
    • Firewall rules permit bidirectional HTTPS between kipuka and Dogtag
  4. TLS Certificates:

    • Dogtag CA certificate chain for TLS verification
    • If Dogtag uses a private CA, install the root and intermediate certificates on kipuka host
  5. Optional: KRA Subsystem:

    • KRA installed and configured if server-side key generation is required
    • KRA transport certificate and storage certificate properly configured
    • KRA connector enabled in CA subsystem
  6. Profiles:

    • Ensure the certificate profiles referenced in kipuka configuration exist in Dogtag
    • Verify profile policies match your security requirements
    • Test profile submission via Dogtag CLI before configuring kipuka

Verification

Test Dogtag connectivity from the kipuka host:

# Verify CA REST API is accessible
curl -k https://dogtag.example.com:8443/ca/rest/certs/ca

# Submit a test certificate request using agent cert
curl -k https://dogtag.example.com:8443/ca/rest/certrequests \
  -X POST \
  --cert /etc/kipuka/dogtag/agent.pem \
  --key /etc/kipuka/dogtag/agent-key.pem \
  -H "Content-Type: application/json" \
  -d '{
    "ProfileID": "caServerCert",
    "Renewal": false,
    "Input": [{
      "Name": "cert_request_type",
      "Value": "pkcs10"
    }, {
      "Name": "cert_request",
      "Value": "<base64-encoded-csr>"
    }]
  }'

A successful response indicates the agent certificate is configured correctly and has enrollment privileges.

See Also

RFC Support Reference

This page documents every standards specification that kipuka implements, which sections are covered, and any caveats relevant to conformance testing.

Core Enrollment Standards

RFC 7030 – Enrollment over Secure Transport (EST)

SectionOperationStatusNotes
4.1/cacertsImplementedReturns the CA certificate chain as a PKCS#7 certs-only message. Unauthenticated; available without client credentials.
4.2/simpleenrollImplementedPKCS#10 CSR in, signed certificate out. Supports OTP, mTLS, GSSAPI, and CMS-based authentication.
4.2.2/simplereenrollImplementedRenewal with the existing client certificate presented via mTLS. Subject and SAN matching enforced by default.
4.3/fullcmcImplementedFull CMC (RFC 5272) request/response over the EST transport. Disabled by default; enable with est.fullcmc = true.
4.4/serverkeygenImplementedServer generates the key pair; returns a PKCS#7 EnvelopedData wrapping the private key and the signed certificate. Disabled by default.
4.5/csrattrsImplementedReturns a DER-encoded sequence of OIDs and attribute definitions the server expects in a CSR.
3.2EST-over-TLS bindingImplementedAll endpoints served over TLS 1.2 or 1.3 via rustls. The well-known URI prefix /.well-known/est is configurable via est.base_path.
3.2.2EST labelsImplementedLabel-specific paths (/.well-known/est/{label}/...) route to per-CA configurations with independent certificate profiles, allowed key types, and validity limits.
3.3.2HTTP-based client authImplementedHTTP-layer authentication (OTP via Authorization header) supplements TLS-layer mTLS authentication.
4.2.3Retry-After handlingImplementedWhen a request is deferred (e.g., pending manual approval), kipuka returns HTTP 202 with a Retry-After header. The interval is configurable via est.retry_after.
3.5Linking identity and POPImplementedProof-of-Possession via CSR self-signature is verified. When mTLS is used, the binding between the TLS identity and the CSR subject is enforced.

RFC 8951 – Clarifications for EST

RFC 8951 addresses ambiguities in RFC 7030. kipuka incorporates all clarifications that affect server behavior:

ClarificationStatusNotes
Content-Type enforcementImplementedStrict application/pkcs10 for enrollment; application/pkcs7-mime; smime-type=certs-only for /cacerts responses.
Base64 encoding rulesImplementedAccepts both raw DER and Base64-encoded bodies. Responses use Base64 with Content-Transfer-Encoding: base64.
Error response formatImplementedHTTP 4xx/5xx responses include a Content-Type: application/json body with a machine-readable error code and human-readable message.
TLS-unique channel bindingImplementedChannel binding values used for proof-of-possession when available (TLS 1.2 with tls-unique; TLS 1.3 uses Exporter instead).

RFC 5272 – Certificate Management over CMS (Full CMC)

The /fullcmc endpoint accepts a Full PKI Request (a ContentInfo containing a PKIData structure) and returns a Full PKI Response.

FeatureStatusNotes
PKIData parsingImplementedParses TaggedRequest sequences containing PKCS#10 or CRMF requests.
PKIResponse constructionImplementedReturns ResponseBody with certificates and optional control attributes.
Control attributesPartialid-cmc-statusInfoV2, id-cmc-identification, id-cmc-transactionId, and id-cmc-senderNonce / id-cmc-recipientNonce are supported. RA-delegated controls are not yet implemented.
Nested CMS signingImplementedRequests may be signed by a Registration Authority; kipuka validates the RA certificate against a configured RA trust store.

RFC 8739 – Short-Term, Automatically Renewed Certificates (STAR)

kipuka implements the server-side portion of STAR for short-lived certificate management. The kipuka::star module tracks renewal orders and issues replacement certificates before expiry.

FeatureStatusNotes
STAR order lifecycleImplementedStarOrder tracks each auto-renewal series through pending, active, expired, and cancelled states.
Automatic re-issuanceImplementedThe StarManager monitors active orders and issues replacement certificates before the not-after of the current certificate. Renewal interval is configurable per order.
Certificate fetch endpointImplementedThe /.well-known/est/{label}/star/{order-id} path returns the latest certificate in the renewal series.
CancellationImplementedOperators can cancel a STAR order via the admin API. The server stops issuing renewals and the order transitions to cancelled.

Transport

RFC 7252 – Constrained Application Protocol (CoAP)

The kipuka-coap crate implements EST-over-CoAP for resource-constrained devices (IoT sensors, embedded controllers) that cannot maintain persistent TCP connections.

FeatureStatusNotes
CoAP message parsingImplementedConfirmable (CON) and Non-confirmable (NON) message types. Token matching for request/response correlation.
DTLS transportImplementedCoAP endpoints served over DTLS 1.2 with PSK or certificate-based authentication via the kipuka_coap::dtls module.
Block-wise transferImplementedRFC 7959 Block1/Block2 options for transferring certificates and CSRs that exceed a single CoAP datagram. Block size is negotiated automatically.
Content-Format mappingImplementedEST content types mapped to CoAP Content-Format option values per the EST-coaps draft.
/cacerts over CoAPImplementedReturns the CA certificate chain using block-wise transfer.
/simpleenroll over CoAPImplementedEnrollment via PKCS#10 CSR over CoAP/DTLS.

Certificate Policy

CA/Browser Forum Baseline Requirements

kipuka enforces the CA/B Forum Baseline Requirements for TLS server certificates when configured to issue publicly-trusted certificates. See CA/B Forum Baseline Requirements for the full mapping.

RequirementStatusNotes
Certificate profileEnforcedSubject fields, key types, extensions validated against BR profiles. Non-compliant CSRs are rejected before signing.
Serial number generationEnforced160-bit serials from OS CSPRNG (exceeds the BR minimum of 64 bits).
Maximum validity periodEnforcedConfigurable max_validity_days with declining defaults that track the BR timeline (398/200/100/47 days).
Key size minimumsEnforcedRSA >= 2048 bits, ECDSA P-256 or P-384. Smaller keys are rejected at CSR validation.

NIAP Common Criteria – CA Protection Profile v2.0

kipuka is designed to satisfy the Security Functional Requirements (SFRs) of the NIAP CA Protection Profile. See NIAP CA Protection Profile for the full SFR-by-SFR mapping.

Cryptographic Standards

FIPS 140-3 – Cryptographic Module Validation

kipuka does not itself hold a FIPS 140-3 certificate. FIPS compliance is achieved by delegating all cryptographic operations to a validated PKCS#11 HSM module.

AspectMechanismNotes
Key generationC_GenerateKeyPairRSA and ECDSA key pairs generated inside the HSM boundary.
SigningC_SignAll certificate signing operations execute within the validated module.
Random number generationC_GenerateRandomSerial number entropy sourced from the HSM’s validated DRBG when an HSM is configured; falls back to OS CSPRNG (getrandom) otherwise.
Key storageHSM tokenPrivate keys are CKA_SENSITIVE and CKA_EXTRACTABLE=false by default.

When kipuka operates with a software-only key (no HSM), it uses the Synta crate’s software implementations. These are suitable for testing and non-FIPS environments but do not carry a FIPS validation.

FIPS 204 – ML-DSA (Post-Quantum Digital Signatures)

kipuka supports ML-DSA (formerly CRYSTALS-Dilithium) signing via two paths:

PathMechanismLevels
Synta softwarekipuka_hsm::sign::sign_ml_dsaML-DSA-44 (L2), ML-DSA-65 (L3), ML-DSA-87 (L5)
PKCS#11 HSMCKM_IBM_DILITHIUM or vendor-specificDepends on HSM firmware; see HSM Compatibility Matrix

ML-DSA support is experimental. CA certificates and end-entity certificates can use ML-DSA key pairs, but client and browser ecosystem support is limited as of 2026.

FIPS 203 – ML-KEM (Post-Quantum Key Encapsulation)

ML-KEM (formerly CRYSTALS-Kyber) is supported for key encapsulation in hybrid key exchange scenarios. The kipuka_hsm::key module defines MlKemLevel variants L1, L3, and L5 corresponding to ML-KEM-512, ML-KEM-768, and ML-KEM-1024 respectively.

PathMechanismLevels
Synta softwareSoftware KEMML-KEM-512 (L1), ML-KEM-768 (L3), ML-KEM-1024 (L5)
PKCS#11 HSMVendor-specific mechanismsDepends on HSM firmware support

ML-KEM is primarily relevant for /serverkeygen responses where the server encrypts the generated private key for transport to the client.

Summary Matrix

StandardScopeStatus
RFC 7030EST protocolCore implementation – all six endpoints
RFC 8951EST clarificationsFully implemented
RFC 5272CMC (Full)/fullcmc endpoint, partial control attributes
RFC 8739STAR auto-renewalShort-lived certificate management
RFC 7252CoAP transportConstrained device enrollment over DTLS
CA/B Forum BRCertificate profiles, validityEnforced at CSR validation and signing
NIAP CA PP v2.0Protection ProfileSFR mapping documented
FIPS 140-3Cryptographic modulesVia HSM integration
FIPS 204ML-DSA post-quantum signingVia Synta / PKCS#11
FIPS 203ML-KEM post-quantum KEMVia Synta / PKCS#11

NIAP CA Protection Profile

This page maps kipuka capabilities to the Security Functional Requirements (SFRs) defined in the NIAP Certificate Authority Protection Profile v2.0. The mapping is intended for evaluation teams preparing a Common Criteria assessment and for operators who need to verify that their deployment satisfies the Protection Profile.

Note: A Common Criteria evaluation requires an accredited evaluation facility. This document describes how kipuka addresses each SFR at the implementation level; it does not constitute a certified Security Target.

Audit (FAU)

FAU_GEN.1 – Audit Data Generation

Requirement: The TOE shall generate an audit record for each auditable event, including the date/time, event type, subject identity, and outcome.

How kipuka satisfies it:

The kipuka::audit module records structured audit events through the AuditEvent struct. Every event includes:

  • Timestamp (UTC, microsecond precision)
  • Event type (AuditEventType enum)
  • Subject identity (client certificate DN, OTP identifier, or GSSAPI principal)
  • Outcome (success or failure with reason)
  • Source IP address and EST label

The following event types are audited:

Event TypeTrigger
EnrollRequestClient submits a CSR to /simpleenroll, /simplereenroll, or /fullcmc
CertIssueCertificate successfully signed and returned to client
CertReenrollCertificate re-enrollment completed
CertRevokeCertificate revocation processed
EnrollRejectCSR rejected (policy violation, invalid key, unauthorized)
AuthSuccessClient authentication succeeded (any method)
AuthFailureClient authentication failed (bad OTP, invalid cert, GSSAPI error)
KeyGenerateCA key pair generated (HSM or software)
KeyLoadCA key loaded from HSM slot or file
KeyDestroyCA key destroyed (HSM C_DestroyObject or file wipe)
OtpCreateOTP token created via admin API
OtpUseOTP token consumed during enrollment
OtpRevokeOTP token revoked via admin API
OtpExpireOTP token expired (TTL exceeded)
AdminLoginOperator authenticated to admin API
AdminLogoutOperator session ended
AdminActionOperator performed an administrative operation
CaStartCA instance started and ready for signing
CaStopCA instance shut down
CaHealthChangeCA health status changed (HA failover event)
CrlGenerateCRL generated (when using Dogtag back-end)
SecurityViolationPolicy violation detected (tampered request, replay, etc.)

Implementation location: kipuka::audit::record() is called from every EST handler and authentication layer.

Evaluation notes: The event set covers all “minimum audit events” listed in Table 5 of the Protection Profile. Additional events (OTP lifecycle, HA health changes) exceed the minimum.

FAU_GEN.2 – User Identity Association

Requirement: The TOE shall associate each auditable event with the identity of the user that caused the event.

How kipuka satisfies it:

The AuditEvent struct contains a subject field populated from the authenticated identity:

  • mTLS: Subject DN and serial number of the client certificate.
  • OTP: The OTP identifier (opaque token ID, not the secret).
  • GSSAPI: The Kerberos principal name.
  • CMS: The signer DN from the CMS SignerInfo.
  • Unauthenticated (/cacerts): Recorded as "anonymous" with the source IP.

FAU_STG.1 – Protected Audit Trail Storage

Requirement: The TOE shall protect the stored audit records from unauthorized deletion.

How kipuka satisfies it:

Audit records are written through two channels, both designed for append-only semantics:

  1. Database – Audit records are INSERT-only. The database schema does not expose UPDATE or DELETE operations on the audit table. When using PostgreSQL, row-level security policies can further restrict modification.

  2. File – When audit.file is configured, events are appended to a JSON Lines file. kipuka opens the file in append-only mode (O_APPEND). File-system permissions should restrict the audit file to the kipuka service account.

  3. Syslog – When audit.syslog is configured, events are forwarded to a remote syslog server over TLS, placing the audit trail outside the TOE boundary.

Evaluation notes: The Protection Profile requires that “the TSF shall be able to prevent unauthorized deletion of the stored audit records.” Operators must ensure that file-system permissions and database RBAC are configured to prevent the kipuka process from deleting its own audit records. Shipping audit events to a remote syslog server (channel 3) provides the strongest protection.

Cryptographic Support (FCS)

FCS_CKM.1 – Cryptographic Key Generation

Requirement: The TOE shall generate asymmetric cryptographic keys in accordance with a specified key generation algorithm and key sizes.

How kipuka satisfies it:

Key TypeGeneration MethodModule
RSA 2048, 3072, 4096PKCS#11 C_GenerateKeyPair (HSM) or Synta softwarekipuka_hsm::key
ECDSA P-256, P-384, P-521PKCS#11 C_GenerateKeyPair (HSM) or Synta softwarekipuka_hsm::key
ML-DSA-44, ML-DSA-65, ML-DSA-87Synta software or vendor PKCS#11kipuka_hsm::key
Certificate serial numbersOS CSPRNG (getrandom) or PKCS#11 C_GenerateRandomkipuka::ca::issue

Serial numbers are 160 bits (20 bytes) from a CSPRNG, exceeding the CA/B Forum minimum of 64 bits and the NIAP requirement for unpredictable serial numbers.

Implementation location: kipuka_hsm::key::HsmKeyPair for CA keys; kipuka_est::serverkeygen for end-entity keys generated via /serverkeygen.

FCS_CKM.2 – Cryptographic Key Distribution

Requirement: The TOE shall distribute cryptographic keys in accordance with a specified key distribution method.

How kipuka satisfies it:

  • CA certificate distribution – The /cacerts endpoint returns the CA certificate chain as a PKCS#7 certs-only message. Distribution is over TLS (FCS_TLSS_EXT.1).

  • Server-generated key distribution – When /serverkeygen is enabled, the generated private key is encrypted to the client using PKCS#7 EnvelopedData:

    • AES-256 key wrapping with the content-encryption key wrapped to the client’s public key via RSA-OAEP or ECDH-ES.
    • The wrapped key and signed certificate are returned as a single multipart MIME response.

Implementation location: kipuka_est::serverkeygen for key wrapping; kipuka_est::cacerts for CA chain distribution.

FCS_COP.1 – Cryptographic Operation

Requirement: The TOE shall perform cryptographic operations in accordance with specified algorithms and key sizes.

How kipuka satisfies it:

OperationAlgorithm(s)Key SizeModule
Certificate signingRSA PKCS#1 v1.5, RSA-PSS, ECDSA>= 2048 (RSA), P-256/P-384/P-521 (EC)kipuka_hsm::sign
Certificate signing (PQC)ML-DSAL2/L3/L5kipuka_hsm::sign::sign_ml_dsa
HashingSHA-256, SHA-384, SHA-512Synta / HSM
TLSTLS 1.2 / TLS 1.3Per cipher suiterustls
OTP hashingArgon2id, bcrypt, SHA-256-HMACkipuka_otp
Key wrapping (/serverkeygen)AES-256-WRAP, RSA-OAEP256 (AES), >= 2048 (RSA)kipuka_est::serverkeygen

Implementation location: kipuka_hsm::sign dispatches to sign_rsa_pkcs1, sign_rsa_pss, sign_ecdsa, or sign_ml_dsa depending on the CA key type.

FCS_RBG_EXT.1 – Random Bit Generation

Requirement: The TOE shall use a DRBG that is seeded by an entropy source.

How kipuka satisfies it:

  • With HSM: Random bytes are obtained via PKCS#11 C_GenerateRandom, which invokes the HSM’s FIPS-validated DRBG.
  • Without HSM: Random bytes are obtained from the operating system’s CSPRNG via getrandom(2) on Linux or the equivalent on other platforms.

Serial number generation always uses whichever RBG is active.

FCS_TLSS_EXT.1 – TLS Server

Requirement: The TOE shall implement TLS 1.2 and/or TLS 1.3 as a server.

How kipuka satisfies it:

kipuka uses rustls for TLS termination.

ParameterConfigurationNotes
Minimum versiontls.min_version (default "1.2")Set to "1.3" to disable TLS 1.2 entirely.
Cipher suitestls.cipher_suitesDefaults to rustls safe defaults (TLS_AES_256_GCM_SHA384, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256 for TLS 1.3; ECDHE suites for TLS 1.2).
Client authtls.client_authSupports required, optional, and none modes.
Server certificate EKUid-kp-cmcRAThe server certificate should include the CMC Registration Authority extended key usage OID (1.3.6.1.5.5.7.3.28) for NIAP compliance.

Implementation location: kipuka::tls module configures rustls::ServerConfig.

Identification and Authentication (FIA)

FIA_AFL.1 – Authentication Failure Handling

Requirement: The TOE shall detect when a configurable number of unsuccessful authentication attempts occur and take action.

How kipuka satisfies it:

The OTP authentication subsystem implements rate limiting and lockout:

ParameterConfig KeyDefault
Max consecutive failuresotp.max_failures5
Failure counting windowotp.failure_window15 minutes
Lockout durationotp.lockout_duration30 minutes

When the threshold is reached:

  1. An AuthFailure audit event is recorded with reason "lockout".
  2. Subsequent authentication attempts for that OTP identifier return HTTP 403 until the lockout expires.
  3. If the lockout is triggered by mTLS (invalid client cert), the TLS handshake fails before the HTTP layer is reached; the audit event records the client IP and certificate serial number.

Implementation location: kipuka::auth::otp for OTP lockout; kipuka::auth::mtls for mTLS failure logging.

FIA_UAU.1 – Timing of Authentication

Requirement: The TOE shall allow only the retrieval of CA certificates before the user is authenticated.

How kipuka satisfies it:

  • /cacerts – Unauthenticated. No client credential is required.
  • All other endpoints (/simpleenroll, /simplereenroll, /fullcmc, /serverkeygen, /csrattrs) – Require authentication via at least one configured method (mTLS, OTP, GSSAPI, or CMS signature).

The EstAuth extractor in kipuka::auth enforces this policy. The OptionalAuth extractor is used only for /cacerts and returns AuthMethod::None when no credential is presented.

Implementation location: kipuka::auth::EstAuth and kipuka::auth::OptionalAuth.

FIA_UAU.5 – Multiple Authentication Mechanisms

Requirement: The TOE shall provide multiple authentication mechanisms.

How kipuka satisfies it:

kipuka supports five authentication methods, selectable per EST label:

MethodAuthMethod VariantUse Case
Mutual TLSMtlsPrimary method for re-enrollment and machine identity
One-Time PasswordOtpInitial device bootstrapping
GSSAPI / KerberosGssapiEnterprise domain-joined devices
CMS signatureCmsFull CMC requests signed by an RA
None (unauthenticated)None/cacerts only

Security Management (FMT)

FMT_SMR.1 – Security Roles

Requirement: The TOE shall maintain the roles of operator and user.

How kipuka satisfies it:

kipuka defines two distinct roles:

RoleAccessAuthentication
OperatorAdmin API (OTP management, CA health, HA control, audit retrieval)Admin-specific mTLS certificate or bearer token, on a separate listener (admin_listen)
UserEST endpoints (enrollment, renewal, CA certificate retrieval)EST client credentials (mTLS, OTP, GSSAPI)

The admin API listens on a separate address and port (server.admin_listen, default 127.0.0.1:9443) with independent TLS configuration. No EST client credential grants access to the admin API, and no admin credential grants access to EST endpoints on behalf of a client.

Implementation location: kipuka::routes::admin for operator endpoints; kipuka::routes::est for user endpoints.

FMT_SMF.1 – Management Functions

Requirement: The TOE shall provide management functions for authorized operators.

How kipuka satisfies it:

The admin API provides:

FunctionEndpointDescription
OTP managementPOST /admin/otp, DELETE /admin/otp/{id}Create and revoke OTP tokens
CA healthGET /admin/ca/healthView CA status, signing latency, HSM connectivity
HA controlPOST /admin/ha/failoverForce failover to a backup CA
Audit retrievalGET /admin/auditQuery audit records with time range and event type filters
Configuration reloadPOST /admin/reloadHot-reload certificate and label configuration without restart

Trusted Path (FTP)

FTP_TRP.1 – Trusted Path

Requirement: The TOE shall provide a communication path between itself and remote users that is logically distinct from other paths and provides assured identification of its endpoints.

How kipuka satisfies it:

  • EST over TLS – All enrollment communication uses TLS with server certificate authentication and (for authenticated endpoints) mutual TLS. The server certificate identifies the kipuka instance; the client certificate identifies the enrolling device.

  • Admin on separate TLS – The admin API uses an independent TLS listener with its own certificate and trust anchors. This provides logical separation between the user-facing EST path and the operator-facing management path.

  • DTLS for CoAP – CoAP enrollment uses DTLS 1.2 for the trusted path, providing the same endpoint authentication guarantees over UDP.

Implementation location: kipuka::tls for EST and admin TLS; kipuka_coap::dtls for DTLS.

SFR Coverage Summary

SFRRequirementkipuka ModuleStatus
FAU_GEN.1Audit data generationkipuka::auditSatisfied
FAU_GEN.2User identity associationkipuka::auditSatisfied
FAU_STG.1Protected audit trailkipuka::audit, DB, syslogSatisfied
FCS_CKM.1Key generationkipuka_hsm::keySatisfied
FCS_CKM.2Key distributionkipuka_est::serverkeygen, kipuka_est::cacertsSatisfied
FCS_COP.1Crypto operationskipuka_hsm::sign, rustlsSatisfied
FCS_RBG_EXT.1Random bit generationOS CSPRNG, PKCS#11Satisfied
FCS_TLSS_EXT.1TLS serverkipuka::tls (rustls)Satisfied
FIA_AFL.1Auth failure handlingkipuka::auth::otpSatisfied
FIA_UAU.1Timing of authenticationkipuka::authSatisfied
FIA_UAU.5Multiple auth mechanismskipuka::authSatisfied
FMT_SMR.1Security roleskipuka::routes::admin, kipuka::routes::estSatisfied
FMT_SMF.1Management functionskipuka::routes::adminSatisfied
FTP_TRP.1Trusted pathkipuka::tls, kipuka_coap::dtlsSatisfied

CA/B Forum Baseline Requirements

This page documents how kipuka enforces the CA/Browser Forum Baseline Requirements (BR) for the issuance and management of publicly-trusted TLS server certificates. Operators issuing certificates under a publicly-trusted root must configure kipuka to comply with these requirements; operators running private PKI may relax some constraints through configuration.

Certificate Profile Enforcement

kipuka validates every CSR against the BR certificate profile before signing. A CSR that violates any of the following rules is rejected with an EnrollReject audit event and an HTTP 400 response describing the violation.

Subject Fields

FieldBR Requirementkipuka Enforcement
commonNameMust match a SAN value if presentValidated at CSR parsing; rejected if CN is present but does not appear in the SAN extension
organizationNameMust be verified if includedkipuka does not verify organizational identity (this is a CA responsibility); operators can restrict allowed subjects via est.label.subject_pattern
serialNumberMust be unique within the CANot included by default; kipuka does not inject Subject serial numbers
countryNameTwo-letter ISO 3166 code if presentFormat-validated at CSR parsing

Key Type Requirements

Key TypeMinimum Sizekipuka Config
RSA2048 bitsEnforced unconditionally; CSRs with RSA keys < 2048 are rejected
RSA3072 bits (recommended)Configurable via est.label.allowed_key_types
ECDSA P-256256 bitsAccepted
ECDSA P-384384 bitsAccepted
ECDSA P-521521 bitsAccepted but not recommended by BR

To restrict a label to specific key types:

[[est.label]]
name = "web-servers"
ca_id = "issuing-ca-1"
allowed_key_types = ["rsa-3072", "rsa-4096", "ecdsa-p256", "ecdsa-p384"]

Serial Number Generation

The BR requires that certificate serial numbers contain at least 64 bits of output from a CSPRNG.

kipuka generates 160-bit (20-byte) serial numbers, significantly exceeding the minimum. The generation path depends on configuration:

ConfigurationSourceEntropy
HSM configuredPKCS#11 C_GenerateRandom160 bits from HSM’s FIPS-validated DRBG
Software-onlygetrandom(2)160 bits from OS CSPRNG

Serial numbers are encoded as unsigned integers in the serialNumber field of the TBSCertificate, with the high bit set to zero to ensure a positive ASN.1 INTEGER encoding (per RFC 5280 section 4.1.2.2).

Extension Enforcement

kipuka enforces the following extensions on every issued certificate. Extensions not listed here may be added via CSR attributes or label configuration but are not required by the BR.

Mandatory Extensions

ExtensionOIDBR Requirementkipuka Behavior
Authority Key Identifier (AKI)2.5.29.35Must be present (non-critical)Always injected using the SHA-1 hash of the CA’s public key
Subject Key Identifier (SKI)2.5.29.14Must be present (non-critical)Always injected using the SHA-1 hash of the end-entity public key
Basic Constraints2.5.29.19CA:FALSE for end-entity (critical)Always set to CA:FALSE with pathLenConstraint absent
Key Usage2.5.29.15Must be present (critical)Set from ca.default_key_usage; defaults to digitalSignature, keyEncipherment for RSA, digitalSignature for ECDSA
Extended Key Usage2.5.29.37Must include id-kp-serverAuth for TLS certificatesSet from ca.default_ext_key_usage; default includes serverAuth
Subject Alternative Name (SAN)2.5.29.17Must be present; CN deprecated as identity sourcekipuka requires SAN by default (est.label.require_san = true). SAN values from the CSR are passed through to the certificate.
Certificate Policies2.5.29.32Must reference applicable CP/CPSOperators configure this via label-level certificate profile settings

Extension Validation Rules

  • If the CSR contains a basicConstraints extension with CA:TRUE, kipuka rejects the request. CA certificates are issued through the admin API, not through EST enrollment.

  • If the CSR’s Key Usage includes keyCertSign or cRLSign, the request is rejected.

  • SAN entries are validated for syntactic correctness: DNS names must be valid hostnames (no wildcards unless explicitly enabled), IP addresses must parse as IPv4 or IPv6, and email addresses must contain exactly one @.

Validity Period Enforcement

The CA/B Forum is reducing maximum certificate validity on a declining timeline. kipuka tracks this timeline through the max_validity_days configuration parameter.

BR Validity Timeline

Effective DateMaximum ValidityMaximum notBefore to notAfterRecommended max_validity_days
Current398 days398 days398
15 March 2026200 days200 days200
15 March 2027100 days100 days100
15 March 202947 days47 days47

Configuration

Each CA and each EST label can specify a maximum validity:

[[ca]]
id = "public-ca"
name = "Public TLS CA"
cert = "/etc/kipuka/ca/public-ca.pem"
key = "/etc/kipuka/ca/public-ca.key"
validity_days = 90
max_validity_days = 200
[[est.label]]
name = "short-lived"
ca_id = "public-ca"
max_validity_days = 47

The effective maximum validity for a certificate is the minimum of:

  1. The CA’s max_validity_days
  2. The label’s max_validity_days (if set)
  3. The client’s requested validity (if the CSR includes a ValidityPeriod attribute)

If the client’s requested validity exceeds the effective maximum, kipuka clamps the notAfter date to the allowed maximum rather than rejecting the request. This behavior is logged as an audit event with the original and clamped values.

STAR Integration

For STAR (RFC 8739) auto-renewal certificates, the validity period is typically much shorter than the BR maximum – often hours or days. STAR renewal orders track their own validity interval independently of the label’s max_validity_days.

Server-Side Key Generation Compliance

The /serverkeygen endpoint generates a key pair on the server and returns the private key to the client. The BR imposes requirements on how this private key is protected in transit.

Key Generation

  • Key pairs are generated using the same CSPRNG path as serial numbers (PKCS#11 C_GenerateKeyPair when an HSM is configured, Synta software otherwise).
  • The generated key type and size must satisfy the same minimums as client-generated keys (RSA >= 2048, ECDSA P-256 or P-384).
  • The private key exists in kipuka’s memory only for the duration of the request. It is not written to disk or database.

Key Transport

The private key is returned to the client in a PKCS#7 EnvelopedData structure:

ComponentAlgorithmNotes
Content encryptionAES-256-CBC or AES-256-GCMSymmetric encryption of the private key
Key wrapping (RSA)RSA-OAEP (SHA-256)Wraps the CEK to the client’s public key from the CSR
Key wrapping (ECDH)ECDH-ES + AES-256-WRAPKey agreement with the client’s EC public key

The response is a multipart MIME message containing:

  1. The signed certificate (application/pkcs7-mime; smime-type=certs-only)
  2. The encrypted private key (application/pkcs8)

Implementation Location

kipuka_est::serverkeygen handles key generation, wrapping, and response construction.

Name Constraints and Encoding

Distinguished Name Encoding

All Distinguished Name components are encoded as UTF8String per RFC 5280 section 4.1.2.4 and BR section 7.1.4. kipuka does not use PrintableString or TeletexString encoding for any DN attribute.

Name Constraints

kipuka supports Name Constraints through CA configuration. When a CA certificate contains a Name Constraints extension, kipuka enforces those constraints at CSR validation time:

  • Permitted subtrees – SAN DNS names must fall within the permitted DNS name subtrees. IP addresses must fall within permitted IP ranges.
  • Excluded subtrees – SAN entries matching excluded subtrees cause CSR rejection.

This enforcement happens before signing, so a constraint violation results in an EnrollReject audit event rather than an improperly-issued certificate.

Internationalized Domain Names

kipuka accepts internationalized domain names (IDN) in SAN dNSName entries only in their A-label (Punycode) form, per BR section 7.1.4.2. U-label (Unicode) forms are rejected at CSR validation.

Compliance Checklist

The following checklist summarizes BR compliance status for operators running kipuka under a publicly-trusted root.

RequirementBR SectionStatusConfiguration
RSA >= 2048 bits6.1.5EnforcedUnconditional
ECDSA P-256 or P-3846.1.5EnforcedUnconditional
Serial >= 64 bits CSPRNG7.1Exceeded (160 bits)Unconditional
AKI present7.1.2.7EnforcedUnconditional
SKI present7.1.2.8EnforcedUnconditional
CA:FALSE for EE certs7.1.2.3EnforcedUnconditional
Key Usage critical7.1.2.1EnforcedUnconditional
SAN required7.1.4.2Default trueest.label.require_san
Max validity period6.3.2Enforcedca.max_validity_days, est.label.max_validity_days
/serverkeygen key protection6.1.2EnforcedPKCS#7 EnvelopedData transport
Certificate Policies extension7.1.2.2Operator-configuredLabel certificate profile
DN encoding (UTF8String)7.1.4EnforcedUnconditional

HSM Compatibility Matrix

This page documents the Hardware Security Modules that kipuka has been tested with, their supported mechanisms, configuration specifics, and known limitations. kipuka communicates with HSMs exclusively through the PKCS#11 (Cryptoki) interface via the cryptoki crate.

Compatibility Overview

FeatureEntrust nShieldUtimaco CryptoServerKryopticThales Luna 7
PKCS#11 version2.402.402.402.40
RSA 2048YesYesYesYes
RSA 3072YesYesYesYes
RSA 4096YesYesYesYes
ECDSA P-256YesYesYesYes
ECDSA P-384YesYesYesYes
ECDSA P-521YesYesYesYes
RSA PKCS#1 v1.5 signingYesYesYesYes
RSA-PSS signingYesYesYesYes
ECDSA signingYesYesYesYes
ML-DSA (FIPS 204)NoFirmware-dependentYes (software)No
AES-WRAP key wrappingYesYesYesYes
RSA-OAEP key wrappingYesYesYesYes
Concurrent sessionsUp to 256Up to 128UnlimitedUp to 64
FIPS 140-3 levelLevel 3Level 3 (CP5)N/A (software)Level 3
Key non-exportabilityCKA_EXTRACTABLE=falseCKA_EXTRACTABLE=falseConfigurableCKA_EXTRACTABLE=false
Remote/network HSMYes (nShield Connect)Yes (LAN)N/AYes (Luna Network HSM)

Signing Mechanisms

kipuka uses the following PKCS#11 mechanisms for certificate signing, selected automatically based on the CA key type:

Key TypePKCS#11 Mechanismkipuka Function
RSA (PKCS#1 v1.5)CKM_RSA_PKCSsign_rsa_pkcs1
RSA (PSS)CKM_RSA_PKCS_PSSsign_rsa_pss
ECDSA (any curve)CKM_ECDSAsign_ecdsa
ML-DSACKM_IBM_DILITHIUM (vendor-specific)sign_ml_dsa

The hash algorithm for RSA mechanisms is configurable per CA via the RsaHashAlgorithm enum: SHA-256 (default), SHA-384, or SHA-512.

Key Wrapping for /serverkeygen

When /serverkeygen generates a key pair on the server, the private key must be encrypted for transport to the client. kipuka supports the following wrapping methods:

Wrapping MethodPKCS#11 MechanismClient Key TypeNotes
AES-256-WRAPCKM_AES_KEY_WRAPAny (symmetric wrapping)Content encryption key wrapped; CEK encrypts the private key via AES-CBC or AES-GCM
RSA-OAEP (SHA-256)CKM_RSA_PKCS_OAEPRSACEK wrapped directly to client’s RSA public key
ECDH-ES + AES-WRAPCKM_ECDH1_DERIVE + CKM_AES_KEY_WRAPECDSA/ECDHEphemeral ECDH key agreement derives a wrapping key

Not all HSMs support all wrapping combinations. The following table shows which wrapping methods are available per vendor:

Wrapping MethodEntrust nShieldUtimaco CryptoServerKryopticThales Luna 7
AES-256-WRAPYesYesYesYes
RSA-OAEP (SHA-256)YesYesYesYes
RSA-OAEP (SHA-384)YesNoYesYes
ECDH-ES + AES-WRAPYesYesYesYes

Per-Vendor Configuration

Entrust nShield

The nShield HSM family includes the Solo XC (PCIe), Connect XC (network), and Edge (compact form factor) models.

Library path:

[hsm]
library = "/opt/nfast/toolkits/pkcs11/libcknfast.so"

Environment variables:

# Security World configuration
export NFAST_HOME="/opt/nfast"
export NFAST_KMDATA="/opt/nfast/kmdata/local"

Key generation (via nShield tools):

# Generate an RSA 4096 CA key in the Security World
generatekey pkcs11 \
  --key-type=RSA \
  --size=4096 \
  --token-label="kipuka-ca" \
  --key-label="ca-signing-key" \
  --protect=module

# Generate an ECDSA P-384 CA key
generatekey pkcs11 \
  --key-type=EC \
  --curve=P-384 \
  --token-label="kipuka-ca" \
  --key-label="ca-ec-signing-key" \
  --protect=module

Configuration example:

[hsm]
library = "/opt/nfast/toolkits/pkcs11/libcknfast.so"
token_label = "kipuka-ca"
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "nshield-ca"
name = "nShield-backed CA"
cert = "/etc/kipuka/ca/nshield-ca.pem"
key = "pkcs11:token=kipuka-ca;object=ca-signing-key"
hsm_slot = 0

Known limitations:

  • nShield requires the Security World to be initialized and the module to be in operational state before kipuka starts.
  • Concurrent session limits depend on the module model and license. The Solo XC supports up to 256 concurrent PKCS#11 sessions.
  • ML-DSA is not supported as of nShield firmware 13.x. Use Synta software fallback for post-quantum signing with nShield.

Utimaco CryptoServer

Utimaco CryptoServer models include the LAN appliance, PCIe card, and Se-Series (cloud HSM).

Library path:

[hsm]
library = "/usr/lib/utimaco/libcs_pkcs11_R3.so"

Configuration file:

Utimaco requires a configuration file referenced by the CS_PKCS11_R3_CFG environment variable:

export CS_PKCS11_R3_CFG="/etc/utimaco/cs_pkcs11_R3.cfg"

Example cs_pkcs11_R3.cfg:

[Global]
Logging = 0
Logpath = /var/log/utimaco
SlotCount = 10

[CryptoServer]
Device = TCP:192.168.1.100:3001
Timeout = 30000

Key generation (via Utimaco tools):

# Generate RSA 4096 CA key
p11tool2 --module=/usr/lib/utimaco/libcs_pkcs11_R3.so \
  --login --so-pin=$SO_PIN \
  --generate-rsa --bits=4096 \
  --label="ca-signing-key" \
  --id=01

# Generate ECDSA P-384 CA key
p11tool2 --module=/usr/lib/utimaco/libcs_pkcs11_R3.so \
  --login --so-pin=$SO_PIN \
  --generate-ec --curve=P-384 \
  --label="ca-ec-signing-key" \
  --id=02

Configuration example:

[hsm]
library = "/usr/lib/utimaco/libcs_pkcs11_R3.so"
slot = 0
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "utimaco-ca"
name = "Utimaco-backed CA"
cert = "/etc/kipuka/ca/utimaco-ca.pem"
key = "pkcs11:slot=0;object=ca-signing-key"
hsm_slot = 0

Known limitations:

  • RSA-OAEP with SHA-384 is not supported; use SHA-256 for /serverkeygen key wrapping.
  • Maximum 128 concurrent PKCS#11 sessions. Size the kipuka connection pool accordingly (db.max_connections should not exceed this when HSM-bound operations dominate).
  • ML-DSA support depends on firmware version. Check with Utimaco for availability on your CryptoServer model.

Kryoptic

Kryoptic is a software PKCS#11 implementation used for development, testing, and non-FIPS deployments. It implements the standard PKCS#11 interface without requiring physical hardware.

Library path:

[hsm]
library = "/usr/lib/kryoptic/libkryoptic_pkcs11.so"

On macOS:

[hsm]
library = "/usr/local/lib/libkryoptic_pkcs11.dylib"

Token initialization:

# Initialize a Kryoptic token
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --init-token --label="kipuka-dev" \
  --so-pin=12345678

# Set the user PIN
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --init-pin --token-label="kipuka-dev" \
  --so-pin=12345678 --new-pin=userpin

Key generation:

# Generate RSA 2048 key for testing
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --login --pin=userpin \
  --token-label="kipuka-dev" \
  --keypairgen --key-type=RSA:2048 \
  --label="test-ca-key" --id=01

# Generate ECDSA P-256 key for testing
pkcs11-tool --module=/usr/lib/kryoptic/libkryoptic_pkcs11.so \
  --login --pin=userpin \
  --token-label="kipuka-dev" \
  --keypairgen --key-type=EC:prime256v1 \
  --label="test-ec-key" --id=02

Configuration example:

[hsm]
library = "/usr/lib/kryoptic/libkryoptic_pkcs11.so"
token_label = "kipuka-dev"
pin = "userpin"  # Acceptable for development only

[[ca]]
id = "dev-ca"
name = "Development CA"
cert = "/etc/kipuka/ca/dev-ca.pem"
key = "pkcs11:token=kipuka-dev;object=test-ca-key"

Known limitations:

  • Kryoptic is a software implementation and does not hold a FIPS 140-3 validation. Do not use it for production deployments that require hardware-backed key protection.
  • ML-DSA signing uses the Synta software fallback through SoftwarePqcFallback, not native PKCS#11 mechanisms.
  • Token state is stored on the filesystem. Protect the Kryoptic data directory with appropriate file permissions.
  • Useful for CI/CD pipelines and integration testing where a real HSM is not available.

Thales Luna 7 (Network HSM and PCIe)

The Thales Luna 7 family includes the Luna Network HSM (SA 7000) and Luna PCIe HSM.

Library path:

[hsm]
library = "/usr/safenet/lunaclient/lib/libCryptoki2_64.so"

On macOS (Luna client):

[hsm]
library = "/usr/local/lib/libCryptoki2.dylib"

Client configuration:

Luna requires a client certificate registered with the HSM partition. Configuration is managed through the Luna client tools and the Chrystoki.conf file:

Chrystoki2 = {
  LibUNIX64 = /usr/safenet/lunaclient/lib/libCryptoki2_64.so;
}

LunaSA Client = {
  ServerCAFile = /etc/Chrystoki2/certs/server.pem;
  ClientCertFile = /etc/Chrystoki2/certs/client.pem;
  ClientPrivKeyFile = /etc/Chrystoki2/certs/client-key.pem;
  ServerName00 = luna-hsm.example.com;
  ServerPort00 = 1792;
}

Key generation (via Luna tools):

# Generate RSA 4096 CA key in partition
cmu generatekeypair \
  -modulusLen=4096 \
  -publicExponent=65537 \
  -label="ca-signing-key" \
  -sign=True \
  -verify=True \
  -extractable=False \
  -token=True

# Generate ECDSA P-384 CA key
cmu generatekeypair \
  -keyType=EC \
  -curvetype=3 \
  -label="ca-ec-signing-key" \
  -sign=True \
  -verify=True \
  -extractable=False \
  -token=True

Configuration example:

[hsm]
library = "/usr/safenet/lunaclient/lib/libCryptoki2_64.so"
slot = 1
pin_env = "KIPUKA_HSM_PIN"

[[ca]]
id = "luna-ca"
name = "Luna 7-backed CA"
cert = "/etc/kipuka/ca/luna-ca.pem"
key = "pkcs11:slot=1;object=ca-signing-key"
hsm_slot = 1

Known limitations:

  • Maximum 64 concurrent PKCS#11 sessions per partition. For high-throughput deployments, consider using HA groups across multiple partitions or HSMs.
  • ML-DSA is not supported as of Luna firmware 7.x. Use Synta software fallback for post-quantum signing.
  • Luna Network HSM requires a network partition and registered client certificate. The client certificate must be registered before kipuka can connect to the HSM.
  • Session timeout: Luna partitions have a configurable idle session timeout. Set Idle Sessions Timeout to 0 (disabled) or to a value longer than kipuka’s HA health check interval to prevent session churn.

HSM Session Management

kipuka maintains a pool of PKCS#11 sessions to avoid the overhead of opening and closing sessions for every signing operation. The pool is managed by the Pkcs11Context struct in kipuka_hsm::pkcs11.

ParameterDescriptionRecommendation
Session pool sizeControlled by db.max_connections (shared pool)Set to the lesser of the HSM’s max concurrent sessions and the expected peak signing rate
Login stateSessions are logged in once at pool creationThe HSM PIN is used at startup; it is not stored in memory after login
Error recoveryFailed sessions are dropped and replacedkipuka logs a CaHealthChange audit event when session errors exceed the HA threshold

Adding a New HSM

kipuka’s HSM support is modular. The kipuka_hsm::providers module contains per-vendor configuration through the HsmProvider enum:

  • Entrust – Entrust nShield family
  • Utimaco – Utimaco CryptoServer family
  • Kryoptic – Kryoptic software PKCS#11
  • ThalesCsp – Thales CipherTrust / SafeNet (legacy)
  • ThalesTct – Thales Luna 7 and later

Each provider module exports:

  • default_library_path() – Platform-specific default library location
  • provider_config() – Vendor-specific PKCS#11 initialization settings
  • supported_mechanisms() – List of PKCS#11 mechanisms the provider is known to support

To add support for a new HSM vendor, implement these three functions in a new module under kipuka_hsm::providers and add a variant to the HsmProvider enum. The PKCS#11 standard interface means most HSMs will work without vendor-specific code as long as the library path is configured correctly; the provider module primarily documents known quirks and default paths.

EST Endpoints

kipuka implements the six operations defined by RFC 7030 (Enrollment over Secure Transport) and the clarifications in RFC 8951. All EST operations are served under the /.well-known/est/ path on the EST listener (default port 9443).

Base URL and EST labels

The base URL for all operations is:

https://<host>:9443/.well-known/est/

When EST labels are configured, a label segment is inserted between /est/ and the operation name to route the request to a specific CA:

https://<host>:9443/.well-known/est/{label}/simpleenroll

For example, if two labels are configured – iot-fleet backed by a hardware-HSM CA and corp-devices backed by a Dogtag CA – clients target the appropriate CA by including the label in the URL:

/.well-known/est/iot-fleet/simpleenroll
/.well-known/est/corp-devices/simpleenroll

Requests to the unlabeled path (/.well-known/est/simpleenroll) use the default CA. See EST Labels for configuration details.

Content types

EST uses CMS (PKCS #7) encoding throughout. All request and response bodies are base64-encoded DER unless otherwise noted.

DirectionContent-TypeEncoding
CSR requestapplication/pkcs10base64-encoded DER PKCS #10
Certificate responseapplication/pkcs7-mime; smime-type=certs-onlybase64-encoded DER PKCS #7
CMC requestapplication/pkcs7-mime; smime-type=CMC-requestbase64-encoded DER CMS
CMC responseapplication/pkcs7-mime; smime-type=CMC-responsebase64-encoded DER CMS
Server keygen responsemultipart/mixedSee Server-Side Key Generation
CSR attributesapplication/csrattrsbase64-encoded DER

GET /cacerts

Retrieve the CA certificate chain. This operation requires no authentication and is typically the first call a client makes to bootstrap trust.

Authentication: None

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=certs-only. The body is a base64-encoded PKCS #7 certs-only message containing the CA certificate chain in order from the issuing CA to the root.

Example

# Fetch the CA chain (no auth required)
curl -o cacerts.p7b \
  https://est.example.com:9443/.well-known/est/cacerts

# Decode the base64 PKCS#7 and view the certificates
base64 -d cacerts.p7b | openssl pkcs7 -inform DER -print_certs -noout

# Save decoded certificates to a PEM file
base64 -d cacerts.p7b | openssl pkcs7 -inform DER -print_certs -out ca-chain.pem

With an EST label:

curl -o cacerts.p7b \
  https://est.example.com:9443/.well-known/est/iot-fleet/cacerts

POST /simpleenroll

Submit a PKCS #10 certificate signing request (CSR) for initial enrollment. The server validates the request, signs the certificate using the configured CA, and returns the signed certificate in a PKCS #7 response.

Authentication: OTP (HTTP Basic) or mTLS (client certificate).

For initial enrollment of a device that does not yet have a certificate, use OTP authentication. For enrollment of a device that already holds a valid certificate from a trusted CA, use mTLS.

Request body: base64-encoded DER PKCS #10 CSR, Content-Type application/pkcs10.

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=certs-only.

Example with OTP authentication

# Generate a private key and CSR
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout device.key -out device.csr -nodes \
  -subj "/CN=device-001.example.com"

# Base64-encode the CSR (DER format)
openssl req -in device.csr -outform DER | base64 > device.csr.b64

# Enroll using OTP (entity-id:otp-value as HTTP Basic auth)
curl -X POST \
  --cacert ca-chain.pem \
  -u "device-001:a7f3b9c2d1e4" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  -o enrolled.p7b \
  https://est.example.com:9443/.well-known/est/simpleenroll

# Decode the response to get the signed certificate
base64 -d enrolled.p7b | openssl pkcs7 -inform DER -print_certs -out device.crt

Example with mTLS authentication

# Enroll using an existing client certificate for authentication
curl -X POST \
  --cacert ca-chain.pem \
  --cert existing-device.crt \
  --key existing-device.key \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  -o enrolled.p7b \
  https://est.example.com:9443/.well-known/est/simpleenroll

Example with an EST label

curl -X POST \
  --cacert ca-chain.pem \
  -u "device-001:a7f3b9c2d1e4" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  -o enrolled.p7b \
  https://est.example.com:9443/.well-known/est/corp-devices/simpleenroll

POST /simplereenroll

Renew or rekey an existing certificate. The client authenticates with its current certificate via mTLS and submits a new CSR. The server validates that the CSR subject matches the authenticated client certificate (or that the change is permitted by policy) and returns a fresh certificate.

Authentication: mTLS required. The client must present the certificate being renewed.

Request body: base64-encoded DER PKCS #10 CSR, Content-Type application/pkcs10.

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=certs-only.

Example

# Generate a new CSR using the existing key (or a new key for rekeying)
openssl req -new -key device.key -out renew.csr -nodes \
  -subj "/CN=device-001.example.com"

# Base64-encode the CSR
openssl req -in renew.csr -outform DER | base64 > renew.csr.b64

# Re-enroll using the current device certificate for mTLS auth
curl -X POST \
  --cacert ca-chain.pem \
  --cert device.crt \
  --key device.key \
  -H "Content-Type: application/pkcs10" \
  --data-binary @renew.csr.b64 \
  -o renewed.p7b \
  https://est.example.com:9443/.well-known/est/simplereenroll

# Decode the renewed certificate
base64 -d renewed.p7b | openssl pkcs7 -inform DER -print_certs -out device-renewed.crt

POST /fullcmc

Full Certificate Management over CMS (RFC 5272). This operation accepts a full CMC request wrapped in a CMS SignedData structure and returns a full CMC response. Full CMC supports advanced features not available through simple enrollment: certificate revocation, key update with proof-of-possession, and batch operations.

Authentication: The CMC request itself carries authentication (the CMS SignedData is signed by the requester). mTLS may also be required depending on server policy.

Request body: base64-encoded DER CMS, Content-Type application/pkcs7-mime; smime-type=CMC-request.

Response: 200 OK with Content-Type application/pkcs7-mime; smime-type=CMC-response.

Example

# Full CMC requests are typically constructed by a CMC-aware client library.
# This example uses a pre-built CMC request.
curl -X POST \
  --cacert ca-chain.pem \
  --cert ra-agent.crt \
  --key ra-agent.key \
  -H "Content-Type: application/pkcs7-mime; smime-type=CMC-request" \
  --data-binary @cmc-request.b64 \
  -o cmc-response.p7b \
  https://est.example.com:9443/.well-known/est/fullcmc

# Decode the CMC response
base64 -d cmc-response.p7b | openssl cms -inform DER -cmsout -print

Note: Full CMC support requires the fullcmc feature in the server configuration. When Dogtag PKI is the back-end CA, CMC requests are forwarded to the Dogtag CMC profile.


POST /serverkeygen

Request server-side key generation. The server generates a key pair, signs the certificate, and returns both the certificate and the encrypted private key. This operation is used in environments where the client device cannot generate strong keys locally, or where key escrow through a Key Recovery Authority (KRA) is required.

Authentication: OTP (HTTP Basic) or mTLS.

Request body: base64-encoded DER PKCS #10 CSR, Content-Type application/pkcs10. The CSR provides the subject and requested attributes; the public key in the CSR is replaced by the server-generated key.

Response: 200 OK with Content-Type multipart/mixed. The response contains two parts:

PartContent-TypeContents
1application/pkcs7-mime; smime-type=certs-onlySigned certificate (base64 PKCS #7)
2application/pkcs8Encrypted private key (base64 PKCS #8)

The private key is encrypted to the client using the asymmetric key from the CSR or a pre-shared symmetric key, depending on configuration.

Example

# Generate a throwaway CSR (the server will generate the real key pair)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout throwaway.key -out serverkeygen.csr -nodes \
  -subj "/CN=device-002.example.com"

# Base64-encode
openssl req -in serverkeygen.csr -outform DER | base64 > serverkeygen.csr.b64

# Request server-side key generation
curl -X POST \
  --cacert ca-chain.pem \
  -u "device-002:b8e4c1d5f2a7" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @serverkeygen.csr.b64 \
  -o serverkeygen-response.mime \
  https://est.example.com:9443/.well-known/est/serverkeygen

# The response is multipart/mixed -- extract the certificate and key
# (exact parsing depends on the MIME boundary in the response headers)

Note: Server-side key generation requires a KRA (Key Recovery Authority) when key escrow is mandated by policy. With Dogtag PKI, the KRA subsystem handles key archival automatically.


GET /csrattrs

Retrieve the set of CSR attributes that the server expects or recommends clients include in their certificate signing requests. The response is an ASN.1 sequence of OIDs and attribute definitions.

Authentication: None (or mTLS, depending on server policy).

Response: 200 OK with Content-Type application/csrattrs. The body is a base64-encoded DER CsrAttrs structure as defined in RFC 7030 Section 4.5.2.

Example

# Fetch supported CSR attributes
curl --cacert ca-chain.pem \
  -o csrattrs.b64 \
  https://est.example.com:9443/.well-known/est/csrattrs

# Decode and inspect (requires ASN.1 tools)
base64 -d csrattrs.b64 | openssl asn1parse -inform DER

Typical attributes returned include:

  • ecPublicKey with secp256r1 or secp384r1 – indicating preferred key algorithms
  • challengePassword – when the server requires a challenge in the CSR
  • Extension requests – specific X.509v3 extensions the server will honor

Error responses

All EST endpoints return standard HTTP error codes. The response body for errors is application/json on the admin API and plain text on the EST endpoints.

StatusMeaningWhen returned
400 Bad RequestMalformed CSR, invalid base64 encoding, missing Content-Type, or CSR fails policy validation.Any POST endpoint
401 UnauthorizedMissing or invalid authentication. For OTP: the OTP value is wrong, expired, or already consumed. For mTLS: no client certificate presented or the certificate is not trusted./simpleenroll, /simplereenroll, /serverkeygen
403 ForbiddenAuthentication succeeded but the client is not authorized for the requested operation. Common cause: re-enrollment with a certificate whose subject does not match the CSR./simplereenroll
404 Not FoundUnknown EST label.Any endpoint with an invalid label
500 Internal Server ErrorServer-side failure: HSM unreachable, database error, or CA signing failure.Any endpoint
503 Service UnavailableThe requested CA is temporarily unavailable (all replicas down, HSM session exhausted).Any endpoint

Retry-After

When an operation requires asynchronous processing (for example, a Dogtag PKI back-end that queues signing requests for RA approval), the server returns 202 Accepted with a Retry-After header indicating the number of seconds the client should wait before polling.

HTTP/1.1 202 Accepted
Retry-After: 30
Content-Length: 0

The client should repeat the same request (with the same CSR and authentication) after the indicated interval. The server tracks pending requests and returns the signed certificate when it becomes available, or another 202 if the request is still pending.

Example: handling a 401

# A failed OTP enrollment returns 401
curl -v -X POST \
  --cacert ca-chain.pem \
  -u "device-001:wrong-otp-value" \
  -H "Content-Type: application/pkcs10" \
  --data-binary @device.csr.b64 \
  https://est.example.com:9443/.well-known/est/simpleenroll

# Response:
# < HTTP/1.1 401 Unauthorized
# < WWW-Authenticate: Basic realm="est"

TLS requirements

The EST listener enforces TLS 1.2 or 1.3 for all connections. The server certificate must be configured in [tls] in the kipuka configuration file.

For mTLS-authenticated endpoints, the server’s TLS configuration must include a client_ca trust anchor that covers the certificates clients present. See TLS Configuration for details.

# Verify server TLS configuration
openssl s_client -connect est.example.com:9443 -showcerts </dev/null

Admin API Reference

The admin API provides management operations for kipuka: health checks, CA status, OTP lifecycle, and issued certificate queries. It runs on a separate TLS listener (default port 9444) from the EST enrollment endpoints, allowing operators to restrict admin access at the network level independently of client enrollment traffic.

Admin listener configuration

The admin API binds to its own address and port, configured in the [admin] section of the kipuka configuration file:

[admin]
listen = "0.0.0.0:9444"

[admin.tls]
cert = "/etc/kipuka/admin-server.crt"
key = "/etc/kipuka/admin-server.key"
client_ca = "/etc/kipuka/admin-ca.pem"

The admin TLS certificate and trust anchors are independent of the EST listener. This separation allows operators to:

  • Issue admin certificates from a different CA than enrollment certificates.
  • Restrict admin access to a management VLAN by binding to a specific interface.
  • Apply different TLS policies (cipher suites, minimum version) for admin traffic.

Authentication

The admin API supports two authentication methods:

MethodHeaderDescription
mTLS(client certificate)The client presents a certificate trusted by the admin client_ca. Preferred for automated integrations.
Bearer tokenAuthorization: Bearer <token>A static token configured in the [admin] section. Suitable for quick manual access and scripts.
[admin]
bearer_token = "${KIPUKA_ADMIN_TOKEN}"

All examples below use Bearer token authentication. Substitute --cert / --key flags for mTLS.


Endpoints

GET /admin/health

System health check. Returns server uptime, version, and the health status of every configured CA.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/health | jq .
{
  "status": "healthy",
  "version": "0.4.0",
  "uptime_seconds": 86412,
  "cas": [
    {
      "id": "default",
      "label": null,
      "status": "healthy",
      "last_sign_time": "2026-06-24T14:22:01Z"
    },
    {
      "id": "iot-fleet",
      "label": "iot-fleet",
      "status": "healthy",
      "last_sign_time": "2026-06-24T14:18:33Z"
    },
    {
      "id": "corp-devices",
      "label": "corp-devices",
      "status": "degraded",
      "last_sign_time": "2026-06-24T13:55:10Z",
      "error": "HSM session pool exhausted, 1 of 2 replicas available"
    }
  ],
  "database": {
    "status": "healthy",
    "backend": "postgresql",
    "pool_size": 10,
    "active_connections": 3
  }
}

The top-level status field aggregates CA and database health:

ValueMeaning
healthyAll CAs and the database are operational.
degradedAt least one CA is degraded (partial replica loss) but enrollment is still possible.
unhealthyA critical CA or the database is down. Enrollment requests will fail.

GET /admin/ca

List all configured CAs with their current health status.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/ca | jq .
[
  {
    "id": "default",
    "label": null,
    "type": "local",
    "status": "healthy",
    "last_sign_time": "2026-06-24T14:22:01Z"
  },
  {
    "id": "iot-fleet",
    "label": "iot-fleet",
    "type": "hsm",
    "status": "healthy",
    "last_sign_time": "2026-06-24T14:18:33Z"
  },
  {
    "id": "corp-devices",
    "label": "corp-devices",
    "type": "dogtag",
    "status": "degraded",
    "last_sign_time": "2026-06-24T13:55:10Z"
  }
]

GET /admin/ca/:id

Retrieve detailed information about a specific CA, including its type, EST label binding, certificate chain, and operational status.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/ca/iot-fleet | jq .
{
  "id": "iot-fleet",
  "label": "iot-fleet",
  "type": "hsm",
  "status": "healthy",
  "hsm": {
    "module": "/usr/lib/libCryptoki2_64.so",
    "slot": 1,
    "key_label": "iot-signing-key"
  },
  "certificate_chain": [
    {
      "subject": "CN=IoT Fleet Issuing CA, O=Example Corp",
      "issuer": "CN=Example Root CA, O=Example Corp",
      "serial": "0A:1B:2C:3D:4E:5F",
      "not_before": "2025-01-15T00:00:00Z",
      "not_after": "2030-01-15T00:00:00Z",
      "key_algorithm": "ECDSA P-384"
    },
    {
      "subject": "CN=Example Root CA, O=Example Corp",
      "issuer": "CN=Example Root CA, O=Example Corp",
      "serial": "01",
      "not_before": "2020-01-01T00:00:00Z",
      "not_after": "2040-01-01T00:00:00Z",
      "key_algorithm": "RSA 4096"
    }
  ],
  "last_sign_time": "2026-06-24T14:18:33Z",
  "total_certs_issued": 14832
}

Error: 404 Not Found if the CA id does not exist.


POST /admin/otp

Generate a one-time password for initial device enrollment. The OTP is bound to an entity identifier (typically a device hostname or serial number) and is valid for a limited time and number of uses.

Request body: application/json

FieldTypeRequiredDescription
entity_idstringyesIdentifier for the device or entity. Must match the HTTP Basic username during enrollment.
ttlintegernoTime-to-live in seconds. Default: 3600 (1 hour).
max_usesintegernoMaximum number of times the OTP can be used. Default: 1.

Response: 201 Created

curl -s -X POST \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  -H "Content-Type: application/json" \
  --cacert admin-ca.pem \
  -d '{
    "entity_id": "device-001.example.com",
    "ttl": 7200,
    "max_uses": 1
  }' \
  https://admin.example.com:9444/admin/otp | jq .
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "entity_id": "device-001.example.com",
  "otp": "a7f3b9c2d1e4",
  "created": "2026-06-24T15:00:00Z",
  "expires": "2026-06-24T17:00:00Z",
  "max_uses": 1,
  "remaining_uses": 1
}

The otp value is returned only in this response and is never stored in plaintext on the server (the server stores a salted hash). Record it immediately.


GET /admin/otp

List all active (non-expired, non-exhausted) OTPs. The OTP secret value is not included – only metadata.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/otp | jq .
[
  {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "entity_id": "device-001.example.com",
    "created": "2026-06-24T15:00:00Z",
    "expires": "2026-06-24T17:00:00Z",
    "max_uses": 1,
    "remaining_uses": 1
  },
  {
    "id": "e29b1d4a-7c83-4f91-b234-1a23c4d5e678",
    "entity_id": "device-002.example.com",
    "created": "2026-06-24T14:30:00Z",
    "expires": "2026-06-24T15:30:00Z",
    "max_uses": 3,
    "remaining_uses": 2
  }
]

DELETE /admin/otp/:id

Revoke an active OTP before it expires or is fully consumed. Revoked OTPs are immediately invalid for enrollment.

Response: 204 No Content

curl -s -X DELETE \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  https://admin.example.com:9444/admin/otp/f47ac10b-58cc-4372-a567-0e02b2c3d479

Error: 404 Not Found if the OTP id does not exist or has already expired.


GET /admin/certs

List certificates issued by kipuka. Results are paginated and can be filtered by subject, issuer, status, or date range.

Query parameters:

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed).
per_pageinteger50Results per page (max 500).
subjectstringFilter by subject CN (substring match).
statusstringFilter by status: active, expired, revoked.
issued_afterstringISO 8601 datetime. Only certificates issued after this time.
issued_beforestringISO 8601 datetime. Only certificates issued before this time.

Response: 200 OK

curl -s \
  -H "Authorization: Bearer ${KIPUKA_ADMIN_TOKEN}" \
  --cacert admin-ca.pem \
  "https://admin.example.com:9444/admin/certs?status=active&per_page=5" | jq .
{
  "page": 1,
  "per_page": 5,
  "total": 14832,
  "certificates": [
    {
      "serial": "0A:1B:2C:3D:4E:5F:60:71",
      "subject": "CN=device-001.example.com",
      "issuer": "CN=IoT Fleet Issuing CA, O=Example Corp",
      "not_before": "2026-06-24T14:22:01Z",
      "not_after": "2027-06-24T14:22:01Z",
      "status": "active",
      "ca_id": "iot-fleet",
      "enrollment_type": "simpleenroll"
    },
    {
      "serial": "0A:1B:2C:3D:4E:5F:60:72",
      "subject": "CN=device-002.example.com",
      "issuer": "CN=IoT Fleet Issuing CA, O=Example Corp",
      "not_before": "2026-06-24T14:18:33Z",
      "not_after": "2027-06-24T14:18:33Z",
      "status": "active",
      "ca_id": "iot-fleet",
      "enrollment_type": "serverkeygen"
    }
  ]
}

Error responses

Admin API errors return JSON with a consistent structure:

{
  "error": "not_found",
  "message": "CA with id 'nonexistent' does not exist"
}
StatusMeaning
400 Bad RequestInvalid request body or query parameters.
401 UnauthorizedMissing or invalid Bearer token; untrusted client certificate.
404 Not FoundResource (CA, OTP, certificate) not found.
500 Internal Server ErrorServer-side failure.

Rust API Reference

kipuka’s Rust API documentation is auto-generated from source code doc comments using cargo doc and published alongside this book. The generated docs are the authoritative reference for types, traits, function signatures, and module structure.

Online API docs: kipuka.dev/api/kipuka/

Workspace crates

The kipuka workspace is organized into six crates, each with a focused responsibility:

CratePathAPI docsDescription
kipuka-estcrates/kipuka-estkipuka_estEST protocol implementation. Axum route handlers for all six RFC 7030 operations, TLS listener setup with rustls, mTLS client authentication, CSR validation, and certificate response encoding.
kipuka-hsmcrates/kipuka-hsmkipuka_hsmPKCS #11 HSM integration via the cryptoki crate. Manages HSM sessions, slot enumeration, key lookup by label, signing operations (RSA-PSS, ECDSA), and session pool lifecycle.
kipuka-otpcrates/kipuka-otpkipuka_otpOTP lifecycle management. Generation of cryptographically random OTP values, salted hash storage, validation against entity ID binding, use-count tracking, and expiry enforcement.
kipuka-utilcrates/kipuka-utilkipuka_utilShared types and utilities. Configuration file parsing (TOML), ASN.1 helpers built on synta, error type hierarchy, database connection pooling via sqlx, and audit log formatting.
kipuka-dogtagcrates/kipuka-dogtagkipuka_dogtagDogtag PKI REST client. Submits certificate signing requests to a Dogtag CA subsystem, retrieves signed certificates, and interacts with the KRA subsystem for server-side key generation and escrow.
kipuka-coapcrates/kipuka-coapkipuka_coapCoAP transport layer (RFC 7252). Provides EST-over-CoAP endpoints for constrained IoT devices that cannot use HTTP/TLS, with DTLS for transport security.

Building the docs locally

To generate and open the API documentation from a local checkout:

# Clone the repository
git clone https://codeberg.org/czinda/kipuka.git
cd kipuka

# Build docs for all workspace crates (skip dependency docs for speed)
cargo doc --no-deps --open

This builds HTML documentation into target/doc/ and opens it in your default browser. The landing page lists all six crates with links to their module trees.

To build docs for a single crate:

cargo doc --no-deps -p kipuka-est --open

Including private items

By default, cargo doc only documents public API surface. To include private functions, types, and modules (useful during development):

cargo doc --no-deps --document-private-items --open

Prerequisites

Building the docs requires:

  • Rust 1.88+ (edition 2021)
  • A working C toolchain (required by cryptoki build script for PKCS #11 header compilation)
  • SQLx offline mode or a running database for query checking – see Development Setup for details

Documentation conventions

The codebase follows these doc comment conventions:

  • Every public type, trait, function, and module has a /// doc comment.
  • Examples in doc comments are runnable via cargo test --doc where practical.
  • Cross-references use intra-doc links ([OtpStore], [CaConfig]) for navigable HTML output.
  • Safety invariants on unsafe blocks are documented with # Safety sections.
  • Error conditions are documented with # Errors sections listing the specific error variants returned.

Architecture

kipuka is structured as a Cargo workspace with six crates, each owning a distinct responsibility. This separation enforces module boundaries at the compilation level and allows operators to build only the features they need.

Workspace layout

                      Clients
                        |
                   TLS + mTLS/OTP
                        |
                +-------+-------+
                |   kipuka-est  |     axum routes, EST protocol
                +---+---+---+---+
                    |   |   |
          +---------+   |   +---------+
          |             |             |
     kipuka-otp    kipuka-hsm    kipuka-util
     OTP lifecycle  PKCS#11      shared types
                    HSM ops         & config
          |             |
          |        kipuka-dogtag
          |         Dogtag PKI
          |         REST client
          |
     +----+----+       kipuka-coap
     |   sqlx  |       CoAP transport
     | sqlite  |       (RFC 7252)
     | postgres|
     | mariadb |
     +---------+

Crate responsibilities

CrateRole
kipuka-estCore server binary. Owns the axum HTTP router, TLS termination (rustls), EST protocol handlers (/cacerts, /simpleenroll, /simplereenroll, /serverkeygen, /fullcmc, /csrattrs), request authentication, CSR validation, certificate construction (via synta), the admin API, database access (sqlx), and the HA state machine.
kipuka-hsmPKCS #11 integration via the cryptoki crate. Provides a Signer trait implementation that delegates cryptographic operations to an HSM. Handles slot enumeration, session management, key lookup, and sign operations. Isolates all unsafe FFI behind a safe Rust API.
kipuka-otpOne-time password lifecycle: generation (CSPRNG), hashing (argon2id / bcrypt), storage, validation, rate limiting, and expiration. Exposes an internal API consumed by kipuka-est for enrollment authentication and by the admin API for token provisioning.
kipuka-utilShared types and configuration parsing. Owns kipuka.toml deserialization (via serde + toml), X.509 helper functions, ASN.1 OID constants, error types, and the zeroize-aware wrappers for sensitive data.
kipuka-dogtagREST client for Red Hat Certificate System / Dogtag PKI. Translates EST enrollment requests into Dogtag profile-based certificate issuance calls, delegating signing to a full CA back-end instead of local key material.
kipuka-coapCoAP (RFC 7252) transport layer for constrained-device enrollment. Maps CoAP request/response semantics onto the same EST handlers used by the HTTPS path, enabling bandwidth-constrained IoT devices to enroll without HTTP overhead.

Dependencies flow strictly downward: kipuka-est depends on all other crates; leaf crates (kipuka-util, kipuka-coap) depend on nothing project-internal except kipuka-util.

EST operation data flow

A certificate enrollment request traverses the following path from TLS handshake to certificate issuance.

1. Client opens TLS connection to kipuka-est (rustls).
2. rustls performs TLS 1.2/1.3 handshake.
   - If mTLS: client certificate is validated against configured trust anchors.
   - If OTP:  TLS completes without client cert; HTTP Basic auth carries the token.
3. axum routes the request based on URL path:
   /.well-known/est/{label?}/simpleenroll  -> enroll handler
   /.well-known/est/{label?}/simplereenroll -> reenroll handler
   /.well-known/est/{label?}/cacerts       -> cacerts handler
   /.well-known/est/{label?}/serverkeygen  -> serverkeygen handler
   ...
4. Label resolution: if a label segment is present, kipuka looks up the
   [[est.label]] entry and resolves the bound [[ca]] configuration.
   If absent, the default CA is used.
5. Authentication:
   a. mTLS: extract client certificate from TLS session, verify chain.
   b. OTP:  extract Basic auth credentials, validate against kipuka-otp
            (argon2 hash comparison, rate limiting, expiration check).
   c. GSSAPI: validate Negotiate header against KDC, map principal.
6. CSR parsing: the request body (application/pkcs10) is parsed by synta.
   Key type, subject DN, SANs, and extensions are extracted and validated
   against the label's policy (allowed_key_types, subject_pattern,
   require_san, required_ext_key_usage).
7. Certificate construction: synta builds the X.509 TBS (to-be-signed)
   certificate from the validated CSR fields, CA configuration, and
   label policy.  Serial number is generated from OsRng (CSPRNG).
8. Signing:
   - File-based CA key: synta signs the TBS certificate directly.
   - HSM-backed CA key: kipuka-hsm opens a PKCS#11 session and delegates
     the sign operation to the hardware token.
   - Dogtag back-end: kipuka-dogtag sends the CSR to the Dogtag REST API
     and retrieves the signed certificate.
9. Response: the signed certificate is wrapped in a PKCS#7 / CMS
   ContentInfo envelope (DER-encoded) and returned with Content-Type
   application/pkcs7-mime.
10. Audit: an audit event is written to the configured destinations
    (file, syslog, database) with request metadata, outcome, and
    certificate fingerprint.

Multi-CA HA failover state machine

When [ha] is enabled, each CA in an [[ha.group]] transitions through four states. The HA controller runs periodic health checks and manages transitions automatically.

                   check passes
          +---------------------------+
          |                           |
          v                           |
    +-----------+    check fails    +-----------+
    |  Healthy  | ----------------> |  Degraded |
    +-----------+                   +-----------+
          ^                           |
          |                           | failure_threshold
          |                           | consecutive failures
          |                           v
    +-----------+    check passes   +-----------+
    |  Recovery | <---------------- | Failed    |
    +-----------+   after           +-----------+
          |         recovery_timeout
          | sustained success
          | (failure_threshold checks pass)
          v
    +-----------+
    |  Healthy  |
    +-----------+

State definitions:

  • Healthy – CA is operational. Health checks pass. Enrollment requests are routed to this CA normally.
  • Degraded – One or more health checks have failed but the threshold has not been reached. The CA continues to receive traffic. An alert is raised.
  • Failed – Consecutive failures have reached failure_threshold. The CA is removed from the routing pool. If the CA was the active node in an active-passive group, the next CA in ca_ids order is promoted.
  • Recovery – After recovery_timeout elapses, the HA controller begins probing the failed CA. It must pass failure_threshold consecutive checks before returning to Healthy. During recovery the CA does not receive enrollment traffic.

Failover strategies (set per [[ha.group]] or globally in [ha]):

StrategyBehavior
active-passiveFirst healthy CA in ca_ids order handles all requests. Failover promotes the next CA in order.
round-robinRequests are distributed across all healthy CAs in rotation.
weightedCAs are weighted by a configurable priority; higher-priority CAs receive more traffic.
latency-basedHealth check latency is tracked; requests are routed to the CA with the lowest observed latency.

The health check itself performs a lightweight signing operation (or, for Dogtag-backed CAs, a REST API ping) to verify that the CA can actually issue certificates. Network reachability alone is insufficient – a reachable HSM that has entered an error state must still be detected as unhealthy.

Authentication flow

OTP validation path

Client ---[HTTP Basic: entity_id:otp_value]---> kipuka-est
  |
  +-> kipuka-otp: look up entity_id in database
      |
      +-> Check expiration (expires_at > now)
      +-> Check use count (uses < max_uses)
      +-> Check lockout (failed_attempts < max_failures within failure_window)
      +-> Hash provided OTP with argon2id
      +-> Timing-safe comparison (subtle::ConstantTimeEq) against stored hash
      |
      +-> On success: increment use count, clear failure counter, return Ok
      +-> On failure: increment failed_attempts, check lockout threshold

OTP values are never stored in plaintext. The argon2id hash is computed at token generation time and only the hash is persisted. The raw token is returned to the administrator exactly once in the generation response.

mTLS certificate chain validation

Client ---[TLS ClientHello + Certificate]---> rustls
  |
  +-> rustls verifies:
      1. Certificate signature is valid
      2. Certificate is not expired
      3. Issuer chain terminates at a configured trust anchor
      4. Key usage includes digitalSignature
      5. Extended key usage includes clientAuth (if enforced)
  |
  +-> kipuka-est extracts:
      - Subject DN (for audit and authorization)
      - Serial number (for certificate identity tracking)
      - SAN entries (for device identification)

GSSAPI / Kerberos

Client ---[Negotiate: base64(SPNEGO token)]---> kipuka-est
  |
  +-> Accept security context using server keytab
  +-> Extract authenticated principal (e.g., user@REALM)
  +-> Map principal to certificate subject via [gssapi.principal_mapping]
      or default_template
  +-> Return mapped subject for certificate construction

Database schema overview

kipuka uses sqlx with compile-time checked queries. The schema is managed through sequential migrations in migrations/{sqlite,postgres,mariadb}/.

Core tables

otps – One-time password state.

ColumnTypeDescription
idINTEGER / SERIALPrimary key
entity_idTEXTClient identifier (e.g., device hostname)
otp_hashTEXTArgon2id hash of the OTP value
created_atTIMESTAMPToken generation time
expires_atTIMESTAMPToken expiration time
max_usesINTEGERMaximum allowed uses
use_countINTEGERCurrent use count
failed_attemptsINTEGERConsecutive failed validation attempts
last_failure_atTIMESTAMPTime of most recent failed attempt
locked_untilTIMESTAMPLockout expiration (NULL if not locked)

audit_log – Tamper-evident audit trail.

ColumnTypeDescription
idINTEGER / SERIALPrimary key
timestampTIMESTAMPEvent time (UTC)
event_typeTEXTEvent category (enroll, renew, reject, otp_create, etc.)
entity_idTEXTClient or device identifier
ca_idTEXTCA that processed the request
labelTEXTEST label (NULL for unlabeled requests)
auth_methodTEXTAuthentication method (mtls, otp, gssapi)
outcomeTEXTsuccess or failure
detailTEXTHuman-readable detail or error message
cert_fingerprintTEXTSHA-256 fingerprint of issued certificate (NULL on failure)
client_ipTEXTSource IP address

certs – Certificate inventory.

ColumnTypeDescription
idINTEGER / SERIALPrimary key
serial_numberTEXTCertificate serial (hex-encoded)
subject_dnTEXTSubject distinguished name
issuer_dnTEXTIssuer distinguished name
not_beforeTIMESTAMPValidity start
not_afterTIMESTAMPValidity end
fingerprintTEXTSHA-256 fingerprint
ca_idTEXTIssuing CA identifier
labelTEXTEST label used for issuance
entity_idTEXTAssociated entity (from OTP or mTLS subject)
pemTEXTFull PEM-encoded certificate (optional, controlled by config)

EST label routing

EST labels are the primary mechanism for multi-profile and multi-CA operation. When a request arrives at /.well-known/est/{label}/simpleenroll, kipuka resolves the label to a [[est.label]] configuration entry:

Request URL: /.well-known/est/iot-devices/simpleenroll
                                   |
                                   v
            +--------------------------------------+
            | Label lookup: name == "iot-devices"  |
            +--------------------------------------+
                                   |
                     ca_id = "iot-ca"
                                   |
                                   v
            +--------------------------------------+
            | CA lookup: id == "iot-ca"            |
            | cert, key, chain, validity_days,     |
            | hsm_slot, max_validity_days          |
            +--------------------------------------+
                                   |
                                   v
            +--------------------------------------+
            | Policy enforcement:                  |
            | - allowed_key_types                  |
            | - required_ext_key_usage             |
            | - require_san                        |
            | - subject_pattern                    |
            | - max_validity_days                  |
            +--------------------------------------+
                                   |
                                   v
            +--------------------------------------+
            | Certificate issuance using resolved  |
            | CA key material and label policy     |
            +--------------------------------------+

Requests without a label segment (e.g., /.well-known/est/simpleenroll) use the first [[ca]] entry as the default CA with no additional label-level policy enforcement.

When HA is enabled, label routing is extended: the label’s ca_id is checked against [[ha.group]] memberships. If the CA belongs to an HA group and is in a Failed state, the request is transparently routed to the next healthy CA in the group’s ca_ids list. The label’s policy constraints (key types, subject pattern, etc.) are still enforced regardless of which CA in the group handles the request.

Development Setup

This guide covers everything needed to build kipuka from source and run it locally with Docker Compose.

Prerequisites

ToolMinimum versionPurpose
Rust toolchain1.88+Compiler and cargo
Docker or Podman24+ / 4+Container runtime for Compose profiles
Docker Compose2.20+Orchestrates database and HSM dev containers
OpenSSL CLI1.1.1+ or 3.xGenerating test certificates
pkg-configanyLocating system libraries during build

Clone and build

git clone https://codeberg.org/czinda/kipuka.git
cd kipuka
cargo build

A release-optimized build (slower compilation, faster binary):

cargo build --release

The workspace produces a single binary at target/debug/kipuka (or target/release/kipuka).

OS-specific dependencies

Fedora / RHEL / CentOS Stream

sudo dnf install openssl-devel clang cmake pkg-config sqlite-devel

Debian / Ubuntu

sudo apt install libssl-dev clang cmake pkg-config libsqlite3-dev

macOS

brew install openssl cmake pkg-config
export OPENSSL_DIR=$(brew --prefix openssl)

Docker Compose profiles

The repository includes a compose.yaml with profiles for different database backends and an HSM development environment. Each profile starts only the services relevant to that backend.

Available profiles

ProfileServices startedUse case
sqlite (default)kipuka onlyMinimal local development; SQLite file on disk
postgreskipuka + PostgreSQL 16Testing against PostgreSQL
mariadbkipuka + MariaDB 11Testing against MariaDB
hsmkipuka + Kryoptic SoftHSMPKCS#11 development without hardware

Running with SQLite (default)

docker compose up

This starts kipuka with an in-process SQLite database. No external database container is required.

Running with PostgreSQL

docker compose --profile postgres up

The PostgreSQL container is preconfigured with:

  • Database: kipuka
  • User: kipuka
  • Password: kipuka-dev
  • Port: 5432

The kipuka service automatically uses the connection string postgres://kipuka:kipuka-dev@postgres:5432/kipuka.

Running with MariaDB

docker compose --profile mariadb up

The MariaDB container is preconfigured with:

  • Database: kipuka
  • User: kipuka
  • Password: kipuka-dev
  • Port: 3306

Running with Kryoptic HSM

docker compose --profile hsm up

This starts a Kryoptic SoftHSM container alongside kipuka. Kryoptic is an open-source PKCS#11 implementation written in Rust, suitable for development and testing. It provides the same PKCS#11 API as hardware HSMs without requiring physical tokens.

See the HSM development section below for slot configuration and key management.

Generate test certificates

The repository includes a helper script that creates a complete test PKI hierarchy suitable for local development:

./contrib/local-dev/setup-ca.sh

This generates the following files under contrib/local-dev/pki/:

FileContents
ca.pemSelf-signed root CA certificate
ca-key.pemRoot CA private key
server.pemServer TLS certificate (SAN: localhost, 127.0.0.1)
server-key.pemServer TLS private key
client.pemClient certificate for mTLS testing
client-key.pemClient private key

The script is idempotent – running it again regenerates all certificates.

Minimal kipuka.toml for local development

Create a kipuka.toml in the repository root:

[server]
listen = "0.0.0.0:9443"

[tls]
cert = "contrib/local-dev/pki/server.pem"
key = "contrib/local-dev/pki/server-key.pem"

[tls.client_auth]
trust_anchors = "contrib/local-dev/pki/ca.pem"
mode = "optional"

[db]
url = "sqlite://kipuka-dev.db?mode=rwc"
auto_migrate = true

[[ca]]
id = "dev-ca"
name = "Development CA"
cert = "contrib/local-dev/pki/ca.pem"
key = "contrib/local-dev/pki/ca-key.pem"
validity_days = 365

[est]
base_path = "/.well-known/est"

[[est.label]]
name = "default"
ca_id = "dev-ca"

[otp]
enabled = true
token_length = 16
default_ttl = "24h"
max_uses = 1
hash_algorithm = "argon2id"

[admin]
enabled = true
auth = "bearer"
bearer_token_env = "KIPUKA_ADMIN_TOKEN"

Run the server:

export KIPUKA_ADMIN_TOKEN="dev-token-do-not-use-in-production"
cargo run -- --config kipuka.toml

Running against each database backend

SQLite

No additional setup. The database file is created automatically when auto_migrate = true:

[db]
url = "sqlite://kipuka-dev.db?mode=rwc"
auto_migrate = true

PostgreSQL

Start a local PostgreSQL instance or use the Compose profile:

docker compose --profile postgres up -d postgres

Update kipuka.toml:

[db]
url = "postgres://kipuka:kipuka-dev@localhost:5432/kipuka"
auto_migrate = true

Run migrations explicitly if auto_migrate is disabled:

cargo run -- migrate --config kipuka.toml

MariaDB

Start MariaDB via the Compose profile:

docker compose --profile mariadb up -d mariadb

Update kipuka.toml:

[db]
url = "mysql://kipuka:kipuka-dev@localhost:3306/kipuka"
auto_migrate = true

HSM development with Kryoptic

Kryoptic provides a PKCS#11 interface compatible with the cryptoki crate used by kipuka-hsm. It stores keys in software but exposes the same API as a hardware token.

Starting Kryoptic

docker compose --profile hsm up -d kryoptic

The container exposes the PKCS#11 shared library at a bind-mounted path. Check the compose.yaml for the exact mount point (typically /usr/lib/libkryoptic.so inside the container).

Initializing a token

Use pkcs11-tool (from the OpenSC package) to initialize a slot and generate a CA signing key:

# Initialize the token in slot 0
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-token --slot 0 \
  --label "kipuka-dev" \
  --so-pin 12345678

# Set the user PIN
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-pin --slot 0 \
  --login --so-pin 12345678 \
  --new-pin 1234

# Generate an ECDSA P-256 key pair for CA signing
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --keypairgen --key-type EC:prime256v1 \
  --id 01 --label "dev-ca-key"

Configuring kipuka for HSM

Update kipuka.toml to reference the PKCS#11 module:

[hsm]
library = "/usr/lib/libkryoptic.so"
slot = 0
token_label = "kipuka-dev"
pin = "1234"  # For dev only; use pin_env or pin_file in production

[[ca]]
id = "hsm-dev-ca"
name = "HSM Development CA"
cert = "contrib/local-dev/pki/ca.pem"
key = "pkcs11:object=dev-ca-key"
hsm_slot = 0

Listing objects in the token

pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --list-objects

Verifying HSM signing

# Sign a test payload to confirm the PKCS#11 path works
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --sign --mechanism ECDSA \
  --id 01 \
  --input-file /dev/urandom --read-write

IDE setup

rust-analyzer

kipuka works out of the box with rust-analyzer. The workspace Cargo.toml at the repository root defines all crate members, so rust-analyzer automatically discovers the full project.

VS Code

Recommended extensions:

ExtensionPurpose
rust-lang.rust-analyzerRust language support, inline type hints, go-to-definition
vadimcn.vscode-lldbDebugger for Rust binaries
tamasfe.even-better-tomlTOML syntax highlighting and validation
serayuzgur.cratesCrate version hints in Cargo.toml
usernamehw.errorlensInline compiler error display

Recommended .vscode/settings.json for the workspace:

{
  "rust-analyzer.check.command": "clippy",
  "rust-analyzer.check.extraArgs": ["--", "-D", "warnings"],
  "rust-analyzer.cargo.features": "all",
  "editor.formatOnSave": true,
  "[rust]": {
    "editor.defaultFormatter": "rust-lang.rust-analyzer"
  }
}

Environment variables for development

VariablePurposeExample
RUST_LOGLog verbositydebug, kipuka_est=trace
KIPUKA_ADMIN_TOKENAdmin API bearer tokendev-token-do-not-use-in-production
KIPUKA_HSM_PINHSM PIN (production)(set in env, not in config file)

Set these in a .env file (excluded from version control via .gitignore) or export them in your shell.

Next steps

  • Testing – run the test suite and perform protocol-level verification
  • Database Migrations – create and manage schema changes
  • Contributing – code style, commit conventions, and security invariants

Testing

kipuka’s test suite covers unit tests, integration tests, EST protocol conformance, HSM operations, and full-stack enrollment flows.

Unit tests

Unit tests run against in-memory state and do not require any external services. They execute quickly and are suitable for rapid iteration:

cargo test

Per-crate testing

Run tests for a specific crate to reduce compilation time during focused development:

cargo test -p kipuka-est
cargo test -p kipuka-hsm
cargo test -p kipuka-otp
cargo test -p kipuka-util
cargo test -p kipuka-dogtag
cargo test -p kipuka-coap

Filtering tests

Run a single test or a subset by name:

# Run all tests with "otp" in the name
cargo test otp

# Run a specific test function
cargo test -p kipuka-otp -- test_argon2_hash_roundtrip

# Show test output (including println! in passing tests)
cargo test -- --nocapture

Integration tests

Integration tests exercise the full HTTP stack: TLS handshake, request routing, authentication, CSR validation, certificate issuance, and database persistence. They are gated behind a feature flag because they start a real axum server on a random port:

cargo test --features integration

Integration tests create a temporary SQLite database for each test run and clean it up on completion. No external database is required.

What integration tests cover

  • Full /simpleenroll flow with OTP authentication
  • Full /simplereenroll flow with mTLS client certificate
  • /cacerts response format and PKCS#7 encoding
  • /csrattrs response with configured OID sets
  • /serverkeygen key pair generation and certificate issuance
  • EST label routing to different CAs
  • CSR policy rejection (wrong key type, missing SAN, bad subject DN)
  • OTP rate limiting and lockout behavior
  • Concurrent enrollment under load
  • HA failover (with mock CA health checks)
  • Audit log correctness

EST protocol testing with curl and openssl

These commands verify kipuka’s EST implementation at the protocol level. They assume a running server at localhost:9443 with the test PKI from contrib/local-dev/setup-ca.sh.

Fetch CA certificates

The /cacerts endpoint requires no authentication:

curl -sk https://localhost:9443/.well-known/est/cacerts \
  | base64 -d \
  | openssl pkcs7 -inform DER -print_certs

Expected output: the PEM-encoded CA certificate chain.

Enroll with OTP

Generate an OTP via the admin API, then enroll:

# Generate OTP
OTP=$(curl -sk -X POST https://localhost:9443/admin/otp \
  -H "Authorization: Bearer $KIPUKA_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"entity_id": "test-device"}' \
  | jq -r '.otp')

# Generate CSR
openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout test.key -out test.csr -nodes \
  -subj "/CN=test-device"

# Enroll
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  -u "test-device:${OTP}" \
  --data-binary @test.csr \
  -H "Content-Type: application/pkcs10" \
  -o test.p7 \
  -w "%{http_code}\n" \
  https://localhost:9443/.well-known/est/simpleenroll

# Extract certificate
openssl pkcs7 -inform DER -in test.p7 -print_certs -out test.pem

# Verify
openssl verify -CAfile contrib/local-dev/pki/ca.pem test.pem

Re-enroll with client certificate

Use the certificate from the previous step for mTLS authentication:

# Generate new CSR (with fresh key pair)
openssl req -new \
  -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
  -keyout test-new.key -out test-new.csr -nodes \
  -subj "/CN=test-device"

# Re-enroll with mTLS
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  --cert test.pem --key test.key \
  --data-binary @test-new.csr \
  -H "Content-Type: application/pkcs10" \
  -o test-new.p7 \
  -w "%{http_code}\n" \
  https://localhost:9443/.well-known/est/simplereenroll

# Extract renewed certificate
openssl pkcs7 -inform DER -in test-new.p7 -print_certs -out test-new.pem

Label routing

Test enrollment against a specific EST label:

# Enroll against the "iot" label (if configured)
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  -u "sensor-01:${OTP}" \
  --data-binary @sensor.csr \
  -H "Content-Type: application/pkcs10" \
  -o sensor.p7 \
  https://localhost:9443/.well-known/est/iot/simpleenroll

# Verify the issuer matches the CA bound to the "iot" label
openssl pkcs7 -inform DER -in sensor.p7 -print_certs \
  | openssl x509 -noout -issuer

Server key generation

Test the /serverkeygen endpoint (if enabled in config):

# Request server-generated key pair
curl -sk \
  --cacert contrib/local-dev/pki/ca.pem \
  --cert test.pem --key test.key \
  --data-binary @test.csr \
  -H "Content-Type: application/pkcs10" \
  -o serverkeygen.p7 \
  https://localhost:9443/.well-known/est/serverkeygen

The response is a multipart MIME message containing both the certificate (PKCS#7) and the server-generated private key (PKCS#8).

TLS handshake inspection

Use openssl s_client to inspect the TLS configuration:

# Connect and show server certificate
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  -servername localhost \
  2>/dev/null | openssl x509 -noout -text

# Test mTLS with client certificate
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  -cert contrib/local-dev/pki/client.pem \
  -key contrib/local-dev/pki/client-key.pem \
  -servername localhost

# Show negotiated cipher and protocol
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  2>/dev/null | grep -E "Protocol|Cipher"

# Test TLS 1.3 only
openssl s_client -connect localhost:9443 \
  -CAfile contrib/local-dev/pki/ca.pem \
  -tls1_3 2>/dev/null | grep "Protocol"

HSM testing with Kryoptic

Test the PKCS#11 code path using the Kryoptic SoftHSM container from compose.yaml:

# Start Kryoptic
docker compose --profile hsm up -d kryoptic

# Initialize token and generate test key (see Development Setup for details)
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-token --slot 0 --label "test-hsm" --so-pin 12345678
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --init-pin --slot 0 --login --so-pin 12345678 --new-pin 1234
pkcs11-tool --module /usr/lib/libkryoptic.so \
  --login --pin 1234 \
  --keypairgen --key-type EC:prime256v1 \
  --id 01 --label "test-ca-key"

# Run HSM-specific tests
cargo test -p kipuka-hsm --features integration

HSM tests verify:

  • PKCS#11 session lifecycle (open, login, operate, close)
  • Key lookup by label and ID
  • ECDSA and RSA signing operations
  • Error handling for unavailable slots, wrong PINs, missing keys
  • Concurrent signing from multiple threads (session pooling)

CI pipeline

What runs in CI

Every push and merge request triggers the following:

StepCommandPurpose
Format checkcargo fmt --checkEnforce consistent formatting
Lintcargo clippy -- -D warningsCatch common mistakes and style issues
Unit testscargo testAll per-crate unit tests
Integration testscargo test --features integrationFull-stack EST protocol tests
Build (release)cargo build --releaseVerify release compilation succeeds
Documentationcargo doc --no-depsVerify rustdoc builds without warnings

What requires manual or environment-specific testing

These tests cannot run in a standard CI runner and must be performed during development or in dedicated test environments:

TestReasonHow to run
Hardware HSM signingRequires physical HSM hardwareConnect a YubiHSM 2 or Luna, run cargo test -p kipuka-hsm --features hsm-hardware
PostgreSQL integrationRequires a running PostgreSQL instancedocker compose --profile postgres up -d && cargo test --features integration-postgres
MariaDB integrationRequires a running MariaDB instancedocker compose --profile mariadb up -d && cargo test --features integration-mariadb
Dogtag PKI back-endRequires a running Dogtag instanceDeploy Dogtag in a container, configure connection in test config
GSSAPI authenticationRequires a KDC (FreeIPA or AD)Set up FreeIPA in a container, create service principal, run GSSAPI tests
NIAP compliance auditManual review against Protection ProfileFollow the checklist in docs/compliance/niap.md

Integration testing with idm-ci / Beaker

For full end-to-end testing of kipuka integrated with FreeIPA (IPA-to-kipuka certificate enrollment flows), use the idm-ci framework or Beaker test infrastructure.

idm-ci

idm-ci provisions multi-host test environments with FreeIPA servers, kipuka instances, and client machines. Tests exercise the complete enrollment lifecycle:

  1. FreeIPA issues a Kerberos ticket to the client
  2. Client authenticates to kipuka using GSSAPI
  3. kipuka maps the Kerberos principal to a certificate subject
  4. kipuka issues a certificate
  5. Client installs the certificate and uses it for mTLS re-enrollment

These tests are defined outside the kipuka repository and are triggered by the IdM CI infrastructure.

Local smoke test with FreeIPA container

For a lightweight local approximation:

# Start FreeIPA in a container
podman run -d --name freeipa \
  -h ipa.example.test \
  -p 389:389 -p 443:443 -p 88:88 -p 464:464 \
  quay.io/freeipa/freeipa-server:latest \
  ipa-server-install --unattended \
    --realm EXAMPLE.TEST \
    --domain example.test \
    --ds-password Secret123 \
    --admin-password Secret123

# Wait for installation to complete (5-10 minutes)
podman logs -f freeipa

# Create a service principal for kipuka
podman exec freeipa kinit admin <<< "Secret123"
podman exec freeipa ipa service-add HTTP/kipuka.example.test
podman exec freeipa ipa-getkeytab \
  -s ipa.example.test \
  -p HTTP/kipuka.example.test \
  -k /tmp/kipuka.keytab

# Copy the keytab out
podman cp freeipa:/tmp/kipuka.keytab ./kipuka.keytab

Then configure kipuka’s [gssapi] section to point to the extracted keytab and run enrollment tests using curl --negotiate.

Test data cleanup

Test runs that create database state (OTP tokens, certificates, audit entries) use temporary SQLite databases by default. To clean up after manual testing against a persistent database:

# Delete the development SQLite database
rm -f kipuka-dev.db

# For PostgreSQL, drop and recreate the database
psql -U kipuka -h localhost -c "DROP DATABASE kipuka; CREATE DATABASE kipuka;"

Re-run migrations after cleanup:

cargo run -- migrate --config kipuka.toml

Database Migrations

kipuka manages its database schema through sequential migration files. Every schema change is expressed as a migration that runs exactly once on each database, tracked by a migration history table.

Migration file layout

Migrations live under migrations/ with one subdirectory per supported database backend:

migrations/
  sqlite/
    0001_initial_schema.sql
    0002_add_audit_log.sql
    0003_add_cert_inventory.sql
  postgres/
    0001_initial_schema.sql
    0002_add_audit_log.sql
    0003_add_cert_inventory.sql
  mariadb/
    0001_initial_schema.sql
    0002_add_audit_log.sql
    0003_add_cert_inventory.sql

Naming convention

Migration files follow the pattern NNNN_description.sql where:

  • NNNN is a zero-padded sequential number starting at 0001
  • description is a lowercase, underscore-separated summary of the change
  • The .sql extension is required

Examples:

0004_add_otp_lockout_columns.sql
0005_create_ha_state_table.sql
0006_add_label_to_certs.sql

The numeric prefix determines execution order. kipuka runs migrations in ascending order and skips any that have already been applied (tracked in the _sqlx_migrations table).

Running migrations

Automatic migration on startup

When auto_migrate = true in the [db] configuration section, kipuka applies pending migrations automatically when the server starts:

[db]
url = "sqlite:///var/lib/kipuka/kipuka.db?mode=rwc"
auto_migrate = true

This is convenient for development but may not be appropriate for production environments where schema changes require review and approval.

Explicit migration command

Run migrations manually using the migrate subcommand:

kipuka migrate --config kipuka.toml

Output:

Applied 0001_initial_schema (23ms)
Applied 0002_add_audit_log (11ms)
Applied 0003_add_cert_inventory (15ms)
3 migrations applied successfully

If all migrations have already been applied:

No pending migrations

Checking migration status

View which migrations have been applied:

kipuka migrate --config kipuka.toml --status

Output:

Migration                        Applied At
0001_initial_schema              2026-06-20T10:00:00Z
0002_add_audit_log               2026-06-20T10:00:00Z
0003_add_cert_inventory          2026-06-20T10:00:01Z
0004_add_otp_lockout_columns     (pending)

The three-backend rule

Every migration must have counterparts for all three database backends. A migration that adds a column to a table in SQLite must also add that column in PostgreSQL and MariaDB. This ensures that kipuka can be deployed against any supported backend without schema drift.

The migration runner selects the correct subdirectory based on the database URL scheme:

URL schemeMigration directory
sqlite://migrations/sqlite/
postgres:// or postgresql://migrations/postgres/
mysql:// or mariadb://migrations/mariadb/

Creating a new migration

Step 1: Choose a descriptive name

Pick a name that describes the change, not the ticket number:

Good:  0007_add_gssapi_principal_mapping.sql
Bad:   0007_issue_42.sql

Step 2: Write the SQL for each backend

Create three files with the same sequence number and description:

touch migrations/sqlite/0007_add_gssapi_principal_mapping.sql
touch migrations/postgres/0007_add_gssapi_principal_mapping.sql
touch migrations/mariadb/0007_add_gssapi_principal_mapping.sql

Each file contains the DDL for that specific backend.

SQLite example

-- migrations/sqlite/0007_add_gssapi_principal_mapping.sql

CREATE TABLE IF NOT EXISTS gssapi_mappings (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    principal   TEXT NOT NULL UNIQUE,
    subject_dn  TEXT NOT NULL,
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);

CREATE INDEX idx_gssapi_mappings_principal ON gssapi_mappings(principal);

PostgreSQL example

-- migrations/postgres/0007_add_gssapi_principal_mapping.sql

CREATE TABLE IF NOT EXISTS gssapi_mappings (
    id          SERIAL PRIMARY KEY,
    principal   TEXT NOT NULL UNIQUE,
    subject_dn  TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_gssapi_mappings_principal ON gssapi_mappings(principal);

MariaDB example

-- migrations/mariadb/0007_add_gssapi_principal_mapping.sql

CREATE TABLE IF NOT EXISTS gssapi_mappings (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    principal   VARCHAR(512) NOT NULL UNIQUE,
    subject_dn  VARCHAR(1024) NOT NULL,
    created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE INDEX idx_gssapi_mappings_principal ON gssapi_mappings(principal);

Step 3: Update sqlx query metadata

After adding migrations, regenerate the sqlx offline query data so that compile-time query checking works without a live database:

cargo sqlx prepare --workspace

This updates the .sqlx/ directory with query metadata for all three backends.

Schema differences between backends

While the logical schema is identical across backends, SQL syntax differences require backend-specific migration files.

Type mapping

Logical typeSQLitePostgreSQLMariaDB
Auto-increment PKINTEGER PRIMARY KEY AUTOINCREMENTSERIAL PRIMARY KEYINT AUTO_INCREMENT PRIMARY KEY
TimestampTEXT (ISO 8601 strings)TIMESTAMPTZTIMESTAMP
Variable textTEXTTEXTVARCHAR(n) or TEXT
BooleanINTEGER (0/1)BOOLEANTINYINT(1)
Binary dataBLOBBYTEABLOB

Default value expressions

BackendCurrent timestampUUID generation
SQLitestrftime('%Y-%m-%dT%H:%M:%SZ', 'now')Application-generated
PostgreSQLNOW()gen_random_uuid()
MariaDBCURRENT_TIMESTAMPUUID()

Feature availability

  • PostgreSQL supports ON CONFLICT DO UPDATE (upsert) natively.
  • SQLite supports INSERT OR REPLACE and ON CONFLICT (3.24+).
  • MariaDB uses INSERT ... ON DUPLICATE KEY UPDATE.

When writing migrations that use upsert-like behavior, use the backend-appropriate syntax.

Rules for migration safety

Never modify a released migration

Once a migration has been applied to any environment (including other developers’ local databases), it is immutable. To change an existing table:

  1. Create a new migration with the next sequence number
  2. Use ALTER TABLE to modify the schema
  3. Provide the new migration for all three backends

Additive changes only

Prefer adding columns, tables, and indexes. Avoid dropping columns or tables unless absolutely necessary. If a column is no longer used:

  1. Stop writing to it in the application code
  2. After a release cycle, add a migration to drop the column

Handle NULL for new columns

When adding a column to an existing table, either provide a DEFAULT value or allow NULL. Existing rows cannot retroactively satisfy a NOT NULL constraint without a default:

-- Correct: new column with a default
ALTER TABLE otps ADD COLUMN locked_until TEXT DEFAULT NULL;

-- Incorrect: will fail if the table has existing rows
ALTER TABLE otps ADD COLUMN locked_until TEXT NOT NULL;

Test data migration

If a migration transforms existing data (not just schema), include the data transformation in the same migration file:

-- Add new column
ALTER TABLE audit_log ADD COLUMN auth_method TEXT DEFAULT 'unknown';

-- Backfill existing rows
UPDATE audit_log SET auth_method = 'mtls' WHERE auth_method = 'unknown';

Testing migrations against all backends

Before committing a new migration, verify it applies cleanly against all three database backends:

# SQLite (in-memory)
cargo run -- migrate --config test-sqlite.toml

# PostgreSQL
docker compose --profile postgres up -d
cargo run -- migrate --config test-postgres.toml

# MariaDB
docker compose --profile mariadb up -d
cargo run -- migrate --config test-mariadb.toml

The CI pipeline runs migrations against SQLite automatically. PostgreSQL and MariaDB migration tests require the corresponding Compose profiles and are part of the extended test suite.

Rollback strategy

kipuka migrations are forward-only. There is no built-in migrate down command. This is a deliberate design choice: automated rollback of DDL changes is unreliable in production (data loss, constraint violations, transaction semantics vary by backend).

Manual rollback procedure

If a migration must be undone:

  1. Write a new forward migration that reverses the change:

    -- 0008_revert_gssapi_mappings.sql
    DROP TABLE IF EXISTS gssapi_mappings;
    
  2. Apply it normally:

    kipuka migrate --config kipuka.toml
    

Emergency rollback

In an emergency where the server cannot start due to a bad migration:

  1. Restore the database from backup
  2. Remove the problematic migration files from migrations/
  3. Restart kipuka

For PostgreSQL and MariaDB, point-in-time recovery (PITR) can restore the database to the moment before the migration ran. For SQLite, restore from a filesystem-level backup.

Backup before migrating

Always back up the database before applying migrations in production:

# SQLite
cp /var/lib/kipuka/kipuka.db /var/lib/kipuka/kipuka.db.bak

# PostgreSQL
pg_dump -U kipuka kipuka > kipuka-backup.sql

# MariaDB
mysqldump -u kipuka -p kipuka > kipuka-backup.sql

Contributing

kipuka welcomes contributions. This document covers the license, code style, commit conventions, security invariants, and how to report vulnerabilities.

License

kipuka is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). The full license text is in the LICENSE file at the repository root.

Source: https://codeberg.org/czinda/kipuka

Contribution licensing

kipuka uses an inbound = outbound contribution model. By submitting a patch, you agree that your contribution is licensed under the same GPL-3.0-or-later terms as the rest of the project.

Developer Certificate of Origin (DCO)

All commits must include a Signed-off-by trailer certifying that you have the right to submit the contribution under the project’s license. This follows the Developer Certificate of Origin (DCO v1.1).

Add it automatically with git commit -s:

git commit -s -m "Add OTP lockout duration configuration"

This appends a line like:

Signed-off-by: Your Name <[email protected]>

Commits without a valid Signed-off-by line will be rejected.

Code style

Formatting

All Rust code must pass cargo fmt with no modifications:

cargo fmt --check

Format before committing:

cargo fmt

Linting

All code must compile cleanly under cargo clippy with warnings treated as errors:

cargo clippy -- -D warnings

Fix clippy warnings before submitting. If a specific lint is intentionally suppressed, add an #[allow(...)] attribute with a comment explaining why:

#![allow(unused)]
fn main() {
// Clippy suggests using `map_or`, but the explicit match is clearer
// for this error-handling path.
#[allow(clippy::manual_map)]
fn resolve_ca(&self, label: &str) -> Option<&CaConfig> {
    // ...
}
}

Rust edition

kipuka uses Rust edition 2021. Do not use unstable features or nightly-only APIs.

Commit message conventions

Follow a conventional commit style:

<type>: <short summary>

<optional body explaining what and why>

Signed-off-by: Your Name <[email protected]>

Types

TypeUse for
featNew functionality visible to users or API consumers
fixBug fix
refactorCode restructuring with no behavior change
testAdding or updating tests
docsDocumentation changes
choreBuild system, CI, dependency updates
securitySecurity-related fixes (see disclosure process below)

Examples

feat: add GSSAPI principal-to-subject mapping

Administrators can now define explicit mappings from Kerberos principals
to X.509 subject DNs in kipuka.toml under [gssapi.principal_mapping].
A default template is also supported for unmapped principals.

Signed-off-by: Alice Engineer <[email protected]>
fix: prevent OTP reuse after max_uses reached

The use_count check was off-by-one, allowing one extra use beyond
max_uses.  This commit corrects the comparison to use strict less-than.

Signed-off-by: Bob Developer <[email protected]>

Scope

Keep commits focused on a single logical change. A commit that adds a feature, fixes a bug, and reformats unrelated code should be split into three commits.

Security invariants

The following invariants are critical to kipuka’s security posture. Any patch that weakens or removes these protections will be rejected.

OTP values are never stored in plaintext

OTP tokens are hashed with argon2id (or the configured hash algorithm) immediately upon generation. The raw token is returned to the administrator exactly once in the HTTP response. Only the hash is persisted to the database.

#![allow(unused)]
fn main() {
// Correct: hash before storage
let hash = argon2::hash_encoded(otp.as_bytes(), &salt, &config)?;
db::store_otp_hash(entity_id, &hash).await?;

// WRONG: never store the raw OTP
// db::store_otp(entity_id, &otp).await?;
}

CA private keys are zeroized on drop

All types that hold CA private key material implement Zeroize and ZeroizeOnDrop from the zeroize crate. This ensures that key bytes are overwritten with zeros when the value goes out of scope, preventing residual key material in freed memory.

#![allow(unused)]
fn main() {
use zeroize::{Zeroize, ZeroizeOnDrop};

#[derive(Zeroize, ZeroizeOnDrop)]
struct CaPrivateKey {
    bytes: Vec<u8>,
}
}

Never use std::mem::forget or ManuallyDrop on types containing key material.

Timing-safe comparisons for all authentication checks

All authentication comparisons (OTP validation, bearer token checks, HMAC verification) use constant-time operations from the subtle crate. This prevents timing side-channel attacks that could leak information about valid credentials.

#![allow(unused)]
fn main() {
use subtle::ConstantTimeEq;

fn verify_token(provided: &[u8], expected: &[u8]) -> bool {
    provided.ct_eq(expected).into()
}
}

Never use == for comparing secrets, tokens, or hashes.

CSPRNG for serial numbers

Certificate serial numbers are generated using OsRng from the rand crate, which delegates to the operating system’s cryptographically secure random number generator (/dev/urandom on Linux, CryptGenRandom on Windows).

#![allow(unused)]
fn main() {
use rand::rngs::OsRng;
use rand::RngCore;

fn generate_serial() -> [u8; 20] {
    let mut serial = [0u8; 20];
    OsRng.fill_bytes(&mut serial);
    serial[0] &= 0x7f; // Ensure positive (RFC 5280)
    serial
}
}

Never use rand::thread_rng(), rand::random(), or any non-CSPRNG source for serial numbers, nonces, or key material. Non-cryptographic PRNGs are predictable and can lead to serial number collisions or key compromise.

No logging of sensitive material

The following data must never appear in log output at any verbosity level, including RUST_LOG=trace:

  • CA private keys or key bytes
  • OTP token values (raw or hashed)
  • CSR private keys
  • HSM PINs or passwords
  • Bearer tokens
  • TLS session keys

When logging certificate-related operations, log only the certificate fingerprint, subject DN, serial number, and outcome. When logging OTP operations, log only the entity ID and outcome (success/failure).

#![allow(unused)]
fn main() {
// Correct: log the fingerprint, not the key
tracing::info!(
    entity_id = %entity_id,
    fingerprint = %cert.fingerprint_sha256(),
    "Certificate issued"
);

// WRONG: never log key material
// tracing::debug!(key = ?ca_key, "Signing with CA key");
}

If you are unsure whether a value is sensitive, treat it as sensitive.

Reporting security vulnerabilities

Do not open a public issue for security vulnerabilities.

Report vulnerabilities privately by emailing [email protected] with:

  1. A description of the vulnerability
  2. Steps to reproduce
  3. Affected versions (if known)
  4. Any suggested fix (optional)

You will receive an acknowledgment within 48 hours and a detailed response within 7 business days. Security fixes are developed privately and disclosed after a patch is available.

If you prefer encrypted communication, request a PGP key in your initial email.

Submitting patches

Workflow

  1. Fork the repository on Codeberg

  2. Create a feature branch from main

  3. Make your changes (following the conventions above)

  4. Run the full check suite:

    cargo fmt --check
    cargo clippy -- -D warnings
    cargo test
    cargo test --features integration
    
  5. Commit with Signed-off-by (use git commit -s)

  6. Open a pull request against main

Pull request checklist

Before requesting review, verify:

  • All commits have Signed-off-by trailers
  • cargo fmt --check passes
  • cargo clippy -- -D warnings passes
  • cargo test passes
  • cargo test --features integration passes
  • New database migrations exist for all three backends (if applicable)
  • Security invariants listed above are preserved
  • No sensitive data in log statements, comments, or test fixtures

Review process

All pull requests require at least one maintainer review. Security-sensitive changes require two reviews. CI must pass before merge.

Feature requests and bug reports

File issues on the Codeberg issue tracker:

https://codeberg.org/czinda/kipuka/issues

For feature requests, describe the use case and expected behavior. For bugs, include:

  • kipuka version (kipuka --version)
  • Operating system and architecture
  • Database backend and version
  • Steps to reproduce
  • Expected vs. actual behavior
  • Relevant log output (with sensitive data redacted)