Skip to content

Repository files navigation

Signet

Signet is a narrow, CRD-free X.509-SVID issuer built specifically for Interlink workloads on Kubernetes. It watches the built-in certificates.k8s.io/v1 CertificateSigningRequest API, derives one SPIFFE identity from the authenticated Kubernetes service account, approves requests that satisfy the fixed profile, and writes a directly signed leaf certificate to CSR status.

Signet is not a general-purpose PKI, cert-manager replacement, admission webhook, or network certificate service.

Security model

A request is accepted only when all of these facts agree:

  • spec.signerName is Signet's configured custom signer.
  • The API-server-populated username is system:serviceaccount:<namespace>:<name> and that service account is explicitly allowlisted.
  • spec.usages is exactly digital signature, server auth, and client auth.
  • spec.expirationSeconds is between 600 seconds and the configured maximum.
  • spec.request is one RFC 7468 CERTIFICATE REQUEST containing canonical PKCS #10 v1 DER with a valid Ed25519 proof-of-possession signature.
  • The request has an empty subject and exactly one URI SAN.
  • The URI is exactly spiffe://<trust-domain>/ns/<namespace>/sa/<name>, derived from the authenticated username rather than trusted from the CSR.

Every mismatch is denied with a stable reason code. Signet never copies a requested subject, DNS name, IP address, email address, CA bit, arbitrary extension, or arbitrary lifetime into a certificate.

The online signing CA is intentionally simple: a persistent Ed25519 key in one Kubernetes Secret. Protect that Secret with Kubernetes encryption at rest, tightly limit API access, and keep a protected offline backup. A compromised Signet pod or CA Secret can issue credentials within the CA's trust domain. Signet does not provide HSM integration, revocation, or automatic CA rotation.

The supplied Deployment runs one replica deliberately. Multiple uncoordinated replicas could sign the same newly observed CSR before either status update becomes visible. Kubernetes restarts the single replica, and rolling updates use a surge pod, but there is no leader election.

See RFC_PROFILE.md for the complete certificate, request, and signer contract. The profile is backed by archived RFC Editor source texts and checksums in rfcs/.

Request flow

sequenceDiagram
    participant W as Interlink workload
    participant K as Kubernetes API
    participant S as Signet
    W->>W: Generate Ed25519 key and PKCS #10 request
    W->>K: Create CertificateSigningRequest
    K->>K: Record authenticated service-account username
    S->>K: Watch Signet CSRs
    S->>S: Validate policy, proof of possession, and SPIFFE URI
    S->>K: Add Approved or Denied condition
    S->>K: Write signed leaf to status.certificate
    W->>K: Read issued certificate
Loading

Signet exposes only /healthz and /readyz for kubelet probes. Certificate operations use the Kubernetes API; there is no Signet service endpoint.

Certificate profile

The root and leaf profiles are fixed rather than configurable:

Property Signing CA Interlink leaf
Key and signature Ed25519 Ed25519
Subject Configured non-empty common name Empty
SAN None One critical SPIFFE URI
Basic constraints Critical, cA=TRUE, pathLen=0 Critical, cA=FALSE
Key usage Critical keyCertSign, cRLSign Critical digitalSignature
Extended key usage None serverAuth, clientAuth
Serial Positive, non-zero, at most 20 octets Positive, non-zero, at most 20 octets
Chain Self-signed trust anchor Directly signed by the configured trust anchor
Leaf lifetime Not applicable Exact requested interval, at most 24 hours by default, with a five-minute notBefore backdate

Interlink currently transports one certificate, so Signet emits a directly signed leaf rather than a leaf-plus-intermediate chain. The trust anchor is distributed separately and is never placed in CSR status.

Minimal deployment

Prerequisites: Kubernetes v1.22 or later and kubectl with Kustomize support.

  1. Choose an immutable container image. CI publishes one OCI manifest for both linux/amd64 and linux/arm64 to ghcr.io/alexandernicholson/signet only after every check passes. A push to main publishes latest and sha-<full-commit>; a v* tag also publishes the tag and semantic-version aliases. Pull by digest in production:

    docker pull ghcr.io/alexandernicholson/signet:latest
    docker buildx imagetools inspect ghcr.io/alexandernicholson/signet:latest

    Pull requests still build and smoke-test the Dockerfile but cannot publish. A local single-platform image can be built with docker build -t signet ..

  2. Generate the persistent CA locally. The command refuses to overwrite any existing output:

    cargo run --release -- bootstrap \
      --output-dir deploy/ca \
      --common-name "Interlink production root" \
      --validity-days 3650

    deploy/ca/ is gitignored. Back up ca.key securely before deployment.

  3. Edit deploy/signet.yaml:

    • Set SIGNET_TRUST_DOMAIN.
    • Set the exact comma-separated SIGNET_ALLOW_SERVICE_ACCOUNTS allowlist.
    • Set SIGNET_SIGNER_NAME if the default is not suitable.
    • If the signer name changes, update the ClusterRole signers.resourceNames entry to the same value. Kubernetes checks the virtual approve and sign verbs against that exact signer.
    • Pin image to the digest you published.
  4. Review the rendered objects, then apply them:

    kubectl kustomize deploy > /tmp/signet-rendered.yaml
    kubectl apply -k deploy
    kubectl -n signet-system rollout status deployment/signet
    kubectl -n signet-system get pods
  5. Distribute the public trust bundle to the namespace that runs Interlink. ConfigMaps cannot be mounted across namespaces; the generated signet-system/interlink-trust-bundle is a source copy, not an automatic workload mount. For a workload in workloads:

    kubectl -n workloads create configmap interlink-trust-bundle \
      --from-file=ca-bundle.pem=deploy/ca/bundle.pem \
      --dry-run=client -o yaml | kubectl apply -f -

    Mount that ConfigMap and configure Interlink to trust ca-bundle.pem. Never mount ca.key into a workload pod.

  6. Confirm the controller is ready and inspect CSR outcomes:

    kubectl -n signet-system get deployment signet
    kubectl get certificatesigningrequests
    kubectl describe certificatesigningrequest <name>

