diff --git a/examples/.nickel/build.sh b/examples/.nickel/build.sh index 83137384cb..be15b02cf4 100755 --- a/examples/.nickel/build.sh +++ b/examples/.nickel/build.sh @@ -3,7 +3,6 @@ find . -name "*.ncl" | xargs -I _ nickel format _ nickel export ./root.ncl -f yaml --field st > ../st/compose.yaml nickel export ./root.ncl -f yaml --field st-vault > ../st-vault/compose.yaml nickel export ./root.ncl -f yaml --field st-multi > ../st-multi/compose.yaml -nickel export ./root.ncl -f yaml --field st-oidc4vc > ../st-oidc4vc/compose.yaml nickel export ./root.ncl -f yaml --field mt > ../mt/compose.yaml nickel export ./root.ncl -f yaml --field mt-keycloak > ../mt-keycloak/compose.yaml diff --git a/examples/.nickel/caddy.ncl b/examples/.nickel/caddy.ncl index ca189ad7a8..d2e17062da 100644 --- a/examples/.nickel/caddy.ncl +++ b/examples/.nickel/caddy.ncl @@ -33,6 +33,7 @@ in handle_path /vault* { reverse_proxy %{args.vault.host}:%{std.to_string args.vault.port} } + respond 404 } "% } diff --git a/examples/.shared/hurl/simple_realm/01_init_realm.hurl b/examples/.shared/hurl/simple_realm/01_init_realm.hurl index ff3b4c86c6..3742b51622 100644 --- a/examples/.shared/hurl/simple_realm/01_init_realm.hurl +++ b/examples/.shared/hurl/simple_realm/01_init_realm.hurl @@ -6,6 +6,8 @@ HTTP 200 # Admin login POST {{ keycloak_base_url }}/realms/master/protocol/openid-connect/token +[Options] +retry: 60 [FormParams] grant_type: password client_id: admin-cli diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..1bae323e7a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,60 @@ +# How to run examples + +## Prerequisites + +- docker-compose version >= `2.23.1` + +## Running examples + +Most of the examples should follow the same pattern. +Simply go to each example directory and spin up the docker-compose of each example. + +```bash +cd +docker-compose up +``` + +If some example requires a different command, it should be provided in its own local README. + +## Examples + +|example|description| +|-|-| +|`st`|single-tenant configuration without external services (except database)| +|`st-multi`|3 instances of single-tenant configuration| +|`st-vault`|single-tenant with Vault for secret storage| +|`mt`|multi-tenant configuration using built-in IAM| +|`mt-keycloak`|multi-tenant configuration using Keycloak for IAM| +|`mt-keycloak-vault`|multi-tenant configuration using Keycloak and Vault| + +# Contributing + +All of the docker-compose files in examples are generated using [Nickel](https://nickel-lang.org/). +They are defined in a shared `.nickel` directory and generated using the `build.sh` script. + +## Prerequisites + +- [Nickel](https://nickel-lang.org/) version >= `1.5` installed + +## Generate example compose files + +To generate the docker-compose config for all examples, run + +```bash +cd .nickel +./build.sh +``` + +## Updating example compose files + +To update the configuration, simply edit the `*.ncl` config in the `.nickel` directory and regenerate the docker-compose files. + +## Adding new examples + +To add a new example with docker-compose file, simply create a new configuration key in the `root.ncl` and add a new entry in the `build.sh` script. +You may need to create the target example directory if it does not already exist. + +## Example with bootstrapping script + +If any example requires initialize steps, it should be made part of the docker-compose `depends_on` construct. +Ideally, infrastructure bootstrapping should be automatic (database, IAM), but not necessarily application bootstrapping (tenant onboarding). diff --git a/examples/mt-keycloak-vault/README.md b/examples/mt-keycloak-vault/README.md new file mode 100644 index 0000000000..68af79b316 --- /dev/null +++ b/examples/mt-keycloak-vault/README.md @@ -0,0 +1,16 @@ +## Configuration + +|Exposed Service|Description| +|-|-| +|`localhost:8080/prism-agent`|Multi-tenant Cloud Agent| +|`localhost:8080/keycloak/admin`|Keycloak| +|`localhost:8200`|Vault| + +__Keycloak__ + +- Admin user `admin` +- Admin password `admin` + +__Vault__ + +- Root token `admin` diff --git a/examples/mt-keycloak-vault/compose.yaml b/examples/mt-keycloak-vault/compose.yaml index 08d5d4d8be..421ee3e533 100644 --- a/examples/mt-keycloak-vault/compose.yaml +++ b/examples/mt-keycloak-vault/compose.yaml @@ -14,6 +14,7 @@ configs: handle_path /vault* { reverse_proxy vault-default:8200 } + respond 404 } services: agent-default: diff --git a/examples/mt-keycloak/README.md b/examples/mt-keycloak/README.md new file mode 100644 index 0000000000..580d4ee5f8 --- /dev/null +++ b/examples/mt-keycloak/README.md @@ -0,0 +1,11 @@ +## Configuration + +|Exposed Service|Description| +|-|-| +|`localhost:8080/prism-agent`|Multi-tenant Cloud Agent| +|`localhost:8080/keycloak/admin`|Keycloak| + +__Keycloak__ + +- Admin user `admin` +- Admin password `admin` diff --git a/examples/mt-keycloak/compose.yaml b/examples/mt-keycloak/compose.yaml index c740be4b4d..ccaaeee5cb 100644 --- a/examples/mt-keycloak/compose.yaml +++ b/examples/mt-keycloak/compose.yaml @@ -14,6 +14,7 @@ configs: handle_path /vault* { reverse_proxy vault-default:8200 } + respond 404 } services: agent-default: diff --git a/examples/mt/README.md b/examples/mt/README.md new file mode 100644 index 0000000000..6815854b84 --- /dev/null +++ b/examples/mt/README.md @@ -0,0 +1,5 @@ +## Configuration + +|Exposed Service|Description| +|-|-| +|`localhost:8080/prism-agent`|Multi-tenant Cloud Agent| diff --git a/examples/mt/compose.yaml b/examples/mt/compose.yaml index 265d31cfa3..30bab81704 100644 --- a/examples/mt/compose.yaml +++ b/examples/mt/compose.yaml @@ -14,6 +14,7 @@ configs: handle_path /vault* { reverse_proxy vault-default:8200 } + respond 404 } services: agent-default: diff --git a/examples/st-multi/README.md b/examples/st-multi/README.md new file mode 100644 index 0000000000..9483ed9d44 --- /dev/null +++ b/examples/st-multi/README.md @@ -0,0 +1,7 @@ +## Configuration + +|Exposed Service|Description| +|-|-| +|`localhost:8080/prism-agent`|Single-tenant Cloud Agent#1 (issuer)| +|`localhost:8081/prism-agent`|Single-tenant Cloud Agent#2 (holder)| +|`localhost:8082/prism-agent`|Single-tenant Cloud Agent#3 (verifier)| diff --git a/examples/st-multi/compose.yaml b/examples/st-multi/compose.yaml index 44f0db862e..b487e7b3bf 100644 --- a/examples/st-multi/compose.yaml +++ b/examples/st-multi/compose.yaml @@ -14,6 +14,7 @@ configs: handle_path /vault* { reverse_proxy vault-holder:8200 } + respond 404 } caddyfile_issuer: content: |- @@ -30,6 +31,7 @@ configs: handle_path /vault* { reverse_proxy vault-issuer:8200 } + respond 404 } caddyfile_verifier: content: |- @@ -46,6 +48,7 @@ configs: handle_path /vault* { reverse_proxy vault-verifier:8200 } + respond 404 } services: agent-holder: diff --git a/examples/st-oidc4vc/bootstrap/01_init_realm.hurl b/examples/st-oidc4vc/bootstrap/01_init_realm.hurl deleted file mode 100644 index 78efa0fba8..0000000000 --- a/examples/st-oidc4vc/bootstrap/01_init_realm.hurl +++ /dev/null @@ -1,77 +0,0 @@ -# Wait for keycloak ready -GET {{ keycloak_base_url }}/health/ready -[Options] -retry: 300 -HTTP 200 - -# Admin login -POST {{ keycloak_base_url }}/realms/master/protocol/openid-connect/token -[FormParams] -grant_type: password -client_id: admin-cli -username: {{ keycloak_admin_user }} -password: {{ keycloak_admin_password }} -HTTP 200 -[Captures] -admin_access_token: jsonpath "$.access_token" - -# Create realm -POST {{ keycloak_base_url }}/admin/realms -authorization: Bearer {{ admin_access_token }} -{ - "realm": "{{ keycloak_realm }}", - "enabled": true -} -HTTP 201 - -# Create Alice -POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users -Authorization: Bearer {{ admin_access_token }} -{ - "username": "{{ alice_username }}", - "firstName": "Alice", - "lastName": "Wonderland", - "enabled": true, - "email": "alice@atalaprism.io", - "credentials": [{"value": "{{ alice_password }}", "temporary": false}] -} -HTTP 201 - -############################## -# TODO: actions below to be performed by controller -############################## -# Pre-register holder wallet client // TODO: dynamic registration? -POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients -Authorization: Bearer {{ admin_access_token }} -{ - "id": "{{ alice_wallet_client_id }}", - "publicClient": true, - "consentRequired": true, - "redirectUris": [ "http://localhost:5000/*" ] -} -HTTP 201 - -# Create a scope for issuable credential -POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/client-scopes -Authorization: Bearer {{ admin_access_token }} -{ - "name": "UniversityDegreeCredential", - "description": "The University Degree Credential", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "University Degree", - "display.on.consent.screen": "true", - "include.in.token.scope": "true", - "gui.order": "" - } -} -HTTP 201 -[Captures] -client_scope_id: header "Location" split "/" nth 7 - -# scope mapping -PUT {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients/{{ alice_wallet_client_id }}/optional-client-scopes/{{ client_scope_id }} -Authorization: Bearer {{ admin_access_token }} -{} -HTTP 204 - diff --git a/examples/st-oidc4vc/compose.yaml b/examples/st-oidc4vc/compose.yaml deleted file mode 100644 index c2491a6536..0000000000 --- a/examples/st-oidc4vc/compose.yaml +++ /dev/null @@ -1,149 +0,0 @@ -configs: - caddyfile_issuer: - content: |- - :8080 { - handle_path /didcomm* { - reverse_proxy agent-issuer:8090 - } - handle_path /prism-agent* { - reverse_proxy agent-issuer:8085 - } - handle_path /keycloak* { - reverse_proxy keycloak-issuer:8080 - } - handle_path /vault* { - reverse_proxy vault-issuer:8200 - } - } -services: - agent-issuer: - depends_on: - node: - condition: service_started - environment: - ADMIN_TOKEN: admin - AGENT_DB_HOST: db-issuer - AGENT_DB_NAME: agent - AGENT_DB_PASSWORD: postgres - AGENT_DB_PORT: '5432' - AGENT_DB_USER: postgres - API_KEY_ENABLED: 'false' - CONNECT_DB_HOST: db-issuer - CONNECT_DB_NAME: connect - CONNECT_DB_PASSWORD: postgres - CONNECT_DB_PORT: '5432' - CONNECT_DB_USER: postgres - DIDCOMM_SERVICE_URL: http://caddy-issuer:8080/didcomm - POLLUX_DB_HOST: db-issuer - POLLUX_DB_NAME: pollux - POLLUX_DB_PASSWORD: postgres - POLLUX_DB_PORT: '5432' - POLLUX_DB_USER: postgres - PRISM_NODE_HOST: node - PRISM_NODE_PORT: '50053' - REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent - SECRET_STORAGE_BACKEND: postgres - image: ghcr.io/input-output-hk/prism-agent:1.30.1 - restart: always - caddy-issuer: - configs: - - source: caddyfile_issuer - target: /etc/caddy/Caddyfile - image: caddy:2.7.6-alpine - ports: - - 8080:8080 - restart: always - db-issuer: - environment: - POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - healthcheck: - interval: 10s - retries: 5 - test: - - CMD - - pg_isready - - -U - - postgres - - -d - - postgres - timeout: 5s - image: postgres:13 - restart: always - volumes: - - pg_data_issuer:/var/lib/postgresql/data - - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh - - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql - external-keycloak-init-issuer: - command: - - --glob - - /hurl/*.hurl - - --test - environment: - HURL_alice_password: '1234' - HURL_alice_username: alice - HURL_alice_wallet_client_id: alice-wallet - HURL_keycloak_admin_password: admin - HURL_keycloak_admin_user: admin - HURL_keycloak_base_url: http://external-keycloak-issuer:8080 - HURL_keycloak_realm: students - image: ghcr.io/orange-opensource/hurl:4.2.0 - volumes: - - ./bootstrap:/hurl - external-keycloak-issuer: - build: ../../extensions/keycloak-oidc4vc - command: - - start-dev - - --features=preview - - --health-enabled=true - - --hostname-url=http://localhost:9980 - - --hostname-admin-url=http://localhost:9980 - environment: - IDENTUS_URL: http://caddy-issuer:8080/prism-agent - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - ports: - - 9980:8080 - restart: always - mockserver: - image: mockserver/mockserver:5.15.0 - ports: - - 5000:1080 - node: - depends_on: - node-db: - condition: service_healthy - environment: - NODE_PSQL_DATABASE: node_db - NODE_PSQL_HOST: node-db:5432 - NODE_PSQL_PASSWORD: postgres - NODE_PSQL_USERNAME: postgres - image: ghcr.io/input-output-hk/prism-node:2.2.1 - restart: always - node-db: - environment: - POSTGRES_MULTIPLE_DATABASES: node_db - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - healthcheck: - interval: 10s - retries: 5 - test: - - CMD - - pg_isready - - -U - - postgres - - -d - - postgres - timeout: 5s - image: postgres:13 - restart: always - volumes: - - pg_data_node:/var/lib/postgresql/data - - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh - - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql -version: '3' -volumes: - pg_data_issuer: {} - pg_data_node: {} diff --git a/examples/st-oidc4vc/demo.py b/examples/st-oidc4vc/demo.py deleted file mode 100644 index 344f091b77..0000000000 --- a/examples/st-oidc4vc/demo.py +++ /dev/null @@ -1,239 +0,0 @@ -import json -import requests -import threading -import time -import urllib - - -MOCKSERVER_URL = "http://localhost:5000" -LOGIN_REDIRECT_URL = "http://localhost:5000/cb" - -AGENT_URL = "http://localhost:8080/prism-agent" -CREDENTIAL_ISSUER = None -CREDENTIAL_CONFIGURATION_ID = "UniversityDegreeCredential" -AUTHORIZATION_SERVER = "http://localhost:9980/realms/students" - -ALICE_CLIENT_ID = "alice-wallet" - - -def prepare_mock_server(): - # reset mock server - requests.put(f"{MOCKSERVER_URL}/mockserver/reset") - - # mock wallet authorization callback endpoint - requests.put( - f"{MOCKSERVER_URL}/mockserver/expectation", - json={ - "httpRequest": {"path": "/cb"}, - "httpResponse": { - "statusCode": 200, - "body": {"type": "string", "string": "Login Successful"}, - }, - }, - ) - - -def prepare_issuer(): - dids = requests.get(f"{AGENT_URL}/did-registrar/dids").json()["contents"] - if len(dids) == 0: - requests.post( - f"{AGENT_URL}/did-registrar/dids", - json={ - "documentTemplate": { - "publicKeys": [{"id": "iss", "purpose": "assertionMethod"}], - "services": [], - } - }, - ) - dids = requests.get(f"{AGENT_URL}/did-registrar/dids").json()["contents"] - - issuer_did = dids[0] - while issuer_did["status"] != "PUBLISHED": - time.sleep(2) - canonical_did = issuer_did["did"] - issuer_did = requests.get( - f"{AGENT_URL}/did-registrar/dids/{canonical_did}" - ).json() - - # publish if not pending - if issuer_did["status"] == "CREATED": - requests.post(f"{AGENT_URL}/did-registrar/dids/{canonical_did}/publications") - - global CREDENTIAL_ISSUER - canonical_did = issuer_did["did"] - CREDENTIAL_ISSUER = f"{AGENT_URL}/oidc4vc/{canonical_did}" - print(f"CREDENTIAL_ISSUER: {CREDENTIAL_ISSUER}") - - -def issuer_create_credential_offer(claims): - response = requests.post( - f"{CREDENTIAL_ISSUER}/credential-offers", - json={"schemaId": "TODO", "claims": claims}, - ) - return response.json()["credentialOffer"] - - -def holder_get_issuer_metadata(credential_issuer: str): - metadata_url = f"{credential_issuer}/.well-known/openid-credential-issuer" - # TODO: OEA should return these instead of hardcoded values - return { - "credential_issuer": CREDENTIAL_ISSUER, - "authorization_servers": [AUTHORIZATION_SERVER], - "credential_endpoint": f"{CREDENTIAL_ISSUER}/credentials", - "credential_identifiers_supported": False, - "credential_configurations_supported": { - CREDENTIAL_CONFIGURATION_ID: { - "format": "jwt_vc_json", - "scope": CREDENTIAL_CONFIGURATION_ID, - "credential_signing_alg_values_supported": ["ES256K"], - "credential_definition": { - "type": ["VerifiableCredential", "UniversityDegreeCredential"], - "credentialSubject": { - "degree": {}, - "gpa": {"display": [{"name": "GPA"}]}, - }, - }, - } - }, - } - - -def holder_get_issuer_as_metadata(authorization_server: str): - metadata_url = f"{authorization_server}/.well-known/openid-configuration" - response = requests.get(metadata_url) - metadata = response.json() - # print(json.dumps(metadata, indent=2)) - return metadata - - -def holder_start_login_flow(auth_endpoint: str, token_endpoint: str, issuer_state: str): - def wait_redirect_authorization_code() -> str: - print("wating for authorization redirect ...") - while True: - response = requests.put( - f"{MOCKSERVER_URL}/mockserver/retrieve?type=REQUESTS", - json={"path": "/cb", "method": "GET"}, - ).json() - if len(response) > 0: - break - time.sleep(1) - - authorzation_code = response[0]["queryStringParameters"]["code"][0] - print(f"code: {authorzation_code}") - return authorzation_code - - def start_authorization_request(auth_endpoint: str, issuer_state: str): - # Authorization Request - queries = urllib.parse.urlencode( - { - "redirect_uri": LOGIN_REDIRECT_URL, - "response_type": "code", - "client_id": ALICE_CLIENT_ID, - "scope": "openid " + CREDENTIAL_CONFIGURATION_ID, - "issuer_state": issuer_state, - } - ) - login_url = f"{auth_endpoint}?{queries}" - print("\n##############################\n") - print("Open this link in the browser to login\n") - print(login_url) - print("\n##############################\n") - - # wait for authorization redirect - authorzation_code = wait_redirect_authorization_code() - return authorzation_code - - def start_token_request(token_endpoint: str, authorization_code: str): - # Token Request - response = requests.post( - token_endpoint, - data={ - "grant_type": "authorization_code", - "code": authorization_code, - "client_id": ALICE_CLIENT_ID, - "redirect_uri": LOGIN_REDIRECT_URL, - }, - ) - return response.json() - - authorization_code = start_authorization_request(auth_endpoint, issuer_state) - token_response = start_token_request(token_endpoint, authorization_code) - return token_response - - -def holder_extract_credential_offer(offer_uri: str): - queries = urllib.parse.urlparse(credential_offer_uri).query - credential_offer = urllib.parse.parse_qs(queries)["credential_offer"] - return json.loads(credential_offer[0]) - - -def holder_get_credential(credential_endpoint: str, token_response): - access_token = token_response["access_token"] - c_nonce = token_response["c_nonce"] - c_nonce_expires_in = token_response["c_nonce_expires_in"] - response = requests.post( - credential_endpoint, - headers={"Authorization": f"Bearer {access_token}"}, - json={ - "format": "jwt_vc_json", - "credential_definition": { - "type": ["VerifiableCredential", CREDENTIAL_CONFIGURATION_ID], - "credentialSubject": {}, - }, - "proof": { - "proof_type": "jwt", - "jwt": f"jwt:{c_nonce}", # TODO: use actual JWT - }, - }, - ) - return response.json() - - -if __name__ == "__main__": - prepare_mock_server() - prepare_issuer() - - # step 1: Issuer create CredentialOffer - credential_offer_uri = issuer_create_credential_offer( - {"degree": "ChemicalEngineering", "gpa": "3.00"} - ) - - # step 2: Issuer present QR code container CredentialOffer URI - credential_offer = holder_extract_credential_offer(credential_offer_uri) - credential_offer_pretty = json.dumps(credential_offer, indent=2) - issuer_state = credential_offer["grants"]["authorization_code"]["issuer_state"] - print("\n##############################\n") - print(f"QR code scanned, got credential-offer\n\n{credential_offer_uri}\n") - print(f"\n{credential_offer_pretty}\n") - print("\n##############################\n") - input("\nEnter to continue ...") - - # step 3: Holdler retreive Issuer's metadata - issuer_metadata = holder_get_issuer_metadata(CREDENTIAL_ISSUER) - authorzation_server = issuer_metadata["authorization_servers"][0] - print("\n::::: Issuer Metadata :::::") - print(json.dumps(issuer_metadata, indent=2)) - input("\nEnter to continue ...") - - # step 3.1: Holder retreive Issuer's AS metadata - issuer_as_metadata = holder_get_issuer_as_metadata(authorzation_server) - issuer_as_token_endpoint = issuer_as_metadata["token_endpoint"] - issuer_as_authorization_endpoint = issuer_as_metadata["authorization_endpoint"] - print("\n::::: Issuer Authorization Server Metadata :::::") - print(f"issuer_as_auth_endpoint: {issuer_as_authorization_endpoint}") - print(f"issuer_as_token_endpoint: {issuer_as_token_endpoint}") - input("\nEnter to continue ...") - - # step 4: Holder start authorization flow - token_response = holder_start_login_flow( - issuer_as_authorization_endpoint, issuer_as_token_endpoint, issuer_state - ) - print("::::: Token Response :::::") - print(json.dumps(token_response, indent=2)) - input("\nEnter to continue ...") - - # step 5: Holder use access_token to get credential - credential_endpoint = issuer_metadata["credential_endpoint"] - jwt_credential = holder_get_credential(credential_endpoint, token_response) - print("\n::::: Credential Received :::::") - print(json.dumps(jwt_credential, indent=2)) diff --git a/examples/st-vault/README.md b/examples/st-vault/README.md new file mode 100644 index 0000000000..eb9997e364 --- /dev/null +++ b/examples/st-vault/README.md @@ -0,0 +1,10 @@ +## Configuration + +|Exposed Service|Description| +|-|-| +|`localhost:8080/prism-agent`|Single-tenant Cloud Agent| +|`localhost:8200`|Vault| + +__Vault__ + +- Root token `admin` diff --git a/examples/st-vault/compose.yaml b/examples/st-vault/compose.yaml index 08f3446ef8..d39b354cce 100644 --- a/examples/st-vault/compose.yaml +++ b/examples/st-vault/compose.yaml @@ -14,6 +14,7 @@ configs: handle_path /vault* { reverse_proxy vault-issuer:8200 } + respond 404 } services: agent-issuer: diff --git a/examples/st/README.md b/examples/st/README.md new file mode 100644 index 0000000000..bfdcfcd702 --- /dev/null +++ b/examples/st/README.md @@ -0,0 +1,5 @@ +## Configuration + +|Exposed Service|Description| +|-|-| +|`localhost:8080/prism-agent`|Single-tenant Cloud Agent| diff --git a/examples/st/compose.yaml b/examples/st/compose.yaml index 8ab5684a9a..a930053597 100644 --- a/examples/st/compose.yaml +++ b/examples/st/compose.yaml @@ -14,6 +14,7 @@ configs: handle_path /vault* { reverse_proxy vault-issuer:8200 } + respond 404 } services: agent-issuer: