Repository: management-node
Description: Provides APIs to be accessed by Consumer and Producer Federators for the purpose of dynamic configuration management
SPDX-License-Identifier: Apache-2.0 AND OGL-UK-3.0
The Management Node is a Spring Boot application that provides REST APIs for federator configuration management and certificate lifecycle operations (key generation, CSR signing, and bootstrap onboarding). It implements secure communication using Mutual TLS (mTLS) and zero-trust authentication via Keycloak.
For a full description of the database tables, relationships, and constraints, see the Database Schema documentation: docs/DATABASE_SCHEMA.md.
- Java 21
- Maven 3.9+
- Docker and Docker Compose
- OpenSSL (for certificate generation)
- Keycloak (for authentication and authorization)
- The below assumes your running in Linux - bash, it has been tested under WSL2.
Note: see lower for setting up prerequisites for local deployment certs, keycloak etc.
This project is a Spring Boot application. You can run it by supplying configuration via either:
- A default application.yml file, or
- A profile-specific file application-{profile}.yml and passing the profile argument at startup.
Quick options:
-
Provide a default config:
- Modify src/main/resources/application.yml with your local settings (SSL keystore/truststore, Keycloak client, DB, etc.). See the Configuration and Certificate Setup sections below.
- Run: (change to suit your local if different)
export POSTGRES_PASSWORD=keycloak_db_user_password export CERTPASSWORD=changeitor:mvn spring-boot:run
java -jar target/management-node-1.0.1.jar
-
Use a profile-specific config:
- Create src/main/resources/application-local.yml (replace "local" with your profile name) with your settings.
- Run with the profile:
or:
mvn spring-boot:run -Dspring-boot.run.profiles=local
java -jar target/management-node-1.0.1.jar --spring.profiles.active=local
- You can also set the environment variable:
export SPRING_PROFILES_ACTIVE=local
Notes:
- Spring Boot will load application.yml and then override with application-{profile}.yml if a profile is active.
- You may also point to an external YAML using:
java -jar target/management-node-0.0.1.jar --spring.config.location=/path/to/your.yml
- The application serves HTTPS on port 8090 by default (see server.ssl in configuration).
The Management Node Module implements a zero-trust security architecture using Mutual TLS (MTLS) for secure communication between all components. This section explains why certificates are needed, how to generate them, and where they are used in the system.
Note: For detailed instructions on configuring MTLS for both Keycloak and the Management Node, see the MTLS Configuration Guide.
-
Zero-Trust Security Model: The system follows a zero-trust approach where all communications must be authenticated and encrypted, regardless of whether they occur inside or outside the network perimeter.
-
Mutual TLS (MTLS): Unlike standard TLS where only the server authenticates itself to the client, MTLS requires both parties to authenticate each other using X.509 certificates.
-
Service-to-Service Authentication: Certificates provide a secure way for services to verify each other's identity without relying on passwords or API keys.
The system requires several certificate files:
-
Private Key (
localhost.key):- The private key used to sign and decrypt data
- Must be kept secure and never shared
- Used by both Keycloak and the Management Node
-
Certificate (
localhost.crt):- The public certificate containing the public key
- Shared with other services to verify the identity
- Used in both server and client authentication
-
PKCS12 Keystore (
localhost.p12):- A container format that stores the private key and certificate
- Used primarily for client authentication
- Imported by Keycloak for client certificate validation
-
Java Keystore (
keystore.jks):- Java-specific format for storing the server's private key and certificate
- Used by both Keycloak and the Management Node for their TLS endpoints
-
Java Truststore (
truststore.jks):- Contains certificates that the server trusts
- Used to validate client certificates during MTLS
For development purposes, follow these steps to generate certificates for mTLS. All passwords used are changeit. When generating these certficates, for the Country Name, you can use the value of 'UK'. All remaining certificate fields can be left to their default values.
move to the docker folder
cd docker-
Generate a Root CA certificate:
openssl req -x509 -sha256 -days 3650 -newkey rsa:4096 -keyout rootCA.key -out rootCA.crt
This creates a Root Certificate Authority (CA) that will be used to sign other certificates. The certificate is valid for 10 years (3650 days).
-
Generate a host certificate:
openssl req -new -newkey rsa:4096 -keyout localhost.key -out localhost.csr -nodes
This creates a private key and certificate signing request (CSR) for the host.
-
Sign the host certificate with the Root CA:
Create a file called
localhost.extfile should contain:authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE subjectAltName = @alt_names [alt_names] DNS.1 = localhost DNS.2 = keycloakopenssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in localhost.csr -out localhost.crt -days 365 -CAcreateserial -extfile localhost.ext
This signs the host CSR with the Root CA, creating a certificate valid for 365 days.
This configuration specifies that the certificate is valid for both
localhostandkeycloakhostnames. -
Create a PKCS12 keystore for the server:
openssl pkcs12 -export -out localhost.p12 -name "localhost" -inkey localhost.key -in localhost.crtThis bundles the host certificate and private key into a PKCS12 format.
-
Create a PEM file for Linux keystore:
openssl pkcs12 -in localhost.p12 -clcerts -nokeys -out localhost.pem
This extracts the certificate (without the private key) in PEM format.
-
Add the Root CA to the Trust Store:
keytool -importcert -noprompt -file rootCA.crt -alias clientca -keystore localhost.p12 -storetype PKCS12 -storepass changeit
This adds the Root CA to the trust store so that clients signed by this CA will be trusted.
-
Generate a client certificate:
openssl req -new -newkey rsa:4096 -nodes -keyout client.key -out client.csr
This creates a private key and CSR for the client.
-
Sign the client certificate with the Root CA:
openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in client.csr -out client.crt -days 365 -CAcreateserial
This signs the client CSR with the Root CA, creating a certificate valid for 365 days.
-
Create a PKCS12 keystore for the client:
openssl pkcs12 -export -out client.p12 -name "client" -inkey client.key -in client.crtThis bundles the client certificate and private key into a PKCS12 format for use in browsers or client applications.
-
Create a Java keystore using keytool (PKCS12 format, compatible with modern Java):
keytool -importkeystore -destkeystore keystore.jks -deststoretype PKCS12 -srckeystore localhost.p12 -srcstoretype PKCS12 -alias "localhost"This converts the PKCS12 keystore. Note: Despite the
.jksextension, modern keytool creates PKCS12 format by default, which is more secure and standard. -
Create a Java truststore using keytool (PKCS12 format):
keytool -import -trustcacerts -noprompt -alias ca -ext san=dns:localhost,ip:127.0.0.1 -file rootCA.crt -keystore truststore.jks -storetype PKCS12
This creates a truststore containing the Root CA certificate in PKCS12 format, which will be used to validate client certificates.
-
Verify the truststore (optional but recommended):
keytool -list -keystore truststore.jks -storetype PKCS12 -storepass changeit
This verifies that the Root CA is properly imported into the truststore.
After generating the certificates, ensure they are readable by Docker containers (Keycloak runs as a non-root user):
chmod 644 localhost.key localhost.crt localhost.p12 keystore.jks truststore.jks rootCA.crt rootCA.keyThen copy them to the management-node root directory:
cp keystore.jks ../keystore.jks
cp truststore.jks ../truststore.jks
cp client.crt ../client.crt
cp client.key ../client.keyThis copies the necessary files to the management-node root directory:
keystore.jks- Used by the Management Node application for its SSL server configurationtruststore.jks- Used by the Management Node to validate client certificatesclient.crtandclient.key- Used for testing API endpoints with mTLS authentication
-
For Keycloak:
- All the certificate files should now be in the
dockerdirectory - The docker-compose.yml maps these files into the Keycloak container:
volumes: - ./localhost.p12:/keystores/localhost.p12 - ./localhost.crt:/cert/localhost.crt - ./localhost.key:/key/localhost.key - ./keystore.jks:/cert/keystore.jks - ./truststore.jks:/cert/truststore.jks
- Keycloak uses these certificates for:
- Securing its HTTPS endpoint (port 8443)
- Validating client certificates for MTLS
- All the certificate files should now be in the
-
For Management Node:
- The application.yml references the certificate files:
server: ssl: key-store: /path/to/keystore.jks key-store-password: changeit trust-store: /path/to/truststore.jks trust-store-password: changeit
- When running in Docker, the Dockerfile copies these files:
COPY docker/keystore.jks /app/docker/keystore.jks COPY docker/truststore.jks /app/docker/truststore.jks
- The application.yml references the certificate files:
-
For Client Applications:
- Client applications connecting to the Management Node need:
- The client certificate and private key for authentication
- The server's certificate in their truststore to validate the server
- Client applications connecting to the Management Node need:
All certificates use the password "changeit" for development. These passwords are configured in the .env file:
SERVER_SSL_KEY_STORE_PASSWORD=changeit
SERVER_SSL_TRUST_STORE_PASSWORD=changeit
KC_HTTPS_KEY_STORE_PASSWORD=changeit
KC_HTTPS_TRUST_STORE_PASSWORD=changeit
KC_SPI_TRUSTSTORE_FILE_PASSWORD=changeit
For production environments, use strong, unique passwords and secure storage solutions for managing these credentials.
For Docker Compose to run successfully, you need to create a .env file in the docker/keycloak directory with the following settings:
POSTGRES_DB=keycloak_db
POSTGRES_USER=keycloak_db_user
POSTGRES_PASSWORD=keycloak_db_user_password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=password
KC_HOSTNAME_STRICT_BACKCHANNEL=false
SERVER_SSL_KEY_STORE_PASSWORD=changeit
SERVER_SSL_TRUST_STORE_PASSWORD=changeit
KC_HTTPS_KEY_STORE_PASSWORD=changeit
KC_HTTPS_TRUST_STORE_PASSWORD=changeit
KC_SPI_TRUSTSTORE_FILE_PASSWORD=changeit
KC_HOSTNAME=keycloak
KC_HOSTNAME_PORT=8080
KC_HTTP_ENABLED=false
KC_HOSTNAME_STRICT_HTTPS=false
KC_HEALTH_ENABLED=true
KC_DB=postgres
KC_HTTPS_CLIENT_AUTH=required
KC_HTTPS_ENABLED=true
KC_HTTPS_PORT=8443
KC_LOG_LEVEL=INFO
This file contains essential environment variables for both PostgreSQL and Keycloak configuration. You can modify these values as needed for your environment, but make sure to create this file before running Docker Compose.
The application uses Keycloak for authentication and authorization. Follow these steps to set up Keycloak using Docker Compose:
-
Navigate to the docker directory:
cd docker -
Make sure you have the required certificates in the
dockerdirectory: see lower for local certificate setupkeystore.jks- Java keystore containing the server certificatetruststore.jks- Java truststore containing trusted certificateslocalhost.p12- PKCS12 keystore for client authenticationlocalhost.crt- Certificate filelocalhost.key- Private key file
If you need to generate these files for development, see the Certificate Setup section.
-
Start Keycloak and PostgreSQL using Docker Compose:
docker compose -f keycloak/docker-compose.yml up -d
-
Verify that Keycloak is running:
curl -k https://localhost:8443/realms/master --cert client.crt --key client.key
Note: Keycloak takes about 30 seconds before its ready and the client certificate files (client.crt and client.key) must be in your current directory or provide the full path. If you haven't generated these yet, see the Certificate Setup section.
-
Access the Keycloak admin console at https://localhost:8443/admin with the following credentials:
- Username:
admin - Password:
password
You must import your
client.p12certificate into your browser before accessing the admin console, otherwise the request will be rejected by mTLS.Chrome:
- Navigate to
chrome://certificate-manager/clientcerts/platformclientcerts - Click Import and select the
client.p12file from thedocker/directory - When prompted for a password, enter
changeit
- Username:
After starting Keycloak, you need to set up a realm for the Management Node. You can either import the pre-configured realm or create it manually. To access the administrative interface at https://localhost:8443/admin.
- Log in to the Keycloak admin console at https://localhost:8443/admin
- Click on the dropdown menu in the top-left corner (it may show "master" if you haven't created any realms yet)
- Click on "Manage realms"
- Click on "Create Realm" or "Add realm" button
- Click on the "Browse" or "Select file" button
- Navigate to and select the
docker/keycloak/management-node.jsonfile from your project directory - Click "Create" or "Import"
- After the import is complete, verify that the
mng-noderealm has been created with all the necessary configurations - Note the client secret for the
management-nodeclient from the Credentials tab (Clients → management-node → Credentials) click regenerate, view it and doexport KEYCLOAK_CLIENT_SECRET=*************
If you prefer to set up the realm manually (updated for Keycloak 26.x):
-
Log in to the Keycloak admin console at https://localhost:8443/admin (you must first import your
client.p12certificate into your browser) -
Create a new realm named
mng-nodeby clicking the dropdown in the top-left and selecting "Create Realm" -
Create the management-node client:
- In the
mng-noderealm, navigate to Clients and click Create client
General Settings:
- Client type:
OpenID Connect - Client ID:
management-node - Click Next
Capability config:
- Client authentication: ON (this enables the Credentials tab)
- Authorization: OFF
- Authentication flow: Enable Service accounts roles
- Click Next
Login settings:
- Valid redirect URIs:
https://localhost:8090/* - Valid post logout redirect URIs:
+ - Web origins:
+ - Click Save
- In the
-
After saving, click on the Credentials tab to view the Client Secret. Copy this secret.
-
Add required roles to the client:
- Go to Clients → management-node → Roles tab
- Click Create role and add the following roles:
access_producer_configurations— access producer configuration endpointaccess_consumer_configurations— access consumer configuration endpointcreate_keys— generate key pairs and create CSRssign_certificate— sign CSRs via the PKI engineaccess_public_certificates— retrieve the intermediate CA certificaterequest_bootstrap_certificate— issue bootstrap certificate packages (website service account only)
See Authentication Requirements for full details on which roles map to which endpoints.
-
Assign roles to the service account:
- Go to Clients → management-node → Service accounts roles tab
- Click Assign role
- Filter by Filter by clients and select management-node
- Check the roles needed for this client:
access_producer_configurationsaccess_consumer_configurationscreate_keyssign_certificateaccess_public_certificatesrequest_bootstrap_certificate
- Click Assign
Note: For local development, assign all 6 roles if you will be testing all endpoints with the single
management-nodeclient credentials. In production, roles should be split across separate clients (e.g.request_bootstrap_certificateonly for the website/onboarding service). -
Create
src/main/resources/application-local.ymlwith all local development overrides:spring: cloud: vault: token: not_setup # replace with Vault root token if using certificate endpoints security: oauth2: resourceserver: jwt: audiences: account opaquetoken: client-secret: <your_client_secret> datasource: password: keycloak_db_user_password server: ssl: key-store-password: changeit trust-store-password: changeit application: client: key-store-password: changeit keyStoreType: PKCS12
Note: The
audiencesis set toaccountbecause Keycloak includesaccountas the default audience in service account tokens. Without this override, JWT validation will reject tokens with an audience mismatch. In production, configure a client scope audience mapper in Keycloak to use a custom audience instead.
Once Keycloak is running and configured, you can test mTLS connectivity using the command below. Replace YOUR_CLIENT_SECRET with the actual client secret obtained from the Keycloak Credentials tab (step 4 in the manual configuration above):
export KEYCLOAK_CLIENT_SECRET=`YOUR_CLIENT_SECRET`
cd docker # or where your certificates are storedcurl -k --location 'https://localhost:8443/realms/mng-node/protocol/openid-connect/token' \
--cert client.crt --key client.key \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=management-node' \
--data-urlencode "client_secret=${KEYCLOAK_CLIENT_SECRET}" \
--data-urlencode 'grant_type=client_credentials'Note: The client_secret parameter is required for confidential clients. Make sure to:
- Copy the client secret from Keycloak admin console: Clients → management-node → Credentials tab
- Replace
YOUR_CLIENT_SECRETin the command above with your actual client secret - The
-kflag is used to allow insecure connections (self-signed certificates) for development
If successful, you will receive a JSON response containing an access_token with the assigned roles in the resource_access.management-node.roles claim. This confirms that:
- ✅ mTLS authentication is working (client certificates validated)
- ✅ Client credentials are correct
- ✅ Keycloak is properly configured
- ✅ Service account has the required roles assigned
This section is only needed if you plan to use the certificate endpoints (/api/v1/certificate/*) or run the federator-certificate-manager. If you only need the configuration endpoints, skip to Building and Running with Maven.
- Create a directory called
configin thedocker/vaultdirectory
mkdir docker/vault/config- Create a file called
vault.hclin thedocker/vault/configdirectory with the following content:
ui = true
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}
storage "file" {
path = "/vault/file"
}
# Optional but helpful: disables mlock warnings in containers
disable_mlock = true
- Start vault using Docker compose:
docker compose -f docker/vault/docker-compose.yaml up -d- Verify that vault is running
docker exec vault vault status -format=json- Initialise vault & generate unseal keys and root token:
# copy the Keys and root token to somewhere safe
docker exec vault vault operator init -key-shares=1 -key-threshold=1 -format=jsonNote: A single key share is used here for convenience. In production, use multiple key shares (e.g.
-key-shares=5 -key-threshold=3) to distribute unseal keys across different operators via Shamir's secret sharing.
- Unseal vault using the unseal key from the previous step:
docker exec vault vault operator unseal <unseal_key>Note: Vault seals itself whenever the container is stopped or restarted. You will need to run the unseal command again each time you bring the container back up. The init and PKI setup steps do not need to be repeated — only the unseal.
You can then access vault using the Web UI and the root token at http://localhost:8200. Add the Vault root token to your application-local.yml:
spring:
cloud:
vault:
token: <your_vault_root_token>The Management Node uses Vault's PKI secrets engine to sign certificate requests. After initialising and unsealing Vault, you need to enable the PKI engine, import the root CA, and create a signing role.
All commands below assume you are using the root token for authentication. Replace <root_token> with the token from the init step above.
- Set the Vault address and token for the CLI:
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=<root_token>- Enable the PKI secrets engine at the
pki-intmount path. This path must match theapplication.vault.pki-mountproperty inapplication.yml(default:pki-int):
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault secrets enable -path=pki-int pki- Set the maximum TTL for the PKI engine. This controls how long certificates issued by this CA can be valid:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault secrets tune -max-lease-ttl=87600h pki-int- Import the existing root CA into Vault's PKI engine. This reuses the same root CA generated during Certificate Setup, so certificates signed by Vault are trusted by the same truststore used for mTLS:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault write pki-int/config/ca \
pem_bundle="$(openssl rsa -in docker/rootCA.key -passin pass:changeit 2>/dev/null && cat docker/rootCA.crt)"- Configure the PKI issuing certificate and CRL distribution URLs. These are embedded in certificates issued by the CA:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault write pki-int/config/urls \
issuing_certificates="http://localhost:8200/v1/pki-int/ca" \
crl_distribution_points="http://localhost:8200/v1/pki-int/crl"- Create a signing role. The role name must match
application.vault.default-roleinapplication.yml(default:default-role). This role defines what the CA is allowed to sign:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault write pki-int/roles/default-role \
allow_any_name=true \
allow_subdomains=true \
max_ttl=8760h \
key_type=any \
allow_ip_sans=true \
allow_glob_domains=true \
server_flag=true \
client_flag=true \
allowed_other_sans="1.3.6.1.4.1.32473.1.1;utf8:*"allow_any_name=truepermits signing CSRs with any common name — appropriate for development. In production, restrict this to specific domains.key_type=anyallows both RSA and EC keys.allowed_other_sanswhitelists the bootstrap OID so the bootstrap flow can embed it in signed certificates. This matches the defaultapplication.bootstrap.oidvalue — if you overrideBOOTSTRAP_OID, update this Vault role to match.
- Verify the PKI engine is working by reading back the CA certificate:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault read pki-int/cert/caYou should see the root CA certificate in PEM format.
- Test that the PKI engine can sign a certificate:
docker exec -e VAULT_TOKEN=$VAULT_TOKEN vault vault write -format=json pki-int/issue/default-role \
common_name="test.example.com" \
ttl=1h | jq -r '.data.certificate'You should see a signed PEM certificate. This confirms the Vault PKI engine is working.
- To verify the full integration with the Management Node, restart the app (with the real Vault token in
application-local.yml) and test the certificate endpoints:
# Get a token
TOKEN=$(curl -k https://localhost:8443/realms/mng-node/protocol/openid-connect/token \
--cert client.crt --key client.key \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=management-node' \
--data-urlencode "client_secret=${KEYCLOAK_CLIENT_SECRET}" \
-s | jq -r '.access_token')
# Generate a key pair via the Management Node
curl -k https://localhost:8090/api/v1/certificate/keyPair \
--cert client.crt --key client.key \
-H "Authorization: Bearer $TOKEN" | jq .If this returns 200 with a JSON response containing the key pair, the Management Node is fully connected to Vault's PKI engine.
Note: This setup imports the development root CA directly at the
pki-intmount point. For production, you would typically create a separate root CA and then generate an intermediate CA signed by it. Both configurations work with the Management Node — the only difference is whetherpki-int/cert/ca_chainreturns a chain or is empty.
The Management Node Module uses Maven for dependency management and build automation. To build the application:
-
Ensure you have Maven 3.9+ installed:
mvn --version
-
Build the application:
cd management-node # change to suit, if following along do cd ../ (from the docker folder) mvn clean package
This command will:
- Clean the target directory
- Compile the source code
- Run the tests
- Package the application into a JAR file
-
If you want to skip tests during the build:
mvn clean package -DskipTests
After building, you can run the application using one of these methods:
Note: if using application-local.yml (see Keycloak Realm Setup step 7), passwords are already configured. Otherwise, export them first:
export POSTGRES_PASSWORD=keycloak_db_user_password
export CERTPASSWORD=changeit
Ensure certificate files are in the management-node root directory (if not already there from certificate setup):
cp docker/keystore.jks keystore.jks
cp docker/truststore.jks truststore.jks
cp docker/client.crt client.crt
cp docker/client.key client.key-
Using the Java command:
java -jar target/management-node-1.0.1.jar
-
Using the Maven Spring Boot plugin (with the local profile if using
application-local.yml):mvn spring-boot:run -Dspring-boot.run.profiles=local
The application will be available at https://localhost:8090
Once you have a valid token, you can test the protected API endpoints:
Step 1: Get your Keycloak Client Secret
- Log in to Keycloak admin console at https://localhost:8443/admin
- Navigate to: mng-node realm → Clients → management-node → Credentials tab
- Copy the Client Secret value (you can regenerate if needed)
- Export it as an environment variable:
export KEYCLOAK_CLIENT_SECRET=your_actual_client_secret_hereStep 2: Get a JWT token and test the endpoints
# Navigate to the root directory where client certificates are located
cd /path/to/management-node
# First, verify you can get a token (view the full response)
curl -k https://localhost:8443/realms/mng-node/protocol/openid-connect/token \
--cert client.crt --key client.key \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=management-node' \
--data-urlencode "client_secret=${KEYCLOAK_CLIENT_SECRET}" \
-s | jq .
# Get a token and save it
TOKEN=$(curl -k https://localhost:8443/realms/mng-node/protocol/openid-connect/token \
--cert client.crt --key client.key \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=management-node' \
--data-urlencode "client_secret=${KEYCLOAK_CLIENT_SECRET}" \
-s | jq -r '.access_token')
# Verify the token was retrieved successfully
echo "Token (first 50 chars): ${TOKEN:0:50}..."
# If TOKEN is "null", check that KEYCLOAK_CLIENT_SECRET is set correctly
# Test the producer endpoint
curl -k https://localhost:8090/api/v1/configuration/producer \
--cert client.crt --key client.key \
-H "Authorization: Bearer $TOKEN" | jq .
# Test the consumer endpoint
curl -k https://localhost:8090/api/v1/configuration/consumer \
--cert client.crt --key client.key \
-H "Authorization: Bearer $TOKEN" | jq .The configuration endpoints will return a 403 Forbidden with "No organisation certificate found". This is expected — the management-node client is not mapped to a Producer or Consumer organisation in the database. At this point the setup is confirmed working:
- ✅ mTLS authentication is working (client certificates validated)
- ✅ JWT token was issued with correct roles
- ✅ Keycloak is properly configured
- ✅ The 403 is the interceptor correctly rejecting an unmapped client
Testing further with a federator client (optional):
To test the full configuration response, create a Keycloak client that matches one of the seeded organisations:
- In the Keycloak admin console, navigate to mng-node realm → Clients → Create client
- Set Client ID to
FEDERATOR_ENV(matches the sample data for Environment Agency) - Enable Client authentication and Service accounts roles
- Save, then go to the Service accounts roles tab
- Assign the
management-noderoles:access_producer_configurationsandaccess_consumer_configurations - Copy the client secret from the Credentials tab
Then test with the new client:
export FEDERATOR_ENV_SECRET=<your_federator_env_secret>
# Get a token for the FEDERATOR_ENV client
TOKEN=$(curl -k https://localhost:8443/realms/mng-node/protocol/openid-connect/token \
--cert client.crt --key client.key \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=FEDERATOR_ENV' \
--data-urlencode "client_secret=${FEDERATOR_ENV_SECRET}" \
-s | jq -r '.access_token')
# This should now return the configuration for the Environment Agency organisation
curl -k https://localhost:8090/api/v1/configuration/producer \
--cert client.crt --key client.key \
-H "Authorization: Bearer $TOKEN" | jq .Spring Boot supports profile-specific property files, which are essential for local development environments where you need to configure sensitive information like passwords and URLs without committing them to version control.
- Security: Keep sensitive information like passwords and API keys out of version control
- Environment-Specific Settings: Configure different settings for development, testing, and production
- Local Development: Each developer can have their own configuration without affecting others
For local development, create src/main/resources/application-local.yml to override secrets and passwords. See Keycloak Realm Setup step 7 for the full example.
Make sure to add this file to .gitignore:
src/main/resources/application-local.yml
Then run with the local profile:
mvn spring-boot:run -Dspring-boot.run.profiles=localOr:
java -jar target/management-node-1.0.1.jar --spring.profiles.active=localThe project uses JaCoCo for code coverage analysis. For detailed information about the JaCoCo setup, thresholds, and recommendations, see the JaCoCo Coverage Documentation.
To generate code coverage reports:
-
Run the Maven verify goal:
mvn clean verify
-
The JaCoCo report will be generated in the
target/site/jacocodirectory. -
Open
target/site/jacoco/index.htmlin a web browser to view the detailed coverage report.
The current configuration aims for 80% code coverage across instructions, branches, lines, methods, and 50% for classes.
-
Certificate Issues:
- Error:
SSL routines::sslv3 alert certificate unknown- The server doesn't trust your client certificate
- Solution: Regenerate the truststore with the current rootCA:
cd docker mv truststore.jks truststore.jks.old keytool -import -trustcacerts -noprompt -alias ca -file rootCA.crt -keystore truststore.jks -storepass changeit # Rebuild the Docker image cd .. docker build -t management-node -f docker/Dockerfile-dev .
- Ensure that the paths to the keystore and truststore files in application.yml are correct
- Verify that the certificate passwords match those in the .env file
- If certificates were regenerated, ensure the truststore contains the new rootCA
- Error:
-
Keycloak Connection Issues:
- Error:
Connection refusedwhen trying to reach Keycloak- From Docker container: Use
--network keycloak_keycloak_networkand connect tokeycloak:8443 - From host machine: Use
localhost:8443orhost.docker.internal:8443
- From Docker container: Use
- Error: Token validation fails with 401 Unauthorized
- Check that
KEYCLOAK_CLIENT_SECRETenvironment variable is set correctly - Verify the token contains required roles using:
echo $TOKEN | cut -d. -f2 | base64 -d | jq .
- Check that
- Check that Keycloak is running:
docker ps | grep keycloak - Verify that the client secret matches the one in Keycloak admin console
- Error:
-
Database Connection Issues:
- Error:
Connection to localhost:5433 refusedfrom Docker container- Docker containers can't reach
localhoston the host - Solution: Use
--network keycloak_keycloak_networkandjdbc:postgresql://keycloak-postgres-1:5432/keycloak_db - Or use
--add-host=host.docker.internal:host-gatewayandjdbc:postgresql://host.docker.internal:5433/keycloak_db
- Docker containers can't reach
- Ensure PostgreSQL is running:
docker ps | grep postgres - Check the database credentials match those in the .env file
- Verify you can connect manually:
docker exec -it keycloak-postgres-1 psql -U keycloak_db_user -d keycloak_db
- Error:
-
Docker-Specific Issues:
- Issue: Management Node can't fetch JWKs from Keycloak (SSL trust issues between containers)
- Symptom: Application starts but JWT validation fails silently
- Workaround: Run the application directly using Maven instead of Docker for local development
- Alternative: Use docker-compose to set up all services with proper SSL configuration
- Issue: Environment variables not being passed to container
- Ensure you use
-eflag for each environment variable - Verify with:
docker exec <container_id> env | grep VARIABLE_NAME
- Ensure you use
- Issue: Management Node can't fetch JWKs from Keycloak (SSL trust issues between containers)
This setup implements a zero-trust security model with:
- MTLS for all service-to-service communication
- JWT-based authentication and authorization via Keycloak
- HTTPS for all endpoints
- Client certificate authentication
For production deployments, consider:
- Using properly signed certificates from a trusted CA
- Implementing network segmentation
- Regularly rotating secrets and certificates
- Setting up monitoring and alerting for security events
The project includes interactive API documentation powered by Springdoc OpenAPI (OAS 3.1). This exposes both a human-friendly Swagger UI and machine-readable OpenAPI definitions.
How to access locally (default settings):
- Swagger UI: https://localhost:8090/swagger-ui.html
- OpenAPI JSON: https://localhost:8090/v3/api-docs
Notes
- HTTPS: The application serves over HTTPS by default (see server.ssl in application.yml). If you use development certificates, your browser may warn about trust; proceed after trusting the dev CA as described in Certificate Setup.
- Security: The security configuration explicitly permits unauthenticated access to the documentation endpoints (/v3/api-docs/, /swagger-ui/, /swagger-ui.html) while keeping all other endpoints protected via OAuth2 Resource Server (JWT). See src/main/java/.../config/SecurityConfig.java for details.
- Port/environment: If you run on a different port or behind a reverse proxy, adjust the base URL accordingly.
How Springdoc OpenAPI works in this project
-
Auto-scanning: The springdoc-openapi-starter-webmvc-ui dependency scans Spring MVC controllers at startup and automatically builds an OpenAPI 3.1 specification from your request mappings, parameters, request/response bodies, and status codes.
-
Annotations (optional but recommended):
- @Operation(summary = "...", description = "...") adds summaries, descriptions, and operation-level metadata.
- @Tag(name = "...") groups endpoints in the UI.
- @Parameter, @Schema, @ApiResponse add fine-grained control over params, models, and responses.
-
Security schema: Because this app is an OAuth2 Resource Server (JWT), you can declare a bearerAuth security scheme to document Authorization: Bearer . Example:
@io.swagger.v3.oas.annotations.security.SecurityScheme( name = "bearerAuth", type = io.swagger.v3.oas.annotations.enums.SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "JWT" )
Then add @SecurityRequirement(name = "bearerAuth") on secured controllers or operations.
-
Global metadata: You can set title, version, and contact details using @OpenAPIDefinition on a @Configuration class if desired.
All protected endpoints require JWT bearer tokens. Tokens must:
- Include the audience (aud) claim with value
account(default Keycloak audience for service accounts). - Contain a
resource_accessclaim with client-specific roles underresource_access.management-node.roles.
Required Client Roles:
access_producer_configurations— access/api/v1/configuration/produceraccess_consumer_configurations— access/api/v1/configuration/consumercreate_keys—GET /api/v1/certificate/keyPair,POST /api/v1/certificate/csr/createsign_certificate—POST /api/v1/certificate/csr/signaccess_public_certificates—GET /api/v1/certificate/intermediaterequest_bootstrap_certificate—POST /api/v1/certificate/bootstrap
Token Structure Example:
{
"aud": "account",
"resource_access": {
"management-node": {
"roles": [
"access_producer_configurations",
"access_consumer_configurations",
"sign_certificate",
"access_public_certificates"
]
}
},
"client_id": "management-node"
}These roles must be:
- Created as client roles in the Keycloak
management-nodeclient - Assigned to the appropriate service accounts
Read the full details, examples, and Keycloak mapping guidance in Authentication Requirements.
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: Developed and maintained by National Digital Twin Programme. Licensed under the Apache License 2.0.
- Documentation: Licensed under the Open Government Licence v3.0.
See
LICENSE.md,OGL_LICENCE.md, andNOTICE.mdfor 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 provides a Software Bill of Materials (SBOM) to help users and integrators understand its dependencies.
Download the latest SBOM for this codebase to view the current list of components used in this repository.
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 on ndtp@businessandtrade.gov.uk.
Maintained by the National Digital Twin Programme (NDTP). © Crown Copyright 2025. 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 entityright 2025. 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.