diff --git a/.gitignore b/.gitignore index 1ad3f4ea..d371ef1e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ tests/mock_data tests/testing_results tests/new_testing_results version.json +example-setup-with-hapi/certificates/* \ No newline at end of file diff --git a/README.md b/README.md index 7164efa9..c99edfed 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Follow the instructions to get the app running: ```bash cd example-setup-with-hapi -docker compose up +./setup-demo.sh ``` This will configure the whole system for you and you should be able to use the @@ -73,7 +73,7 @@ In a terminal in the same `example-setup-with-hapi` folder run the following com with a url parameter you can specify the base url of the directory you want to seed: ```bash -docker compose run --rm mcsd-update-client poetry run seed http://hapi-directory:8080/fhir/ +docker compose exec mcsd-update-client python seeds/seed_hapi.py https://hapi-directory:8443/fhir/ true ``` ## Docker container builds diff --git a/example-setup-with-hapi/app.conf b/example-setup-with-hapi/app.conf index 5da4a275..94c54791 100644 --- a/example-setup-with-hapi/app.conf +++ b/example-setup-with-hapi/app.conf @@ -61,11 +61,11 @@ ssl_cert_file = server.cert ssl_key_file = server.key [mcsd] -update_client_url = http://hapi-update-client:8081/fhir +update_client_url = https://hapi-update-client:8443/fhir authentication = off -mtls_client_cert_path = -mtls_client_key_path = -mtls_server_ca_path_path = +mtls_client_cert_path = /certificates/client.crt +mtls_client_key_path = /certificates/client.key +mtls_server_ca_path = /certificates/ca.crt request_count = 20 fill_required_fields = False diff --git a/example-setup-with-hapi/client.application.yaml b/example-setup-with-hapi/client.application.yaml index 66f38eee..598824ab 100644 --- a/example-setup-with-hapi/client.application.yaml +++ b/example-setup-with-hapi/client.application.yaml @@ -1,5 +1,15 @@ server: - port: 8081 + port: 8443 + ssl: + enabled: true + protocol: TLS + key-store: /certificates/server-keystore.p12 + key-store-password: secret + key-store-type: PKCS12 + client-auth: need + trust-store: /certificates/truststore.p12 + trust-store-password: secret + trust-store-type: PKCS12 spring: datasource: @@ -10,7 +20,7 @@ spring: jpa: properties: hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect - hibernate.search.enabled: false + hibernate.search.enabled: true hapi: fhir: @@ -18,7 +28,7 @@ hapi: home: id: home name: hapi2 Tester - server_address: 'http://localhost:8081/fhir' + server_address: 'https://localhost:8443/fhir' refuse_to_fetch_third_party_urls: false fhir_version: R4 allow_multiple_delete: true diff --git a/example-setup-with-hapi/directory.application.yaml b/example-setup-with-hapi/directory.application.yaml index 414064e7..87d061df 100644 --- a/example-setup-with-hapi/directory.application.yaml +++ b/example-setup-with-hapi/directory.application.yaml @@ -1,5 +1,15 @@ server: - port: 8080 + port: 8443 + ssl: + enabled: true + protocol: TLS + key-store: /certificates/directory-server-keystore.p12 + key-store-password: secret + key-store-type: PKCS12 + trust-store: /certificates/truststore.p12 + trust-store-password: secret + trust-store-type: PKCS12 + client-auth: need spring: datasource: @@ -10,7 +20,7 @@ spring: jpa: properties: hibernate.dialect: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect - hibernate.search.enabled: false + hibernate.search.enabled: true hapi: fhir: @@ -18,7 +28,7 @@ hapi: home: id: home name: hapi2 Tester - server_address: 'http://localhost:8080/fhir' + server_address: 'https://localhost:8444/fhir' refuse_to_fetch_third_party_urls: false fhir_version: R4 allow_multiple_delete: true diff --git a/example-setup-with-hapi/directory_urls.json b/example-setup-with-hapi/directory_urls.json index a1daa456..1222a58d 100644 --- a/example-setup-with-hapi/directory_urls.json +++ b/example-setup-with-hapi/directory_urls.json @@ -3,7 +3,7 @@ { "id": "hapi-directory", "name": "HAPI directory", - "endpoint": "http://hapi-directory:8080/fhir" + "endpoint": "https://hapi-directory:8443/fhir" } ] } \ No newline at end of file diff --git a/example-setup-with-hapi/docker-compose.yaml b/example-setup-with-hapi/docker-compose.yaml index a82c4153..9de7fd25 100644 --- a/example-setup-with-hapi/docker-compose.yaml +++ b/example-setup-with-hapi/docker-compose.yaml @@ -22,6 +22,7 @@ services: - ../pyproject.toml:/src/pyproject.toml - ../Makefile:/src/Makefile - ../README.md:/src/README.md + - ./certificates:/certificates:ro configs: - source: mcsd_update_client_conf target: /src/app.conf @@ -45,26 +46,31 @@ services: hapi-update-client: image: "hapiproject/hapi:latest" ports: - - "8081:8081" + - "8443:8443" + volumes: + - ./certificates:/certificates:ro configs: - source: hapi_update_client_config target: /app/config/application.yaml depends_on: - - postgres + postgres: + condition: service_healthy networks: - mcsd_update_client_net hapi-directory: image: "hapiproject/hapi:latest" ports: - - "8080:8080" + - "8444:8443" + volumes: + - ./certificates:/certificates:ro configs: - source: hapi_directory_config target: /app/config/application.yaml depends_on: - postgres healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] + test: ["CMD", "curl", "-k", "--cert", "/certificates/client.crt", "--key", "/certificates/client.key", "--cacert", "/certificates/ca.crt", "-f", "https://localhost:8443/actuator/health"] interval: 30s timeout: 10s retries: 3 @@ -79,12 +85,19 @@ services: "CMD", "curl", "-f", - "http://hapi-update-client:8081/actuator/health", + "-k", + "--cert", "/certificates/client.crt", + "--key", "/certificates/client.key", + "--cacert", "/certificates/ca.crt", + "https://hapi-update-client:8443/actuator/health", ] start_period: 100s start_interval: 5s + volumes: + - ./certificates:/certificates:ro depends_on: - - hapi-update-client + hapi-update-client: + condition: service_started stop_signal: SIGKILL networks: - mcsd_update_client_net @@ -97,11 +110,20 @@ services: [ "CMD", "curl", + "-k", + "--cert", + "/certificates/client.crt", + "--key", + "/certificates/client.key", + "--cacert", + "/certificates/ca.crt", "-f", - "http://hapi-directory:8080/actuator/health", + "https://hapi-directory:8443/actuator/health", ] start_period: 100s start_interval: 5s + volumes: + - ./certificates:/certificates:ro depends_on: - hapi-directory stop_signal: SIGKILL diff --git a/example-setup-with-hapi/generate-certs.sh b/example-setup-with-hapi/generate-certs.sh new file mode 100755 index 00000000..8cd62d5b --- /dev/null +++ b/example-setup-with-hapi/generate-certs.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +# Generate certificates for mTLS demo +# This script creates a CA, server certificates, and client certificates + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CERT_DIR="$SCRIPT_DIR/certificates" + +mkdir -p "$CERT_DIR" +cd "$CERT_DIR" + +echo "Generating certificates in $CERT_DIR" + +rm -f *.pem *.key *.crt *.csr *.srl *.ext + +# 1. Generate CA private key +echo "1. Generating CA private key..." +openssl genrsa -out ca.key 4096 + +# 2. Generate CA certificate +echo "2. Generating CA certificate..." +openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/C=NL/ST=Netherlands/L=Demo/O=MCSD-Demo/OU=Demo-CA/CN=Demo-CA" + +# 3. Generate update-client server private key +echo "3. Generating update-client server private key..." +openssl genrsa -out server.key 4096 + +# 4. Generate update-client server certificate signing request with SAN +echo "4. Generating update-client server CSR with SAN..." +cat > server.ext << EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = hapi-update-client +DNS.2 = localhost +IP.1 = 127.0.0.1 +EOF + +openssl req -new -key server.key -out server.csr -subj "/C=NL/ST=Netherlands/L=Demo/O=MCSD-Demo/OU=Demo-Server/CN=hapi-update-client" + +# 4c. Generate server certificate signed by CA with SAN +echo "4c. Generating server certificate with SAN..." +openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extfile server.ext + +# 5a. Generate directory server private key +echo "5a. Generating directory server private key..." +openssl genrsa -out directory-server.key 4096 + +# 5b. Generate directory server certificate signing request with SAN +echo "5b. Generating directory server CSR with SAN..." +cat > directory-server.ext << EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = hapi-directory +DNS.2 = localhost +IP.1 = 127.0.0.1 +EOF + +openssl req -new -key directory-server.key -out directory-server.csr -subj "/C=NL/ST=Netherlands/L=Demo/O=MCSD-Demo/OU=Demo-Server/CN=hapi-directory" + +# 5c. Generate directory server certificate signed by CA with SAN +echo "5c. Generating directory server certificate with SAN..." +openssl x509 -req -days 365 -in directory-server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out directory-server.crt -extfile directory-server.ext + +# 6. Generate client private key +echo "6. Generating client private key..." +openssl genrsa -out client.key 4096 + +# 7. Generate client certificate signing request with SAN +echo "7. Generating client CSR with SAN..." +cat > client.ext << EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = mcsd-update-client +DNS.2 = localhost +IP.1 = 127.0.0.1 +EOF + +openssl req -new -key client.key -out client.csr -subj "/C=NL/ST=Netherlands/L=Demo/O=MCSD-Demo/OU=Demo-Client/CN=mcsd-update-client" + +# 8. Generate client certificate signed by CA with SAN +echo "8. Generating client certificate with SAN..." +openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -extfile client.ext + +rm -f *.csr *.ext + +chmod 644 *.crt *.key + +echo "Certificate generation complete!" diff --git a/example-setup-with-hapi/generate-keystores.sh b/example-setup-with-hapi/generate-keystores.sh new file mode 100755 index 00000000..ff93d043 --- /dev/null +++ b/example-setup-with-hapi/generate-keystores.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Generate Java keystores from OpenSSL certificates +# This script converts the certificates to Java keystores for HAPI FHIR + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CERT_DIR="$SCRIPT_DIR/certificates" + +cd "$CERT_DIR" + +KEYSTORE_PASSWORD="secret" + +echo "Generating Java keystores from certificates in $CERT_DIR" + +if [ ! -f ca.crt ] || [ ! -f server.crt ] || [ ! -f server.key ] || [ ! -f directory-server.crt ] || [ ! -f directory-server.key ] || [ ! -f client.crt ] || [ ! -f client.key ]; then + echo "Error: Required certificate files not found. Run generate-certs.sh first." + exit 1 +fi + +rm -f *.p12 + +# 1. Create server keystore (PKCS12) with server certificate and private key +echo "1. Creating update-client server keystore..." +openssl pkcs12 -export -in server.crt -inkey server.key -out server-keystore.p12 -name "hapi-server" -passout pass:$KEYSTORE_PASSWORD + +# 1a. Create directory server keystore (PKCS12) with directory server certificate and private key +echo "1a. Creating directory server keystore..." +openssl pkcs12 -export -in directory-server.crt -inkey directory-server.key -out directory-server-keystore.p12 -name "hapi-directory-server" -passout pass:$KEYSTORE_PASSWORD + +# 2. Create truststore with CA certificate +echo "2. Creating truststore with CA certificate..." +keytool -import -trustcacerts -noprompt -alias ca -file ca.crt -keystore truststore.p12 -storetype PKCS12 -storepass $KEYSTORE_PASSWORD + +# 3. Create client keystore (for client applications if needed) +echo "3. Creating client keystore..." +openssl pkcs12 -export -in client.crt -inkey client.key -out client-keystore.p12 -name "mcsd-client" -passout pass:$KEYSTORE_PASSWORD + +chmod 644 *.p12 *.crt *.key + +echo "Java keystore generation complete!" +echo "All keystores use password: $KEYSTORE_PASSWORD" diff --git a/example-setup-with-hapi/setup-demo.sh b/example-setup-with-hapi/setup-demo.sh new file mode 100755 index 00000000..ae40732e --- /dev/null +++ b/example-setup-with-hapi/setup-demo.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Setup script for mTLS demo +# This script generates certificates and starts the demo environment + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "Setting up mTLS demo environment..." + +chmod +x generate-certs.sh +chmod +x generate-keystores.sh + +if [ ! -f certificates/ca.crt ]; then + echo "Generating TLS certificates..." + ./generate-certs.sh + ./generate-keystores.sh +else + echo "TLS certificates already exist, skipping generation..." +fi + +echo "" +echo "Starting Docker Compose environment with mTLS..." +docker-compose up -d + +echo "" +echo "mTLS demo setup complete!" +echo "" diff --git a/seeds/seed_hapi.py b/seeds/seed_hapi.py index f4d193a8..30dd5af1 100644 --- a/seeds/seed_hapi.py +++ b/seeds/seed_hapi.py @@ -2,21 +2,42 @@ from fastapi.encoders import jsonable_encoder from seeds.generate_data import DataGenerator import sys +import os generator = DataGenerator() def generate_data(): - if len(sys.argv) != 2: - raise ValueError("Usage: python seed_hapi.py \n For example: python seed_hapi.py http://example.com/fhir/") + if len(sys.argv) < 2: + raise ValueError("Usage: python seed_hapi.py [use_mtls] \n For example: python seed_hapi.py https://hapi-directory:8443/fhir/ true") _DIRECTORY_URL = sys.argv[1] + use_mtls = len(sys.argv) > 2 and sys.argv[2].lower() == "true" + + # Setup mTLS configuration if requested + session = requests.Session() + if use_mtls: + cert_file = "/certificates/client.crt" + key_file = "/certificates/client.key" + ca_file = "/certificates/ca.crt" + + # Check if certificate files exist + if not all(os.path.exists(f) for f in [cert_file, key_file, ca_file]): + print("Warning: Certificate files not found, falling back to no SSL verification") + session.verify = False + else: + session.cert = (cert_file, key_file) + session.verify = ca_file + print(f"Using mTLS with certificates: {cert_file}, {key_file}, {ca_file}") + + print(f"Seeding directory at: {_DIRECTORY_URL}") + print(f"Using mTLS: {use_mtls}") for x in range(10): print(f"Generating data set {x+1}") endpoint = generator.generate_endpoint() print("Generated endpoint") - endpoint_response = requests.post( + endpoint_response = session.post( url=_DIRECTORY_URL+"Endpoint", json=jsonable_encoder(endpoint.model_dump()), headers={"Content-Type": "application/json"}, @@ -27,7 +48,7 @@ def generate_data(): org = generator.generate_organization(endpoint_ids=[endpoint_id]) print("Generated organization") - org_response = requests.post( + org_response = session.post( url=_DIRECTORY_URL+"Organization", json=jsonable_encoder(org.model_dump()), headers={"Content-Type": "application/json"}, @@ -41,7 +62,7 @@ def generate_data(): part_of=org_id, ) print("Generated child organization") - child_response = requests.post( + child_response = session.post( url=_DIRECTORY_URL+"Organization", json=jsonable_encoder(child_org.model_dump()), headers={"Content-Type": "application/json"}, @@ -55,7 +76,7 @@ def generate_data(): endpoint_ids=[endpoint_id], ) print("Generated location") - loc_response = requests.post( + loc_response = session.post( url=_DIRECTORY_URL+"Location", json=jsonable_encoder(loc.model_dump()), headers={"Content-Type": "application/json"}, @@ -70,7 +91,7 @@ def generate_data(): part_of_location=loc_id, ) print("Generated child location") - child_loc_resp = requests.post( + child_loc_resp = session.post( url=_DIRECTORY_URL+"Location", json=jsonable_encoder(child_loc.model_dump()), headers={"Content-Type": "application/json"}, @@ -86,7 +107,7 @@ def generate_data(): endpoint_ids=[endpoint_id], ) print("Generated healthcare service") - health_serv_response = requests.post( + health_serv_response = session.post( url=_DIRECTORY_URL+"HealthcareService", json=jsonable_encoder(health_serv.model_dump()), headers={"Content-Type": "application/json"}, @@ -99,7 +120,7 @@ def generate_data(): qualification_issuer_org_ids=[org_id], ) print("Generated practitioner") - practitioner_response = requests.post( + practitioner_response = session.post( url=_DIRECTORY_URL+"Practitioner", json=jsonable_encoder(practitioner.model_dump()), headers={"Content-Type": "application/json"}, @@ -116,7 +137,7 @@ def generate_data(): endpoint_ids=[endpoint_id], ) print("Generated practitioner role") - practitioner_role_response = requests.post( + practitioner_role_response = session.post( url=_DIRECTORY_URL+"PractitionerRole", json=jsonable_encoder(practitioner_role.model_dump()), headers={"Content-Type": "application/json"}, @@ -128,7 +149,7 @@ def generate_data(): # Setup some affiliations between multiple organizations org2 = generator.generate_organization(endpoint_ids=[endpoint_id]) print("Generated second organization") - org2_response = requests.post( + org2_response = session.post( url=_DIRECTORY_URL+"Organization", json=jsonable_encoder(org2.model_dump()), headers={"Content-Type": "application/json"}, @@ -146,7 +167,7 @@ def generate_data(): endpoint_ids=[endpoint_id], ) print("Generated organization affiliation") - org_afil_response = requests.post( + org_afil_response = session.post( url=_DIRECTORY_URL+"OrganizationAffiliation", json=jsonable_encoder(org_afil.model_dump()), headers={"Content-Type": "application/json"},