Skip to main content

EJBCA

The EJBCA upstream authority integrates Defakto with Keyfactor EJBCA. Instead of operating as a self-signed root CA, the Trust Domain Server obtains its intermediate CA certificate from EJBCA, chaining the SPIFFE trust bundle to your existing EJBCA-issued PKI.

How it works​

On startup or during a CA rotation, the Trust Domain Server generates a fresh key pair and submits a CSR to EJBCA's REST API using a pre-configured Certificate Profile and End Entity Profile, requesting the CA chain be returned inline with the enrollment response. EJBCA signs the CSR and returns the new intermediate CA certificate together with its issuing chain in that same response. The Trust Domain Server then uses the issued intermediate CA to sign workload SVIDs.

The Trust Domain Server authenticates to EJBCA using a bearer token read from a file. This is a separate credential from the intermediate CA certificate it is requesting. EJBCA must be configured to trust the token's issuer (a Trusted OAuth Provider) and map it to a role permitted to enroll certificates.

How to Deploy​

Step 1 — Set Up EJBCA​

  1. Create or identify the EJBCA CA that will issue the Trust Domain Server's intermediate CA certificate.
  2. Create a Certificate Profile that permits CA-type certificates (Basic Constraints: CA=true, pathLenConstraint=0, Key Usage: keyCertSign, cRLSign) with a validity period matching your desired CA rotation cadence. The Trust Domain Server only ever signs leaf SVIDs, never further CAs, so pathLenConstraint must be 0. EJBCA's built-in SUBCA profile already satisfies this and can be used as-is, or cloned as a starting point for a custom profile.
  3. Create an End Entity Profile referencing that Certificate Profile and CA. Every enrollment from the Trust Domain Server uses this profile.
  4. Enable the REST API under System Configuration → Protocol Configuration (not enabled by default in EJBCA Community or Enterprise).
  5. Register a Trusted OAuth Provider in EJBCA, and grant tokens issued for the Trust Domain Server a role permitted to call /v1/certificate/pkcs10enroll for the CA and End Entity Profile above. Recommended: register the cluster's own Kubernetes OIDC issuer as the Trusted OAuth Provider. This lets you use a projected Kubernetes service-account token in Step 2 below with no separate IdP or token-refresh mechanism to manage. Register a separate external IdP instead if that's your organization's standard.
  6. Arrange for a valid bearer token to be written to a file the Trust Domain Server can read, matching whichever Trusted OAuth Provider you registered above (see Step 2 below).

Certificate Profile requirements​

The Certificate Profile referenced by certificateProfileName must:

  • Set Basic Constraints → CA to true, with Path Length Constraint set to 0 so that the Trust Domain Server can only sign X.509-SVIDs and is prevented from signing CA certificates.
  • Set Key Usage to include Certificate Sign and CRL Sign.
  • Set a Validity matching your desired CA rotation cadence. EJBCA's Certificate Profile validity is authoritative: the Trust Domain Server reads the validity period off the certificate EJBCA actually issues and schedules its own rotation relative to that.

The built-in SUBCA profile meets all of the above and is the simplest starting point.

End Entity Profile requirements​

The End Entity Profile referenced by endEntityProfileName must be compatible with the CSR the Trust Domain Server actually submits: an empty Subject DN and a single URI Subject Alternative Name carrying the trust domain's SPIFFE ID (e.g. spiffe://example.com) — there is no Common Name, organization, or any other Subject DN field. Configure the profile's Subject DN Attributes → CN, Common name as "not required". A newly created End Entity Profile requires CN by default in EJBCA and will reject the enrollment with Subject DN field 'COMMONNAME' must exist. if left as-is. Enable Subject Alternative Name Attributes → URI, Uniform Resource Identifier, allowing at least one value. Omitting it fails enrollment with Wrong number of UNIFORMRESOURCEID fields in Subject Alternative Name..

Under Main Certificate Data, set the profile's Default Certificate Profile and Available Certificate Profiles to the Certificate Profile from the previous section, and its Default CA / Available CAs to the CA identified in step 1.

Step 2 — Mount the bearer token and CA bundle into the Trust Domain Server​

tokenPath and caCertPath are paths the Trust Domain Server reads from its own pod's filesystem — configuring them in UpstreamAuthority does not put anything on disk by itself. Something else needs to make those files exist at those exact paths, and — for the token specifically — keep them current, since EJBCA-issued tokens expire.

Both approaches below use the spirl-server Helm chart's additionalVolumes / additionalVolumeMounts values, which accept arbitrary Kubernetes volume and volume-mount specs. The Trust Domain Server re-reads the token file every minute, so any mechanism that rewrites the file at least that often works without a restart.

Recommended: a projected Kubernetes service-account token. If your Trusted OAuth Provider in EJBCA is the cluster's own Kubernetes OIDC issuer (rather than a separate IdP), use a serviceAccountToken projected volume. Kubelet mints and silently refreshes this token in place:

trustDomainDeployment:
deployment:
additionalVolumes:
- name: ejbca-token
projected:
sources:
- serviceAccountToken:
path: token
audience: ejbca # must match the aud claim EJBCA's Trusted OAuth Provider expects
expirationSeconds: 3600
additionalVolumeMounts:
- name: ejbca-token
mountPath: /var/run/secrets/ejbca
readOnly: true

Alternative: a Kubernetes Secret. For any OAuth provider that isn't the cluster's own service-account issuer — an external IdP, or a token minted by an operator-run process — put the token and CA bundle in a Secret instead, and have whatever mints the token keep updating it:

kubectl create secret generic ejbca-creds \
--from-file=token=/path/to/bearer-token \
--from-file=ca.pem=/path/to/ejbca-ca-bundle.pem \
-n <trust-domain-deployment-namespace>
trustDomainDeployment:
deployment:
additionalVolumes:
- name: ejbca-creds
secret:
secretName: ejbca-creds
additionalVolumeMounts:
- name: ejbca-creds
mountPath: /var/run/secrets/ejbca
readOnly: true

In this configuration nothing refreshes the Secret automatically. A sidecar, an operator, or a CronJob needs to rewrite it before the token expires.

If needed, the CA bundle to validate EJBCA's certification should be added as its own volume. A ConfigMap is a natural fit, since it is not secret material:

additionalVolumes:
- name: ejbca-ca
configMap:
name: ejbca-ca-bundle
items:
- key: ca.pem
path: ca.pem
additionalVolumeMounts:
- name: ejbca-ca
mountPath: /etc/spirl/ejbca
readOnly: true

Note: The tokenPath and caCertPath in the UpstreamAuthority config (next step) must match the mount path and the file's name exactly.

Step 3 — Update cluster configuration​

section: UpstreamAuthority
schema: v1
spec:
extensions:
ejbca:
baseURL: "https://ejbca.example.com/ejbca/ejbca-rest-api"
caName: "Defakto-Issuing-CA"
certificateProfileName: "Defakto-Intermediate-CA"
endEntityProfileName: "Defakto-Intermediate-CA-EEP"
authType: bearer
tokenPath: /var/run/secrets/ejbca/token
caCertPath: /etc/spirl/ejbca/ejbca-ca-bundle.pem
timeout: "30s"

Apply it using spirlctl:

spirlctl config set trust-domain-deployment --id <deployment-id> upstream-authority.yaml

Or using Terraform:

resource "spirl_trust_domain_deployment_config" "upstream_authority" {
trust_domain_deployment_id = spirl_trust_domain_deployment.example.id
sections = {
UpstreamAuthority = <<-YAML
section: UpstreamAuthority
schema: v1
spec:
extensions:
ejbca:
baseURL: "https://ejbca.example.com/ejbca/ejbca-rest-api"
caName: "Defakto-Issuing-CA"
certificateProfileName: "Defakto-Intermediate-CA"
endEntityProfileName: "Defakto-Intermediate-CA-EEP"
authType: bearer
tokenPath: /var/run/secrets/ejbca/token
caCertPath: /etc/spirl/ejbca/ejbca-ca-bundle.pem
timeout: "30s"
YAML
}
}

Once a configuration document passes validation and is stored, the Defakto control plane syncs it to your Trust Domain Servers automatically. No server or agent restart is required.

Configuration Reference​

FieldRequiredDefaultDescription
baseURLYes—Base URL of the EJBCA REST API, e.g. https://ejbca.example.com/ejbca/ejbca-rest-api.
caNameYes—Name of the EJBCA CA that will issue the Trust Domain Server's intermediate CA certificate.
certificateProfileNameYes—Name of the EJBCA Certificate Profile to use for enrollment.
endEntityProfileNameYes—Name of the EJBCA End Entity Profile to use for enrollment.
endEntityNameNoThe trust domain deployment's IDFixed end-entity username submitted with every enrollment. Stable across CA rotations by default, so EJBCA edits the same End Entity in place instead of accumulating a new one each rotation.
authTypeNobearerAuthentication type for outbound requests: none or bearer. bearer is the only authenticated option (this client does not support mTLS); use none only in the specific cases described below.
tokenPathYes, unless authType is explicitly set to none—Path to a file containing the bearer token. Something outside the Trust Domain Server (a sidecar, a projected service-account token, an operator refresh job) is responsible for keeping this file populated with an unexpired token.
caCertPathNoSystem root certificatesPath to a PEM file containing the CA bundle used to verify EJBCA's server TLS certificate.
timeoutNo30sMaximum time to wait for an EJBCA response, in Go duration format (e.g. 30s, 2m).
supplementalBundlePathNo—Path to a PEM file containing additional root certificates to include in the SPIFFE trust bundle alongside the upstream root EJBCA returns. Useful when bridging multiple PKI hierarchies or adding cross-signed roots. Unrelated to caCertPath: this affects what the trust bundle publishes, not how the Trust Domain Server connects to EJBCA.

authType: none sends no credential of any kind. EJBCA does not accept unauthenticated pkcs10enroll requests by default, so this only makes sense when something in front of EJBCA — an mTLS-terminating proxy, a network policy, a service mesh — authenticates the connection before it reaches EJBCA, or against a deliberately unauthenticated dev/test instance. For a real deployment, leave authType unset (or set it explicitly) to use bearer.

Step 4 — Verify​

On startup, the Trust Domain Server logs Starting EJBCA upstream authority. The SPIFFE trust bundle published to agents will include the EJBCA CA chain's root as an upstream root.

Check the deployment's signing keyset state:

spirlctl trust-domain info <trust-domain-name> --show-deployments --show-signing-status

Once the X.509 keyset shows Active, fetch the trust domain's SPIFFE bundle endpoint (shown in spirlctl trust-domain info <trust-domain-name>) and confirm the EJBCA-issued root certificate appears among the x509-svid keys.

Migration​

To migrate from the default self-signed CA to EJBCA:

  1. Apply the UpstreamAuthority Managed Config with the ejbca extension.
  2. The Trust Domain Server picks up the new configuration automatically. At the next CA rotation, it fetches a new intermediate CA certificate from EJBCA and begins issuing SVIDs chained to it.
  3. Agents automatically receive the updated trust bundle (including the new upstream roots) on their next bundle refresh.
  4. SVIDs signed with the old self-signed CA remain valid until they expire.

Relying parties that trust only the old self-signed root will fail to verify new SVIDs. Update relying party trust stores before or during migration.

Security Considerations​

  • Intermediate CA, not root. The Trust Domain Server holds a pathLen=0 intermediate CA certificate. It cannot issue intermediate CAs, only leaf SVIDs.
  • Bootstrap credential is separate from the issued CA. The bearer token used to authenticate to EJBCA is a distinct, independently managed credential. It is not derived from the CA certificate the Trust Domain Server is requesting.
  • Token file protection. The bearer token file should be readable only by the Trust Domain Server process. Treat it like any other bearer credential: Anyone who can read it can authenticate to EJBCA as the Trust Domain Server until the token expires or is revoked.
  • Single CA / profile pair. Each ejbca extension targets one CA, Certificate Profile, and End Entity Profile. EJBCA's account-binding and dynamic end-entity features are not used.
  • CA chain kept current. The Trust Domain Server receives the CA chain from EJBCA with every enrollment rather than relying on a chain pasted into configuration, so a re-keyed EJBCA CA is picked up without a config change.