diff --git a/charts/keycloak/templates/keycloak-users-external-secret.yaml b/charts/keycloak/templates/keycloak-users-external-secret.yaml index f9f2a0ba..ee2bf910 100644 --- a/charts/keycloak/templates/keycloak-users-external-secret.yaml +++ b/charts/keycloak/templates/keycloak-users-external-secret.yaml @@ -21,18 +21,18 @@ spec: data: - secretKey: qtodo_admin_password remoteRef: - key: secret/data/global/keycloak-users + key: {{ .Values.keycloak.users.passwordVaultKey }} property: qtodo-admin-password - secretKey: qtodo_user1_password remoteRef: - key: secret/data/global/keycloak-users + key: {{ .Values.keycloak.users.passwordVaultKey }} property: qtodo-user1-password - secretKey: rhtas_user_password remoteRef: - key: secret/data/global/keycloak-users + key: {{ .Values.keycloak.users.passwordVaultKey }} property: rhtas-user-password - secretKey: rhtpa_user_password remoteRef: - key: secret/data/global/keycloak-users + key: {{ .Values.keycloak.users.passwordVaultKey }} property: rhtpa-user-password {{- end }} diff --git a/charts/keycloak/templates/oidc-client-secret-external-secret.yaml b/charts/keycloak/templates/oidc-client-secret-external-secret.yaml index d31c3547..5f5a5ad6 100644 --- a/charts/keycloak/templates/oidc-client-secret-external-secret.yaml +++ b/charts/keycloak/templates/oidc-client-secret-external-secret.yaml @@ -18,6 +18,6 @@ spec: data: - secretKey: client_secret remoteRef: - key: secret/data/global/oidc-client-secret + key: {{ .Values.keycloak.oidcSecrets.qtodo.vaultPath }} property: client-secret {{- end }} diff --git a/charts/keycloak/templates/rhtpa-oidc-cli-secret-external-secret.yaml b/charts/keycloak/templates/rhtpa-oidc-cli-secret-external-secret.yaml index 10975c5a..db08e155 100644 --- a/charts/keycloak/templates/rhtpa-oidc-cli-secret-external-secret.yaml +++ b/charts/keycloak/templates/rhtpa-oidc-cli-secret-external-secret.yaml @@ -19,7 +19,7 @@ spec: data: - secretKey: client_secret remoteRef: - key: secret/data/global/rhtpa-oidc-cli-secret + key: {{ .Values.keycloak.oidcSecrets.rhtpaCli.vaultPath }} property: client-secret {{- end }} diff --git a/charts/keycloak/values.yaml b/charts/keycloak/values.yaml index 057562bd..2d78dda8 100644 --- a/charts/keycloak/values.yaml +++ b/charts/keycloak/values.yaml @@ -7,7 +7,8 @@ keycloak: adminUser: enabled: true username: admin - passwordVaultKey: secret/data/global/keycloak + # Keycloak admin password (infra) + passwordVaultKey: secret/data/hub/infra/keycloak/keycloak secretName: keycloak-admin-user defaultConfig: true defaultRealm: @@ -349,7 +350,8 @@ keycloak: name: keycloak postgresqlDb: database: keycloak - passwordVaultKey: secret/data/global/keycloak + # Keycloak DB password path (infra) + passwordVaultKey: secret/data/hub/infra/keycloak/keycloak secretName: postgresql-db username: keycloak realms: [] @@ -357,5 +359,14 @@ keycloak: secret: keycloak-tls serviceServing: true users: - passwordVaultKey: secret/data/global/keycloak-users + # User credentials path (infra) + passwordVaultKey: secret/data/hub/infra/users/keycloak-users secretName: keycloak-users + # OIDC client secrets for realm configuration + oidcSecrets: + # QTodo OIDC client secret (app-level) + qtodo: + vaultPath: secret/data/apps/qtodo/qtodo-oidc-client + # RHTPA CLI OIDC client secret (infra) + rhtpaCli: + vaultPath: secret/data/hub/infra/rhtpa/rhtpa-oidc-cli \ No newline at end of file diff --git a/charts/qtodo/values.yaml b/charts/qtodo/values.yaml index c48b3954..d5fd70c8 100644 --- a/charts/qtodo/values.yaml +++ b/charts/qtodo/values.yaml @@ -19,8 +19,9 @@ app: secretName: qtodo-registry-auth user: quay-user # domain: quay-registry-quay-quay-enterprise.apps.example.com - vaultPath: secret/data/global/quay-users - passwordVaultKey: password + # Registry credentials - stored in quay path + vaultPath: secret/data/hub/infra/quay/quay-users + passwordVaultKey: quay-user-password spiffeHelper: name: registry.redhat.io/zero-trust-workload-identity-manager/spiffe-helper-rhel9 version: v0.10.0 @@ -53,21 +54,26 @@ app: - qtodo # Vault configuration for SPIFFE integration + # Uses SPIFFE JWT to authenticate and fetch DB password vault: url: "" - role: "qtodo-role" - secretPath: "secret/data/global/keycloak-users" + role: "qtodo" + # QTodo secrets path (app-level isolation) + secretPath: "secret/data/apps/qtodo/qtodo-db" # OIDC External Secret configuration oidcSecret: enabled: true name: "oidc-client-secret" - vaultPath: "secret/data/global/oidc-client-secret" - - # Truststore configuration for Java CA certificates + # QTodo OIDC secret path (app-level isolation) + vaultPath: "secret/data/apps/qtodo/qtodo-oidc-client" + + # Truststore configuration for Java CA certificates (PKCS12 format) truststore: enabled: true - vaultPath: "secret/data/global/qtodo-truststore" + secretName: "qtodo-truststore-secret" + # QTodo truststore password path (app-level isolation) + vaultPath: "secret/data/apps/qtodo/qtodo-truststore" # PostgreSQL database configuration postgresql: @@ -78,4 +84,5 @@ postgresql: secretName: qtodo-db-secret database: tasks username: qtodo_user - passwordVaultKey: secret/data/global/qtodo + # QTodo DB password path (app-level isolation) + passwordVaultKey: secret/data/apps/qtodo/qtodo-db diff --git a/charts/rhtpa-operator/templates/oidc-cli-secret.yaml b/charts/rhtpa-operator/templates/oidc-cli-secret.yaml index d32ec1cf..5f1255b0 100644 --- a/charts/rhtpa-operator/templates/oidc-cli-secret.yaml +++ b/charts/rhtpa-operator/templates/oidc-cli-secret.yaml @@ -20,7 +20,7 @@ spec: data: - secretKey: client-secret remoteRef: - key: secret/data/global/rhtpa-oidc-cli-secret + key: {{ .Values.rhtpa.zeroTrust.keycloak.clients.cli.secretVaultPath }} property: client-secret {{- end }} diff --git a/charts/rhtpa-operator/values.yaml b/charts/rhtpa-operator/values.yaml index 7277f93d..5613f7e6 100644 --- a/charts/rhtpa-operator/values.yaml +++ b/charts/rhtpa-operator/values.yaml @@ -24,15 +24,18 @@ rhtpa: enabled: true url: "https://vault.vault.svc.cluster.local:8200" role: "rhtpa" - policy: "global-secret" - secretPath: "secret/data/global/rhtpa" + # Application-specific policy (least privilege) + policy: "rhtpa-secrets" + # RHTPA DB password path (infra) + secretPath: "secret/data/hub/infra/rhtpa/rhtpa-db" keycloak: enabled: true url: "" # Constructed dynamically from global.localClusterDomain in templates realm: "ztvp" namespace: "keycloak-system" # Namespace where Keycloak is deployed instanceName: "keycloak" # Name of the Keycloak CR - userPasswordVaultKey: "secret/data/global/keycloak-users" # Vault path for RHTPA user password + # User credentials - stored in infra users path + userPasswordVaultKey: "secret/data/hub/infra/users/keycloak-users" # OIDC Client Configuration clients: frontend: @@ -40,6 +43,8 @@ rhtpa: cli: clientId: "rhtpa-cli" # Confidential client for RHTPA CLI/API secretName: "rhtpa-oidc-cli-secret" # Kubernetes secret containing client secret + # RHTPA OIDC CLI secret path (infra) + secretVaultPath: "secret/data/hub/infra/rhtpa/rhtpa-oidc-cli" # TLS Configuration tls: @@ -128,7 +133,8 @@ rhtpa: name: "rhtpa" username: "rhtpa" secretName: "rhtpa-db-secret" - passwordVaultKey: "secret/data/global/rhtpa" + # RHTPA DB password path (infra) + passwordVaultKey: "secret/data/hub/infra/rhtpa/rhtpa-db" storageSize: "10Gi" image: "registry.redhat.io/rhel8/postgresql-16" diff --git a/charts/supply-chain/values.yaml b/charts/supply-chain/values.yaml index 58ac7abc..4a54d048 100644 --- a/charts/supply-chain/values.yaml +++ b/charts/supply-chain/values.yaml @@ -14,7 +14,8 @@ rhtpa: oidcRealm: "ztvp" clientId: "rhtpa-cli" clientSecretName: "qtodo-rhtpa-cli-password" - clientSecretVaultPath: "secret/data/global/rhtpa-oidc-cli-secret" + # RHTPA OIDC CLI secret path (infra) + clientSecretVaultPath: "secret/data/hub/infra/rhtpa/rhtpa-oidc-cli" clientSecretVaultKey: "client-secret" # qtodo repository configuration @@ -43,7 +44,8 @@ registry: tlsVerify: "true" user: "quay-user" passwordVaultKey: "quay-user-password" - vaultPath: "secret/data/global/quay-users" + # Infrastructure secrets - stored in quay path + vaultPath: "secret/data/hub/infra/quay/quay-users" # spire configuration spire: diff --git a/common/scripts/vault-utils.sh b/common/scripts/vault-utils.sh index 0cd33f98..42ef0c31 100755 --- a/common/scripts/vault-utils.sh +++ b/common/scripts/vault-utils.sh @@ -28,10 +28,15 @@ fi EXTRA_PLAYBOOK_OPTS="${EXTRA_PLAYBOOK_OPTS:-}" +EXTRA_VARS_FILE=$(mktemp) +trap "rm -f ${EXTRA_VARS_FILE}" EXIT + if [ "$(yq ".clusterGroup.applications.vault.jwt.enabled // \"false\"" "${MAIN_CLUSTERGROUP_FILE}")" == "true" ]; then OCP_DOMAIN="$(oc get dns cluster -o jsonpath='{.spec.baseDomain}')" OIDC_DISCOVERY_URL="$(yq ".clusterGroup.applications.vault.jwt.oidcDiscoveryUrl" "${MAIN_CLUSTERGROUP_FILE}" | sed "s/{{ \$.Values.global.clusterDomain }}/${OCP_DOMAIN}/g")" - JWT_ROLES="$(yq -o json ".clusterGroup.applications.vault.jwt.roles" "${MAIN_CLUSTERGROUP_FILE}" | jq -rc | sed "s/{{ \$.Values.global.clusterDomain }}/${OCP_DOMAIN}/g")" + JWT_ROLES="$(yq -o json ".clusterGroup.applications.vault.jwt.roles" "${MAIN_CLUSTERGROUP_FILE}" | sed "s/{{ \$.Values.global.clusterDomain }}/${OCP_DOMAIN}/g")" + # Extract JWT policies (policies ending in -jwt-secret) + JWT_POLICIES="$(yq -o json ".clusterGroup.applications.vault.policies" "${MAIN_CLUSTERGROUP_FILE}" | jq '[.[] | select(.name | test("-jwt-secret$"))]')" if [ "${OIDC_DISCOVERY_URL}" == "null" ] || [ "${JWT_ROLES}" == "null" ] || [ "${JWT_ROLES}" == "[]" ]; then echo "Vault JWT config is disabled because of missing required fields" @@ -46,13 +51,21 @@ if [ "$(yq ".clusterGroup.applications.vault.jwt.enabled // \"false\"" "${MAIN_C else VAULT_JWT_CONFIG="false" + JWT_ROLES="[]" + JWT_POLICIES="[]" echo "Vault JWT config is disabled" fi +# Write extra vars to temp file to handle complex JSON with embedded quotes +cat > "${EXTRA_VARS_FILE}" < **Routes** from the lefthand navigation bar 2. Click the arrow next to the the URL underneath the _Location_ column to open the qtodo application in a new browser tab -You will be presented with a login page to access the application. When using the default External Identity Provider (RHBK), two users (`qtodo-admin` and `qtodo-user`) were provisioned automatically. Their initial credentials are stored in a Secret in the `keycloak-system` namespace called `keycloak-users`. You can reveal the credentials by switching to the tab containing the OpenShift Console using the following steps: +You will be presented with a login page to access the application. When using the default External Identity Provider (RHBK), two users (`qtodo-admin` and `qtodo-user1`) were provisioned automatically. Their initial credentials are stored in a Secret in the `keycloak-system` namespace called `keycloak-users`. You can reveal the credentials by switching to the tab containing the OpenShift Console using the following steps: 1. Select **Home** -> **Projects** from the left hand navigation bar 2. Locate and select the **keycloak-system** project diff --git a/values-hub.yaml b/values-hub.yaml index d23cc8ce..94284cf7 100644 --- a/values-hub.yaml +++ b/values-hub.yaml @@ -255,6 +255,40 @@ clusterGroup: project: hub chart: hashicorp-vault chartVersion: 0.1.* + # Custom Vault policies for least-privilege access + # Each application gets access only to its specific secrets path + # + # TWO types of policies needed: + # 1. -k8s-secret - for Kubernetes auth (ClusterSecretStore/ExternalSecrets) + # 2. -jwt-secret - for JWT/SPIFFE auth (application workloads) + # + # NOTE: K8s auth policies are auto-created by Ansible from vaultPrefixes + # JWT auth policies below are manually defined for apps that need direct Vault access + policies: + # ============================================================ + # JWT/SPIFFE Auth Policies (for application workloads) + # These are used by apps authenticating via SPIFFE JWT tokens + # Only define policies for apps that need direct Vault access + # K8s auth policies (-k8s-secret) are auto-created by Ansible + # ============================================================ + - name: apps-qtodo-jwt-secret + policy: | + path "secret/data/apps/qtodo/*" { + capabilities = ["read"] + } + - name: hub-infra-rhtpa-jwt-secret + policy: | + path "secret/data/hub/infra/rhtpa/*" { + capabilities = ["read"] + } + - name: hub-supply-chain-jwt-secret + policy: | + path "secret/data/hub/infra/quay/*" { + capabilities = ["read"] + } + path "secret/data/hub/infra/rhtpa/rhtpa-oidc-cli" { + capabilities = ["read"] + } jwt: enabled: true oidcDiscoveryUrl: https://spire-spiffe-oidc-discovery-provider.zero-trust-workload-identity-manager.svc.cluster.local @@ -265,13 +299,19 @@ clusterGroup: audience: qtodo subject: spiffe://apps.{{ $.Values.global.clusterDomain }}/ns/qtodo/sa/qtodo policies: - - global-secret + - apps-qtodo-jwt-secret # RHTPA vault role # - name: rhtpa # audience: rhtpa # subject: spiffe://apps.{{ $.Values.global.clusterDomain }}/ns/trusted-profile-analyzer/sa/rhtpa # policies: - # - global-secret + # - hub-infra-rhtpa-jwt-secret + # Supply chain vault role (for Tekton pipelines) + # - name: supply-chain + # audience: supply-chain + # subject: spiffe://apps.{{ $.Values.global.clusterDomain }}/ns/pipeline/sa/pipeline + # policies: + # - hub-supply-chain-jwt-secret # Shared Object Storage Backend # Required for RHTPA and QUAY (provides S3-compatible storage via NooBaa MCG) # NooBaa MCG provides S3-compatible object storage for multiple applications @@ -403,7 +443,7 @@ clusterGroup: - name: app.vault.role value: qtodo - name: app.vault.secretPath - value: secret/data/global/qtodo + value: secret/data/apps/qtodo/qtodo-db # For Secure Supply Chain, we changed the qtodo image to use the one built in the secure supply chain # - name: app.images.main.name # value: quay-registry-quay-quay-enterprise.apps.{{ $.Values.global.clusterDomain }}/ztvp/qtodo diff --git a/values-secret.yaml.template b/values-secret.yaml.template index a6e25327..e1f48ac9 100644 --- a/values-secret.yaml.template +++ b/values-secret.yaml.template @@ -5,6 +5,26 @@ version: "2.0" # Ideally you NEVER COMMIT THESE VALUES TO GIT (although if all passwords are # automatically generated inside the vault this should not really matter) +# Vault Secret Organization: +# -------------------------- +# Secrets are organized for least-privilege access: +# +# Application Secrets (fine-grained isolation): +# apps/qtodo/ - QTodo application secrets (app-level isolation) +# apps// - Add your app here for isolated secrets +# +# Infrastructure Secrets (hub/infra/*): +# hub/infra/keycloak/ - Keycloak infrastructure secrets +# hub/infra/rhtpa/ - RHTPA infrastructure secrets +# hub/infra/quay/ - Quay registry credentials +# hub/infra/users/ - User credentials managed by IdP +# +# Framework Secrets: +# global/ - VP framework default (config-demo, etc.) +# +# Each path has a corresponding Vault policy granting access ONLY to its +# specific path (e.g., apps-qtodo-secret grants read to secret/data/apps/qtodo/*). + vaultPolicies: basicPolicy: | length=10 @@ -26,6 +46,10 @@ vaultPolicies: rule "charset" { charset = "0123456789" min-chars = 1 } secrets: + # =========================================================================== + # GLOBAL SECRETS (global/) + # VP framework default path for demo/test secrets + # =========================================================================== - name: config-demo vaultPrefixes: - global @@ -33,9 +57,15 @@ secrets: - name: secret onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy - - name: keycloak + + # =========================================================================== + # QTODO APPLICATION SECRETS (apps/qtodo/) + # Secrets specific to the QTodo application - isolated at app level + # Policy: apps-qtodo-secret (read access to apps/qtodo/*) + # =========================================================================== + - name: qtodo-db vaultPrefixes: - - global + - apps/qtodo fields: - name: admin-password onMissingValue: generate @@ -43,60 +73,117 @@ secrets: - name: db-password onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy - - name: qtodo + + - name: qtodo-oidc-client vaultPrefixes: - - global + - apps/qtodo fields: - - name: db-password + - name: client-secret onMissingValue: generate - vaultPolicy: validatedPatternDefaultPolicy + vaultPolicy: alphaNumericPolicy + - name: qtodo-truststore vaultPrefixes: - - global + - apps/qtodo fields: - name: truststore-password onMissingValue: generate vaultPolicy: alphaNumericPolicy - - name: keycloak-users + + # =========================================================================== + # KEYCLOAK INFRASTRUCTURE SECRETS (hub/infra/keycloak/) + # Secrets for Keycloak infrastructure deployment + # Policy: hub-infra-keycloak-secret (read access to hub/infra/keycloak/*) + # =========================================================================== + - name: keycloak vaultPrefixes: - - global + - hub/infra/keycloak fields: - - name: qtodo-admin-password - onMissingValue: generate - vaultPolicy: validatedPatternDefaultPolicy - - name: qtodo-user1-password + - name: admin-password onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy - - name: rhtas-user-password + - name: db-password onMissingValue: generate vaultPolicy: validatedPatternDefaultPolicy - - name: rhtpa-user-password + + # =========================================================================== + # RHTPA INFRASTRUCTURE SECRETS (hub/infra/rhtpa/) + # Secrets for Red Hat Trusted Profile Analyzer infrastructure + # Policy: hub-infra-rhtpa-secret (read access to hub/infra/rhtpa/*) + # =========================================================================== + - name: rhtpa-db + vaultPrefixes: + - hub/infra/rhtpa + fields: + - name: db-password onMissingValue: generate vaultPolicy: alphaNumericPolicy - - name: oidc-client-secret + + - name: rhtpa-oidc-cli vaultPrefixes: - - global + - hub/infra/rhtpa fields: - name: client-secret onMissingValue: generate vaultPolicy: alphaNumericPolicy - # RHTPA (Red Hat Trusted Profile Analyzer) secrets - - name: rhtpa + # =========================================================================== + # USER CREDENTIALS (hub/infra/users/) + # User passwords managed by Keycloak for application access + # Policy: hub-infra-users-secret (Keycloak needs to provision these) + # =========================================================================== + - name: keycloak-users vaultPrefixes: - - global + - hub/infra/users fields: - - name: db-password + - name: qtodo-admin-password + onMissingValue: generate + vaultPolicy: validatedPatternDefaultPolicy + - name: qtodo-user1-password + onMissingValue: generate + vaultPolicy: validatedPatternDefaultPolicy + - name: rhtas-user-password + onMissingValue: generate + vaultPolicy: validatedPatternDefaultPolicy + - name: rhtpa-user-password onMissingValue: generate vaultPolicy: alphaNumericPolicy - - name: rhtpa-oidc-cli-secret + + # =========================================================================== + # QUAY INFRASTRUCTURE SECRETS (hub/infra/quay/) + # Registry credentials for Quay + # Policy: hub-infra-quay-secret (read access to hub/infra/quay/*) + # =========================================================================== + - name: quay-users vaultPrefixes: - - global + - hub/infra/quay fields: - - name: client-secret + - name: quay-admin-password onMissingValue: generate - vaultPolicy: alphaNumericPolicy + vaultPolicy: validatedPatternDefaultPolicy + - name: quay-user-password + onMissingValue: generate + vaultPolicy: validatedPatternDefaultPolicy + + # External Registry Credentials (e.g., Quay.io, Docker Hub, GHCR) + # Reserved for future use with container signing workflows + # Uncomment and provide your credentials when needed + #- name: external-registry + # vaultPrefixes: + # - hub/infra + # fields: + # - name: username + # value: "your-registry-username" # Replace with your username + # onMissingValue: error + # - name: password + # value: "your-registry-token" # Replace with your token/password + # onMissingValue: error + # =========================================================================== + # HUB-SPECIFIC SECRETS (hub/) + # Secrets for hub cluster management (spoke kubeconfigs, etc.) + # Policy: hub-secret (built-in VP policy) + # =========================================================================== # If you use clusterPools you will need to uncomment the following lines #- name: aws # fields: @@ -120,31 +207,6 @@ secrets: # - name: content # path: ~/.pullsecret.json - - name: quay-users - vaultPrefixes: - - global - fields: - - name: quay-admin-password - onMissingValue: generate - vaultPolicy: validatedPatternDefaultPolicy - - name: developer1-password - onMissingValue: generate - vaultPolicy: validatedPatternDefaultPolicy - - # External Registry Credentials (e.g., Quay.io, Docker Hub, GHCR) - # Reserved for future use with container signing workflows - # Uncomment and provide your credentials when needed - #- name: external-registry - # vaultPrefixes: - # - global - # fields: - # - name: username - # value: "your-registry-username" # Replace with your username - # onMissingValue: error - # - name: password - # value: "your-registry-token" # Replace with your token/password - # onMissingValue: error - # If you are going to import spoke clusters, add here their kubeconfig entries #- name: kubeconfig-spoke-1 # vaultPrefixes: