Repository: federator-certificate-manager
Description: The Federator Certificate Manager is a non-interactive Spring Boot service that automates X.509 certificate lifecycle management for federator components within the Node-Net.
SPDX-License-Identifier: Apache-2.0 AND OGL-UK-3.0
The Federator Certificate Manager is a non-interactive Spring Boot service that automates X.509 certificate lifecycle management for federator components within the National Digital Twin Programme (NDTP). It operates as a headless daemon — no HTTP endpoints are exposed — running two scheduled jobs that handle certificate renewal and filesystem synchronisation.
The service integrates with HashiCorp Vault (KV v2) for secret persistence, an external Management Node API for PKI operations (intermediate CA retrieval and CSR signing), and an OAuth2 Identity Provider for token-based authentication. All external HTTP communication is secured via mutual TLS (mTLS).
| Dependency | Version | Purpose |
|---|---|---|
| JDK | 21+ | Runtime and compilation |
| Apache Maven | 3.9+ | Build tool |
| HashiCorp Vault | 1.15+ | Secret storage (KV v2 engine) |
| Management Node | — | External PKI API (intermediate CA, CSR signing) |
| OAuth2 IdP | — | Identity Provider (e.g., Keycloak) for client credentials |
| JKS Keystores | — | Client keystore and truststore for mTLS |
- OS: Linux, macOS, or Windows with Java 21+
- Memory: 256 MB minimum heap (512 MB recommended)
- Disk: Writable path for PKCS#12 output files
- Network: Outbound HTTPS to Vault, Management Node, and IdP
- Automated Certificate Renewal — Monitors certificate validity against a configurable threshold and triggers renewal when approaching expiry
- Intermediate CA Management — Automatically fetches and refreshes the intermediate CA from the Management Node before it expires
- PKCS#12 KeyStore/TrustStore Generation — Produces
keystore.p12andtruststore.p12from PEM artifacts stored in Vault - Atomic Filesystem Writes — Uses temp-file-then-rename to prevent partial writes on crash
- HashiCorp Vault Integration — Persists all cryptographic material (key pairs, certificates, CA chains, passwords) to Vault KV v2
- mTLS-Secured Communication — All outbound HTTP calls use mutual TLS with configurable JKS keystores
- OAuth2 Client Credentials Flow — Authenticates against an IdP with Caffeine-cached tokens and automatic refresh
- SBOM Generation — CycloneDX Maven plugin produces a Software Bill of Materials at build time
For detailed architecture documentation including C4 diagrams, sequence diagrams, and component descriptions, see:
| Document | Description |
|---|---|
| Architecture Overview | C4 context, container, and component diagrams |
| Certificate Lifecycle | Renewal and sync workflows with sequence diagrams |
| Configuration Reference | All application.yml properties with descriptions |
| Vault Integration | Vault KV v2 secret paths and operations |
| Security & mTLS | OAuth2 flow, mTLS setup, and token caching |
git clone git@github.com:National-Digital-Twin/federator-certificate-manager.git
cd federator-certificate-manager
mvn clean package -DskipTestsjava -jar target/federator-certificate-manager-1.0.1.jar --version# All tests
mvn test
# Specific test class
mvn test -Dtest=CertificateManagerServiceImplTest
# With coverage report
mvn verify# Check formatting (Palantir Java Format via Spotless)
mvn spotless:check
# Auto-fix formatting
mvn spotless:applyRunning locally with management-node? Skip to Local Development with Management Node for a streamlined setup.
Enable a KV v2 secrets engine at the mount path matching your configuration:
vault secrets enable -path=pki-client kv-v2Ensure the application's Vault token has read/write access to the configured secret path:
path "pki-client/data/node-net/client/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "pki-client/metadata/node-net/client/*" {
capabilities = ["read", "list"]
}
path "sys/mounts" {
capabilities = ["read"]
}The service requires a JKS keystore (containing the client private key and certificate) and a JKS truststore (containing trusted CA certificates) for mTLS communication with the Management Node and IdP.
# Example: Create a client keystore
keytool -genkeypair -alias client -keyalg RSA -keysize 2048 \
-keystore keystore.jks -storepass changeit \
-dname "CN=certificate-manager,O=NDTP,C=GB"
# Example: Import CA certificate into truststore
keytool -importcert -alias ca -file ca-cert.pem \
-keystore truststore.jks -storepass changeit -nopromptTo simulate starting from a bootstrap certificate, run the following command to provision a certificate with a short expiry window:
make bootstrap-test-certs-clean
If running in tandem with management node and IdP, this script will import the CA certificate to ensure trusted communication between components. The location of this certificate can be set within the Makefile in the ROOT_CA_DIR variable.
Every property in application.yml is backed by an environment variable with a sensible default. You can override any value by setting the corresponding environment variable — no YAML editing required.
See the Configuration Reference for full details.
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
VAULT_URI |
spring.cloud.vault.uri |
http://localhost:8200 |
Vault server URI |
VAULT_TOKEN |
spring.cloud.vault.token |
(empty) | Vault authentication token |
VAULT_PKI_MOUNT |
application.vault.pki-mount |
pki-client |
KV v2 mount path in Vault |
VAULT_SECRET_PATH |
application.vault.secret-path |
node-net/client |
Base relative path for secrets under the mount |
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
MTLS_KEYSTORE_PATH |
application.client.key-store |
...docker/keystore.jks |
Path to JKS keystore (client private key + cert) |
MTLS_KEYSTORE_PASSWORD |
application.client.key-store-password |
changeit |
Keystore password |
MTLS_TRUSTSTORE_PATH |
application.client.trust-store |
...docker/truststore.jks |
Path to JKS truststore (trusted CAs) |
MTLS_TRUSTSTORE_PASSWORD |
application.client.trust-store-password |
changeit |
Truststore password |
MTLS_KEYSTORE_TYPE |
application.client.key-store-type |
JKS |
Keystore format (JKS or PKCS12) |
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
OAUTH2_TOKEN_URI |
application.oauth2.token-uri |
https://localhost:8443/realms/mng-node/protocol/openid-connect/token |
OAuth2 token endpoint |
OAUTH2_CLIENT_ID |
application.oauth2.client-id |
MANAGEMENT_NODE_CLIENT |
OAuth2 client ID for client credentials grant |
MANAGEMENT_NODE_BASE_URL |
application.management-node.base-url |
https://localhost:8090 |
Management Node API base URL |
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
CERT_RENEWAL_RATE |
application.scheduling.certificate-manager.renewal-rate |
3600000 |
Renewal job interval in ms (default: 1 hour) |
CERT_SYNC_RATE |
application.scheduling.certificate-manager.sync-rate |
60000 |
Sync job interval in ms (default: 1 minute) |
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
CERT_RENEWAL_THRESHOLD |
application.certificate.renewal-threshold-percentage |
10 |
Percentage of validity remaining that triggers renewal |
CERT_KEY_SIZE |
application.certificate.key-size |
2048 |
RSA key size in bits (2048 or 4096) |
CERT_INTERMEDIATE_MIN_VALID_DAYS |
application.certificate.intermediate.min-valid-days |
14 |
Minimum days of CA validity before refresh |
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
CERT_SUBJECT_COUNTRY |
application.certificate.subject.country |
UK |
X.500 Country (C) |
CERT_SUBJECT_STATE |
application.certificate.subject.state |
South Yorkshire |
X.500 State (ST) |
CERT_SUBJECT_LOCALITY |
application.certificate.subject.locality |
Sheffield |
X.500 Locality (L) |
CERT_SUBJECT_ORG |
application.certificate.subject.organization |
Acme Digital Solutions Ltd |
X.500 Organization (O) |
CERT_SUBJECT_OU |
application.certificate.subject.organizational-unit |
Platform Engineering |
X.500 Organizational Unit (OU) |
CERT_SUBJECT_CN |
application.certificate.subject.common-name |
api.acme-digital.co.uk |
X.500 Common Name (CN) |
CERT_SUBJECT_ALT_NAMES |
application.certificate.subject.alt-names |
api.acme-digital.co.uk,api.internal.acme-digital.co.uk |
Comma-separated DNS SANs |
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
CERT_DEST_PATH |
application.certificate.destination.path |
/home/developer/test-secrets/ |
Base directory for generated files |
CERT_DEST_KEYSTORE_FILE |
application.certificate.destination.keystore-file |
keystore.p12 |
PKCS#12 keystore filename |
CERT_DEST_TRUSTSTORE_FILE |
application.certificate.destination.truststore-file |
truststore.p12 |
PKCS#12 truststore filename |
CERT_DEST_KEYSTORE_PASSWORD_FILE |
application.certificate.destination.keystore-password-file |
keystore.password |
Keystore password filename |
CERT_DEST_TRUSTSTORE_PASSWORD_FILE |
application.certificate.destination.truststore-password-file |
truststore.password |
Truststore password filename |
CERT_DEST_KEYSTORE_ALIAS |
application.certificate.destination.keystore-alias |
federator |
Private key alias in the keystore |
| Environment Variable | Property Path | Default | Description |
|---|---|---|---|
LOG_LEVEL_SPRING_SECURITY |
logging.level.org.springframework.security |
DEBUG |
Spring Security log level |
LOG_LEVEL_APP |
logging.level.uk.gov.dbt.ndtp.federator.certificate.manager |
DEBUG |
Application log level |
There are two approaches to configuring the application for different environments: Spring Profiles and environment variables. These can be combined.
Create an environment-specific configuration file named application-{profile}.yml alongside the default application.yml (or in an external config directory). Properties defined in the profile file override the defaults.
For example, to create a prod profile, place an application-prod.yml in src/main/resources/ or in an external config location:
# Activate the profile at runtime
java -jar target/federator-certificate-manager-1.0.1.jar --spring.profiles.active=prod
# Or via environment variable
SPRING_PROFILES_ACTIVE=prod java -jar target/federator-certificate-manager-1.0.1.jarSpring Boot resolves configuration in this order (last wins):
application.yml— base defaultsapplication-{profile}.yml— profile-specific overrides- Environment variables — highest precedence
This means a prod profile file only needs to contain the properties that differ from the defaults. Everything else is inherited.
Pass environment variables directly — no additional YAML files needed:
VAULT_URI=https://vault.prod.example.com:8200 \
VAULT_TOKEN=s.xxxxx \
MTLS_KEYSTORE_PATH=/etc/certs/keystore.jks \
MTLS_KEYSTORE_PASSWORD=secret \
MTLS_TRUSTSTORE_PATH=/etc/certs/truststore.jks \
MTLS_TRUSTSTORE_PASSWORD=secret \
OAUTH2_TOKEN_URI=https://idp.prod.example.com/realms/mng-node/protocol/openid-connect/token \
MANAGEMENT_NODE_BASE_URL=https://management-node.prod.example.com:8090 \
CERT_DEST_PATH=/etc/federator/secrets/ \
LOG_LEVEL_APP=INFO \
LOG_LEVEL_SPRING_SECURITY=WARN \
java -jar target/federator-certificate-manager-1.0.1.jarThis approach works well for container orchestrators (Docker, Kubernetes) where environment variables are the standard configuration mechanism.
Point to a config file outside the JAR:
java -jar target/federator-certificate-manager-1.0.1.jar \
--spring.config.location=file:/etc/federator/application.yml# Using Maven (development)
mvn spring-boot:run
# Using the JAR directly
java -jar target/federator-certificate-manager-1.0.1.jar
# With a Spring profile
java -jar target/federator-certificate-manager-1.0.1.jar --spring.profiles.active=prodCheck the application logs for successful startup:
INFO CertificateRenewalJob - Starting certificate renewal check...
INFO CertificateManagerServiceImpl - Intermediate CA is valid
INFO CertificateManagerServiceImpl - Certificate is valid. No renewal needed.
INFO CertificateSyncJob - Starting certificate sync...
INFO KeyStoreSyncServiceImpl - Keystore is up to date, skipping write
This section covers running the certificate manager locally alongside the management-node and its Keycloak/Vault infrastructure. It assumes you have already completed the management-node local setup.
- Management Node is running on
https://localhost:8090 - Keycloak is running on
https://localhost:8443with themng-noderealm configured - Vault is running on
http://localhost:8200, initialised and unsealed - The management-node's Vault PKI engine is configured (see management-node README, Vault Setup section)
The certificate manager uses a separate KV v2 engine (not the PKI engine) to store its secrets:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault secrets enable -path=pki-client kv-v2The sample data migration sets certificate_automation_enabled = FALSE for all organisations. Update the Environment Agency organisation to enable it:
docker exec -e PGPASSWORD=keycloak_db_user_password keycloak-postgres-1 \
psql -U keycloak_db_user -d keycloak_db -c \
"UPDATE mn.organisation SET certificate_automation_enabled = TRUE WHERE id = 1;"The certificate manager needs initial certificate material in Vault before it can start. This uses the management-node client (which already has request_bootstrap_certificate from the management-node setup) to request a bootstrap certificate on behalf of the Environment Agency organisation.
Run from the management-node/docker directory:
a. Generate a key pair for the organisation:
openssl genrsa -out env.key 2048
openssl req -new -key env.key -out env.csr -subj "/CN=api.env.gov.uk/O=Environment Agency/C=UK"b. Get a token and request the bootstrap certificate:
TOKEN=$(curl -sk --cert client.crt --key client.key \
-d "grant_type=client_credentials&client_id=management-node" \
-d "client_secret=${KEYCLOAK_CLIENT_SECRET}" \
"https://localhost:8443/realms/mng-node/protocol/openid-connect/token" | jq -r .access_token)
curl -sk --cert client.crt --key client.key \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg csr "$(cat env.csr)" '{organisationId: 1, csr: $csr}')" \
"https://localhost:8090/api/v1/certificate/bootstrap" \
--output bootstrap_bundle.zip
unzip -o bootstrap_bundle.zipThis produces certificate.pem and ca-chain.pem.
c. Deploy the bootstrap material to Vault:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault kv put pki-client/node-net/client/keypair \
privateKey="$(cat env.key)" \
publicKey="$(openssl rsa -in env.key -pubout 2>/dev/null)"
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault kv put pki-client/node-net/client/certificate \
certificate="$(cat certificate.pem)"
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault kv put pki-client/node-net/client/ca-chain \
chain="$(cat ca-chain.pem)"The cert-manager authenticates with Keycloak using its mTLS client certificate, not a client secret. Update the FEDERATOR_ENV client in the Keycloak admin console:
- Navigate to mng-node realm → Clients → FEDERATOR_ENV
- Go to the Credentials tab
- Change Client Authenticator to
X509 Certificate - Set Subject DN to
CN=api.env.gov.uk - Save
Then add the certificate roles:
- Go to the Service accounts roles tab
- Click Assign role → filter by management-node client
- Add:
create_keys,sign_certificate, andaccess_public_certificates
Note: The cert-manager must authenticate as
FEDERATOR_ENVbecause the management-node maps the JWT's client ID to an organisation via the Producer/ConsumeridpClientIdfield. A separate client ID would not resolve to any organisation. In production, each organisation's cert-manager would authenticate as that organisation's client.
Create src/main/resources/application-local.yml:
spring:
cloud:
vault:
token: <your_vault_root_token>
application:
vault:
secret-path: pki-client/node-net/client
client:
key-store: /tmp/federator-secrets/keystore.p12
trust-store: /tmp/federator-secrets/truststore.p12
key-store-type: PKCS12
oauth2:
client-id: FEDERATOR_ENV
certificate:
subject:
common-name: api.env.gov.uk
destination:
path: /tmp/federator-secrets/Note: The
key-storeandtrust-storepoint at the same destination path where the sync job writes. On startup, the sync job reads the bootstrap material from Vault and creates the initial keystores. The renewal job then uses those keystores for mTLS communication with Keycloak and the management-node. Noclient-secretis needed — the cert-manager authenticates with Keycloak using the X.509 client certificate in the keystore.
mvn spring-boot:run -Dspring-boot.run.profiles=localOn startup:
- The sync job runs first — reads the bootstrap material from Vault and writes
keystore.p12andtruststore.p12to the destination path - The renewal job runs next — uses those keystores for mTLS, detects the bootstrap certificate (short TTL + bootstrap OID), and triggers automatic renewal
If working correctly, you should see Bootstrap certificate detected. Initiating immediate renewal... in the logs.
To observe the renewal cycle more easily during development, you can add shorter intervals to your application-local.yml:
application:
scheduling:
certificate-manager:
renewal-rate: 60000 # 1 minute
sync-rate: 30000 # 30 seconds
certificate:
renewal-threshold-percentage: 100 # always renew# If running as a systemd service
sudo systemctl stop federator-certificate-manager
sudo systemctl disable federator-certificate-manager
# If running as a process
kill $(pgrep -f federator-certificate-manager)# Remove the application JAR and configuration
rm -rf /opt/federator-certificate-manager/
rm -f /etc/systemd/system/federator-certificate-manager.service
# Remove generated keystores and passwords
rm -rf /etc/federator/secrets/# Remove all secrets under the configured path
vault kv metadata delete pki-client/node-net/client/keypair
vault kv metadata delete pki-client/node-net/client/certificate
vault kv metadata delete pki-client/node-net/client/ca-chain
vault kv metadata delete pki-client/node-net/client/intermediate-ca
vault kv metadata delete pki-client/node-net/client/keystore-password
vault kv metadata delete pki-client/node-net/client/truststore-passwordvault token revoke ${VAULT_TOKEN}rm -f /etc/certs/keystore.jks /etc/certs/truststore.jks| Category | Technology | Version |
|---|---|---|
| Runtime | Java (JDK) | 21 |
| Framework | Spring Boot | 3.5.5 |
| Cloud | Spring Cloud (Vault) | 2025.0.0 |
| Cryptography | Bouncy Castle | 1.83 |
| HTTP Client | Apache HttpClient 5 | managed |
| Caching | Caffeine | managed |
| DTO Mapping | ModelMapper | 3.2.0 |
| Build | Apache Maven | 3.9+ |
| Formatting | Spotless (Palantir) | 2.46.1 |
| Coverage | JaCoCo | 0.8.13 |
| SBOM | CycloneDX | 2.9.1 |
| Testing | JUnit 5 + Mockito | 5.10.0 |
This repository has been developed with public funding as part of the National Digital Twin Programme (NDTP), a UK Government initiative. NDTP, alongside its partners, has invested in this work to advance open, secure, and reusable digital twin technologies for any organisation, whether from the public or private sector, irrespective of size.
This repository contains both source code and documentation, which are covered by different licenses:
- Code: Licensed under the Apache License 2.0.
- Documentation: Licensed under the Open Government Licence v3.0.
See LICENSE.md, OGL_LICENCE.md and NOTICE.md for details.
We take security seriously. If you believe you have found a security vulnerability in this repository, please follow our responsible disclosure process outlined in SECURITY.md.
This project generates a CycloneDX SBOM during the package phase:
mvn package
# Output: target/federator-certificate-manager-1.0.0.jarDownload the latest SBOM for this codebase to view the current list of components.
We welcome contributions that align with the Programme's objectives. Please read our CONTRIBUTING.md guidelines before submitting pull requests.
This repository has benefited from collaboration with various organisations. For a list of acknowledgments, see ACKNOWLEDGEMENTS.md.
For questions or support, check our Issues or contact the NDTP team by emailing ndtp@businessandtrade.gov.uk.
Maintained by the National Digital Twin Programme (NDTP).
© Crown Copyright 2026. This work has been developed by the National Digital Twin Programme and is legally attributed to the Department for Business and Trade (UK) as the governing entity.