diff --git a/.github/workflows/deploy-environment.yaml b/.github/workflows/deploy-environment.yaml index 3e3f400..992cd33 100644 --- a/.github/workflows/deploy-environment.yaml +++ b/.github/workflows/deploy-environment.yaml @@ -302,6 +302,20 @@ jobs: id: db run: echo "url=$(terraform output -raw dashboard_url 2>/dev/null || true)" >> $GITHUB_OUTPUT + - name: Get smoke test virtual key + id: smoke_key + shell: bash + run: | + set -euo pipefail + KV_NAME=$(printf 'pvc-%s-%s-kv' "${TF_VAR_env}" "${TF_VAR_projname}" | tr '[:upper:]_' '[:lower:]-' | cut -c1-24) + KEY=$(az keyvault secret show --vault-name "${KV_NAME}" --name vkey-dashboard-playground --query value -o tsv) + if [[ -z "${KEY}" || "${KEY}" != sk-* ]]; then + echo "::error::Key Vault secret vkey-dashboard-playground in ${KV_NAME} must contain a LiteLLM virtual key beginning with sk-." + exit 1 + fi + echo "::add-mask::${KEY}" + echo "key=${KEY}" >> "$GITHUB_OUTPUT" + - name: Runtime diagnostics (Container App config) shell: bash run: | @@ -342,7 +356,7 @@ jobs: uses: ./.github/actions/smoke-test-gateway with: gateway_url: ${{ steps.gw.outputs.url }} - gateway_key: ${{ secrets.AIGATEWAY_KEY }} + gateway_key: ${{ steps.smoke_key.outputs.key }} embedding_model: ${{ env.TF_VAR_embedding_deployment }} codex_model: ${{ env.TF_VAR_codex_model }} aoai_endpoint: ${{ env.TF_VAR_azure_openai_endpoint }} diff --git a/docs/CI_CD.md b/docs/CI_CD.md index 6c10465..3826d27 100644 --- a/docs/CI_CD.md +++ b/docs/CI_CD.md @@ -25,6 +25,11 @@ The composite action `.github/actions/smoke-test-gateway` performs: - Candidate probing for embeddings if the requested model fails. - Azure OpenAI deployment discovery fallback using configured endpoint/key when needed. +Deploy jobs authenticate those gateway smoke calls with the +`vkey-dashboard-playground` virtual key from the environment Key Vault. The +`AIGATEWAY_KEY` secret remains the LiteLLM master key used by Terraform/runtime +configuration, not the user-facing API key for smoke traffic. + Additionally, when `STATE_SERVICE_CONTAINER_IMAGE` is configured, `deploy.yaml` runs state-service smoke checks via dashboard proxy endpoints: - `GET /api/state/catalog` diff --git a/infra/env/prod/main.tf b/infra/env/prod/main.tf index 48aa9d1..a20bdd7 100644 --- a/infra/env/prod/main.tf +++ b/infra/env/prod/main.tf @@ -140,11 +140,12 @@ module "dashboard" { tpm_limit = var.tpm_limit # Entra OIDC plumbing (Item 11, Path 1). Only consumed when auth_mode = "entra". - auth_mode = var.dashboard_auth_mode - entra_tenant_id = var.entra_tenant_id - entra_client_id = var.entra_client_id - key_vault_id = module.sluice.key_vault_id - gateway_key_versionless_id = module.sluice.gateway_key_versionless_id + auth_mode = var.dashboard_auth_mode + entra_tenant_id = var.entra_tenant_id + entra_client_id = var.entra_client_id + key_vault_id = module.sluice.key_vault_id + gateway_key_versionless_id = module.sluice.gateway_key_versionless_id + gateway_proxy_key_secret_name = "vkey-dashboard-playground" } output "gateway_url" { diff --git a/infra/modules/dashboard_aca/main.tf b/infra/modules/dashboard_aca/main.tf index 3a31164..f44e1a8 100644 --- a/infra/modules/dashboard_aca/main.tf +++ b/infra/modules/dashboard_aca/main.tf @@ -16,6 +16,8 @@ locals { use_entra = var.auth_mode == "entra" gateway_url = trimspace(var.gateway_public_url) != "" ? trimspace(var.gateway_public_url) : var.gateway_url + gateway_proxy_key_secret_name = trimspace(var.gateway_proxy_key_secret_name) + gateway_proxy_key_secret_id = local.gateway_proxy_key_secret_name != "" ? data.azurerm_key_vault_secret.gateway_proxy_key[0].versionless_id : var.gateway_key_versionless_id # Derive the dashboard's public URL from the gateway URL by swapping the # subdomain. Gateway: https://pvc-{env}-{proj}-ca. → Dashboard: @@ -40,7 +42,7 @@ data "azurerm_resource_group" "shared" { # When auth_mode = "entra" the dashboard reads three KV secrets at runtime: # - entra-client-secret (set out-of-band during Entra app provisioning) # - dashboard-session-secret (next-auth JWE encryption key) -# - gateway-key (LiteLLM master key, owned by the sluice module) +# - vkey-dashboard-playground or configured gateway proxy key data "azurerm_key_vault_secret" "entra_client_secret" { count = local.use_entra ? 1 : 0 name = "entra-client-secret" @@ -53,6 +55,12 @@ data "azurerm_key_vault_secret" "dashboard_session_secret" { key_vault_id = var.key_vault_id } +data "azurerm_key_vault_secret" "gateway_proxy_key" { + count = local.use_entra && local.gateway_proxy_key_secret_name != "" ? 1 : 0 + name = local.gateway_proxy_key_secret_name + key_vault_id = var.key_vault_id +} + resource "azurerm_user_assigned_identity" "dashboard" { count = local.use_entra ? 1 : 0 name = "${local.ca_name}-id" @@ -77,8 +85,8 @@ resource "azurerm_container_app" "dashboard" { error_message = "container_image must not be empty." } precondition { - condition = var.auth_mode != "entra" || (var.entra_tenant_id != "" && var.entra_client_id != "" && var.key_vault_id != "" && var.gateway_key_versionless_id != "") - error_message = "auth_mode = 'entra' requires entra_tenant_id, entra_client_id, key_vault_id, and gateway_key_versionless_id." + condition = var.auth_mode != "entra" || (var.entra_tenant_id != "" && var.entra_client_id != "" && var.key_vault_id != "" && local.gateway_proxy_key_secret_id != "") + error_message = "auth_mode = 'entra' requires entra_tenant_id, entra_client_id, key_vault_id, and either gateway_proxy_key_secret_name or gateway_key_versionless_id." } } @@ -128,7 +136,7 @@ resource "azurerm_container_app" "dashboard" { for_each = local.use_entra ? [1] : [] content { name = "litellm-gateway-key" - key_vault_secret_id = var.gateway_key_versionless_id + key_vault_secret_id = local.gateway_proxy_key_secret_id identity = azurerm_user_assigned_identity.dashboard[0].id } } diff --git a/infra/modules/dashboard_aca/variables.tf b/infra/modules/dashboard_aca/variables.tf index 6d6c458..7664262 100644 --- a/infra/modules/dashboard_aca/variables.tf +++ b/infra/modules/dashboard_aca/variables.tf @@ -157,12 +157,18 @@ variable "entra_client_id" { variable "key_vault_id" { type = string - description = "Resource ID of the shared Key Vault. Required when auth_mode = 'entra' so the dashboard's UAMI can read entra-client-secret, dashboard-session-secret, and gateway-key. Empty otherwise." + description = "Resource ID of the shared Key Vault. Required when auth_mode = 'entra' so the dashboard's UAMI can read entra-client-secret, dashboard-session-secret, and the gateway proxy key. Empty otherwise." default = "" } variable "gateway_key_versionless_id" { type = string - description = "Versionless KV secret ID for the LiteLLM master key the BFF forwards to the gateway. Only consumed when auth_mode = 'entra'." + description = "Versionless KV secret ID for the fallback LiteLLM gateway key the BFF forwards to the gateway. Only consumed when auth_mode = 'entra' and gateway_proxy_key_secret_name is empty." + default = "" +} + +variable "gateway_proxy_key_secret_name" { + type = string + description = "Key Vault secret name for the LiteLLM virtual key the Entra BFF forwards to the gateway. Leave empty to use gateway_key_versionless_id." default = "" }