The manifest grants only cluster-wide CSR read access, approval/status mutation, and signer-scoped approve/sign authorization. The CA volume is group-readable only by the non-root Signet process.

Configuration

signet run accepts flags or matching environment variables:

Setting Default Meaning
SIGNET_CA_CERTIFICATE /var/run/signet-ca/ca.crt Signing CA certificate PEM
SIGNET_CA_PRIVATE_KEY /var/run/signet-ca/ca.key Matching PKCS #8 Ed25519 private key PEM
SIGNET_SIGNER_NAME example.com/signet Custom Kubernetes signer name
SIGNET_TRUST_DOMAIN Required SPIFFE trust domain
SIGNET_ALLOW_SERVICE_ACCOUNTS Required Comma-separated <namespace>:<name> allowlist
SIGNET_MAX_TTL_SECONDS 86400 Maximum requested certificate interval
SIGNET_HEALTH_ADDRESS 0.0.0.0:8080 Probe listener

Readiness remains false until the initial Kubernetes CSR watch has completed. Liveness reports immediately after the probe listener starts.

Workload credential agent

Run signet agent in the Interlink pod to generate its Ed25519 private key, submit the exact Signet CSR profile, poll the named CSR, validate the returned certificate and trust chain, and write tls.key and tls.crt to a shared volume. The namespace and service-account arguments are mandatory, local configuration; the API server independently records the authenticated service-account username on the CSR, and Signet requires both identities to match.

/signet agent \
  --trust-domain example.com \
  --namespace workloads \
  --service-account example-app \
  --ca-bundle /var/run/interlink/ca-bundle.pem \
  --output-dir /var/run/interlink/identity \
  --reload-address 127.0.0.1:4192

Continuous mode renews before expiry. After each newly issued credential is durably installed, it sends a bounded HTTP/1.1 POST /reload request to the configured loopback address and retries failures. Use --once in an init container to acquire one credential and exit without sending a reload request. The private key never enters Kubernetes; only the CSR and issued certificate do.

The workload service account needs only create and get on the cluster-scoped certificatesigningrequests.certificates.k8s.io resource. It does not need approval, status-update, list, watch, or Secret permissions. Mount the public CA bundle read-only and place the output directory on a volume shared with Interlink.

Configure the Interlink container to read the same files with INTERLINK_CA_BUNDLE_PATH=/var/run/interlink/ca-bundle.pem, INTERLINK_CERT_PATH=/var/run/interlink/identity/tls.crt, and INTERLINK_KEY_PATH=/var/run/interlink/identity/tls.key. Set INTERLINK_IDENTITY to the SPIFFE ID formed from the agent's trust-domain, namespace, and service-account arguments. The pod must give both containers a shared supplementary group (for example with fsGroup) because the agent writes the private key with mode 0640.

Setting Default Meaning
SIGNET_SIGNER_NAME example.com/signet Must equal the issuer's signer name
SIGNET_TRUST_DOMAIN Required SPIFFE trust domain
SIGNET_AGENT_NAMESPACE Required Workload namespace
SIGNET_AGENT_SERVICE_ACCOUNT Required Workload service-account name
SIGNET_AGENT_CA_BUNDLE /var/run/interlink/ca-bundle.pem One or more trusted Signet CA PEM blocks
SIGNET_AGENT_OUTPUT_DIR /var/run/interlink/identity Directory receiving tls.key and tls.crt
SIGNET_AGENT_EXPIRATION_SECONDS 86400 Exact requested certificate interval
SIGNET_AGENT_RENEW_BEFORE_SECONDS 43200 Renewal lead time
SIGNET_AGENT_REQUEST_TIMEOUT_SECONDS 30 CSR and reload timeout
SIGNET_AGENT_POLL_INTERVAL_MILLIS 250 CSR and reload-retry interval
SIGNET_AGENT_RELOAD_ADDRESS 127.0.0.1:4192 Interlink reload listener

Development

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-targets
cargo llvm-cov --all-targets --fail-under-lines 100

The coverage gate enforces 100% source-line coverage. Every unit and integration test has a two-second deadline. Tests exercise the RFC profile, malformed and adversarial CSRs, CA persistence and validation, Kubernetes subresource patches, watch lifecycle, graceful shutdown, health behavior, CLI failures, deployment resource generation, and rollback after partial writes.

For reproducible hot-path commands and measured optimization rounds, see BENCHMARKS.md.

References

License

MIT

About

CRD-free Kubernetes X.509-SVID issuer for Interlink

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages