diff --git a/docker/start.sh b/docker/start.sh index 961190fed8..8376791733 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -37,7 +37,10 @@ fi echo "Checking PyRIT installation..." python -c "import pyrit; print(f'Running PyRIT version: {pyrit.__version__}')" -# Write .env file from PYRIT_ENV_CONTENTS (injected from Key Vault secret) +# Write .env file from PYRIT_ENV_CONTENTS (injected as the Container App's +# inline `env-file` secret; previously a Key Vault secretRef, but ACA isn't on +# Key Vault's "trusted services" list so SFI-locked-down KVs can't be read at +# runtime — see infra/main.bicep for details). if [ -n "$PYRIT_ENV_CONTENTS" ]; then mkdir -p ~/.pyrit echo "$PYRIT_ENV_CONTENTS" > ~/.pyrit/.env diff --git a/gui-deploy.yml b/gui-deploy.yml index 3e3741e48f..5023f6c31c 100644 --- a/gui-deploy.yml +++ b/gui-deploy.yml @@ -10,8 +10,16 @@ # - copyrit-gui-common (azureServiceConnection, acrName, acrLoginServer, imageName) # - copyrit-gui-test (resourceGroup, appName, entraTenantId, entraClientId, # allowedGroupObjectIds, sqlServerFqdn, sqlDatabaseName, -# keyVaultResourceId, acrResourceId, enablePrivateEndpoint, enableOtel) +# keyVaultResourceId, acrResourceId, enablePrivateEndpoint, enableOtel, +# envFileContents [SECURE]) # - copyrit-gui-prod (same keys as test, with production values) +# +# The `envFileContents` variable must be marked SECURE in the ADO library. +# It contains the entire .env file (endpoints, keys, etc.) and is passed to +# the Bicep template as the @secure() `envFileContents` parameter via a +# parameters JSON file written to disk in the runner. Writing to a file +# (instead of `--parameters envFileContents=$(envFileContents)`) avoids +# exposing the value in the runner's process list / `ps` output. trigger: branches: @@ -118,21 +126,97 @@ stages: inlineScript: | set -euo pipefail + # Write deployment parameters to a temp file so the + # @secure() envFileContents value never appears on the + # az CLI command line (visible in process list). + PARAMS_FILE="$(mktemp --suffix=.parameters.json)" + trap 'rm -f "$PARAMS_FILE"' EXIT + + python3 - <<'PY' > "$PARAMS_FILE" + import json, os, sys + + + def parse_bool(name: str) -> bool: + """Strict bool parse: fail loudly if the ADO variable was unresolved. + + ADO leaves unresolved macro variables as the literal string + '$(varname)'. Without this guard, that string would silently + coerce to False (since 'lower() == "true"' is False), which can + deploy with the wrong network posture (e.g., public access on + when private endpoint was intended). Fail loudly instead. + """ + val = os.environ.get(name, "") + if val.lower() in ("true", "false"): + return val.lower() == "true" + sys.exit( + f"ERROR: Pipeline variable {name} must be 'true' or 'false', " + f"got: {val!r}. Check the ADO variable group (likely the " + f"variable is missing or misspelled — ADO leaves unresolved " + f"macros as the literal string '$(name)')." + ) + + + def parse_secret_str(name: str) -> str: + """Fail loudly if a SECURE pipeline variable was unresolved. + + Same root cause as parse_bool: ADO leaves unresolved macros as + '$(varname)'. For securestring parameters (envFileContents), Azure + accepts ANY string, so an unresolved macro silently deploys garbage + that the container can't parse. Detect the pattern and abort. + """ + val = os.environ.get(name, "") + # Match exactly '$(identifier)' — ADO macro syntax for unresolved variables. + if val.startswith("$(") and val.endswith(")") and "(" not in val[2:-1]: + sys.exit( + f"ERROR: Pipeline variable {name} appears unresolved (got " + f"'{val}'). The ADO library variable is likely missing or " + f"misspelled (ADO leaves unresolved macros as the literal " + f"string '$(name)'). For envFileContents, also confirm the " + f"variable is marked SECURE in the ADO library." + ) + if not val: + sys.exit(f"ERROR: Pipeline variable {name} is empty.") + return val + + + doc = { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appName": {"value": os.environ["APP_NAME"]}, + "containerImage": {"value": os.environ["CONTAINER_IMAGE"]}, + "entraTenantId": {"value": os.environ["ENTRA_TENANT_ID"]}, + "entraClientId": {"value": os.environ["ENTRA_CLIENT_ID"]}, + "allowedGroupObjectIds": {"value": os.environ["ALLOWED_GROUP_OBJECT_IDS"]}, + "sqlServerFqdn": {"value": os.environ["SQL_SERVER_FQDN"]}, + "sqlDatabaseName": {"value": os.environ["SQL_DATABASE_NAME"]}, + "keyVaultResourceId": {"value": os.environ["KEY_VAULT_RESOURCE_ID"]}, + "acrResourceId": {"value": os.environ["ACR_RESOURCE_ID"]}, + "enablePrivateEndpoint": {"value": parse_bool("ENABLE_PRIVATE_ENDPOINT")}, + "enableOtel": {"value": parse_bool("ENABLE_OTEL")}, + "envFileContents": {"value": parse_secret_str("ENV_FILE_CONTENTS")}, + }, + } + print(json.dumps(doc)) + PY + az deployment group create \ --resource-group $(resourceGroup) \ --template-file $(Build.SourcesDirectory)/infra/main.bicep \ - --parameters appName=$(appName) \ - --parameters containerImage=$(acrLoginServer)/$(imageName):$(Build.SourceVersion) \ - --parameters entraTenantId=$(entraTenantId) \ - --parameters entraClientId=$(entraClientId) \ - --parameters allowedGroupObjectIds="$(allowedGroupObjectIds)" \ - --parameters sqlServerFqdn=$(sqlServerFqdn) \ - --parameters sqlDatabaseName=$(sqlDatabaseName) \ - --parameters keyVaultResourceId=$(keyVaultResourceId) \ - --parameters acrResourceId=$(acrResourceId) \ - --parameters enablePrivateEndpoint=$(enablePrivateEndpoint) \ - --parameters enableOtel=$(enableOtel) \ - --parameters envSecretName=$(envSecretName) + --parameters @"$PARAMS_FILE" + env: + APP_NAME: $(appName) + CONTAINER_IMAGE: $(acrLoginServer)/$(imageName):$(Build.SourceVersion) + ENTRA_TENANT_ID: $(entraTenantId) + ENTRA_CLIENT_ID: $(entraClientId) + ALLOWED_GROUP_OBJECT_IDS: $(allowedGroupObjectIds) + SQL_SERVER_FQDN: $(sqlServerFqdn) + SQL_DATABASE_NAME: $(sqlDatabaseName) + KEY_VAULT_RESOURCE_ID: $(keyVaultResourceId) + ACR_RESOURCE_ID: $(acrResourceId) + ENABLE_PRIVATE_ENDPOINT: $(enablePrivateEndpoint) + ENABLE_OTEL: $(enableOtel) + ENV_FILE_CONTENTS: $(envFileContents) - task: AzureCLI@2 displayName: 'Health check' @@ -189,21 +273,97 @@ stages: inlineScript: | set -euo pipefail + # Write deployment parameters to a temp file so the + # @secure() envFileContents value never appears on the + # az CLI command line (visible in process list). + PARAMS_FILE="$(mktemp --suffix=.parameters.json)" + trap 'rm -f "$PARAMS_FILE"' EXIT + + python3 - <<'PY' > "$PARAMS_FILE" + import json, os, sys + + + def parse_bool(name: str) -> bool: + """Strict bool parse: fail loudly if the ADO variable was unresolved. + + ADO leaves unresolved macro variables as the literal string + '$(varname)'. Without this guard, that string would silently + coerce to False (since 'lower() == "true"' is False), which can + deploy with the wrong network posture (e.g., public access on + when private endpoint was intended). Fail loudly instead. + """ + val = os.environ.get(name, "") + if val.lower() in ("true", "false"): + return val.lower() == "true" + sys.exit( + f"ERROR: Pipeline variable {name} must be 'true' or 'false', " + f"got: {val!r}. Check the ADO variable group (likely the " + f"variable is missing or misspelled — ADO leaves unresolved " + f"macros as the literal string '$(name)')." + ) + + + def parse_secret_str(name: str) -> str: + """Fail loudly if a SECURE pipeline variable was unresolved. + + Same root cause as parse_bool: ADO leaves unresolved macros as + '$(varname)'. For securestring parameters (envFileContents), Azure + accepts ANY string, so an unresolved macro silently deploys garbage + that the container can't parse. Detect the pattern and abort. + """ + val = os.environ.get(name, "") + # Match exactly '$(identifier)' — ADO macro syntax for unresolved variables. + if val.startswith("$(") and val.endswith(")") and "(" not in val[2:-1]: + sys.exit( + f"ERROR: Pipeline variable {name} appears unresolved (got " + f"'{val}'). The ADO library variable is likely missing or " + f"misspelled (ADO leaves unresolved macros as the literal " + f"string '$(name)'). For envFileContents, also confirm the " + f"variable is marked SECURE in the ADO library." + ) + if not val: + sys.exit(f"ERROR: Pipeline variable {name} is empty.") + return val + + + doc = { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appName": {"value": os.environ["APP_NAME"]}, + "containerImage": {"value": os.environ["CONTAINER_IMAGE"]}, + "entraTenantId": {"value": os.environ["ENTRA_TENANT_ID"]}, + "entraClientId": {"value": os.environ["ENTRA_CLIENT_ID"]}, + "allowedGroupObjectIds": {"value": os.environ["ALLOWED_GROUP_OBJECT_IDS"]}, + "sqlServerFqdn": {"value": os.environ["SQL_SERVER_FQDN"]}, + "sqlDatabaseName": {"value": os.environ["SQL_DATABASE_NAME"]}, + "keyVaultResourceId": {"value": os.environ["KEY_VAULT_RESOURCE_ID"]}, + "acrResourceId": {"value": os.environ["ACR_RESOURCE_ID"]}, + "enablePrivateEndpoint": {"value": parse_bool("ENABLE_PRIVATE_ENDPOINT")}, + "enableOtel": {"value": parse_bool("ENABLE_OTEL")}, + "envFileContents": {"value": parse_secret_str("ENV_FILE_CONTENTS")}, + }, + } + print(json.dumps(doc)) + PY + az deployment group create \ --resource-group $(resourceGroup) \ --template-file $(Build.SourcesDirectory)/infra/main.bicep \ - --parameters appName=$(appName) \ - --parameters containerImage=$(acrLoginServer)/$(imageName):$(Build.SourceVersion) \ - --parameters entraTenantId=$(entraTenantId) \ - --parameters entraClientId=$(entraClientId) \ - --parameters allowedGroupObjectIds="$(allowedGroupObjectIds)" \ - --parameters sqlServerFqdn=$(sqlServerFqdn) \ - --parameters sqlDatabaseName=$(sqlDatabaseName) \ - --parameters keyVaultResourceId=$(keyVaultResourceId) \ - --parameters acrResourceId=$(acrResourceId) \ - --parameters enablePrivateEndpoint=$(enablePrivateEndpoint) \ - --parameters enableOtel=$(enableOtel) \ - --parameters envSecretName=$(envSecretName) + --parameters @"$PARAMS_FILE" + env: + APP_NAME: $(appName) + CONTAINER_IMAGE: $(acrLoginServer)/$(imageName):$(Build.SourceVersion) + ENTRA_TENANT_ID: $(entraTenantId) + ENTRA_CLIENT_ID: $(entraClientId) + ALLOWED_GROUP_OBJECT_IDS: $(allowedGroupObjectIds) + SQL_SERVER_FQDN: $(sqlServerFqdn) + SQL_DATABASE_NAME: $(sqlDatabaseName) + KEY_VAULT_RESOURCE_ID: $(keyVaultResourceId) + ACR_RESOURCE_ID: $(acrResourceId) + ENABLE_PRIVATE_ENDPOINT: $(enablePrivateEndpoint) + ENABLE_OTEL: $(enableOtel) + ENV_FILE_CONTENTS: $(envFileContents) - task: AzureCLI@2 displayName: 'Health check' diff --git a/infra/DEPLOY_NEW_INSTANCE.md b/infra/DEPLOY_NEW_INSTANCE.md index f3137434c1..d789f9adc0 100644 --- a/infra/DEPLOY_NEW_INSTANCE.md +++ b/infra/DEPLOY_NEW_INSTANCE.md @@ -36,7 +36,7 @@ within an instance. The trust boundary is Entra group membership. | User-Assigned Managed Identity | `copyrit-{instance-name}-identity` | | Azure SQL Server + Database | `copyrit-{instance-name}-sql` / `pyrit-{instance-name}` | | Storage Account + Blob Container | `copyrit{instance-name-no-hyphens}sa` / `dbdata` | -| Key Vault | `copyrit-{instance-name}-kv` | +| Key Vault (locked down; backup/audit only — NOT read at runtime) | `copyrit-{instance-name}-kv` | | Entra App Registration | `CoPyRIT GUI ({instance-name})` | | Log Analytics Workspace | `copyrit-{instance-name}-logs` | @@ -225,37 +225,113 @@ Azure OpenAI or OpenAI endpoints — they don't need to match the AIRT instance. ## Updating Secrets -To update the `.env` contents after deployment: +The Container App reads its `.env` contents from an **inline secret** named +`env-file`. To rotate it safely, use `az containerapp secret set` with the +`@file` form (the file path is on the command line, not the secret value, so +nothing leaks via `ps`). -> **Important:** The deploy script auto-injects `AZURE_SQL_DB_CONNECTION_STRING` -> and `AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL` into the `.env` during -> initial deployment. When updating secrets manually, your `updated.env` file -> **must include** both variables. If omitted, the container will fail to -> connect to SQL or read blob results on restart. Check the current values with: -> ```bash -> az keyvault secret show --vault-name copyrit-{instance-name}-kv \ -> --name env-global --query value -o tsv | grep -E 'AZURE_SQL_DB|AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL' -> ``` +> **`updated.env` is your local file** — same format as `infra/env.demo.template` +> and the file you passed to `--env-file` during initial deployment, but with +> the new values you want to deploy. The filename is just a convention; you +> can name it anything. + +> ⚠️ **Verify the file exists before running `az`.** The Azure CLI's `@file` +> expansion is silent: if the path doesn't exist or has a typo, `az` falls +> back to the literal string `@./your-typo.env` and stores **that** as the +> secret value. The container will then read garbage and chat will break with +> no obvious error in the deploy step. + +**bash:** ```bash -az keyvault secret set \ - --vault-name copyrit-{instance-name}-kv \ - --name env-global \ - --file ./updated.env +# Pre-flight: fail fast if the file is missing +test -f ./updated.env || { echo "ERROR: ./updated.env not found"; exit 1; } + +az containerapp secret set \ + -n copyrit-{instance-name} \ + -g copyrit-{instance-name} \ + --secrets "env-file=@./updated.env" +``` + +**PowerShell:** + +```powershell +# Pre-flight: fail fast if the file is missing +if (-not (Test-Path .\updated.env)) { throw 'ERROR: .\updated.env not found' } + +az containerapp secret set ` + -n copyrit-{instance-name} ` + -g copyrit-{instance-name} ` + --secrets "env-file=@./updated.env" ``` -Then force a new revision so the container picks up the updated secret: +> **Important — `.env` content requirements when rotating manually:** +> +> The deploy script auto-injects two values during the **initial** deployment +> that you must include in `updated.env` if you rotate manually: +> - `AZURE_SQL_DB_CONNECTION_STRING` +> - `AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL` +> +> Without them the container will fail to connect to SQL or read blob storage +> on the next restart. Use the values that the deploy script logged on initial +> deploy (or look them up from the SQL/Storage resources). The KV +> `env-global` backup is the authoritative copy of what was last deployed via +> the script. + +After updating the secret, **force a new revision** so the running container +picks up the new value. Per Microsoft Container Apps docs, secret updates do +**not** auto-restart existing revisions — you must either deploy a new +revision or restart an existing one: ```bash +# bash az containerapp update \ -n copyrit-{instance-name} \ -g copyrit-{instance-name} \ --set-env-vars "SECRET_UPDATED=$(date +%s)" ``` -> **Note:** Simple restarts may not refresh Key Vault–backed secrets. Creating a -> new revision (via `az containerapp update`) is the most reliable way to force -> the container to read the latest secret value. +```powershell +# PowerShell +az containerapp update ` + -n copyrit-{instance-name} ` + -g copyrit-{instance-name} ` + --set-env-vars "SECRET_UPDATED=$([DateTimeOffset]::Now.ToUnixTimeSeconds())" +``` + +> **Note: this rotation path updates the inline ACA secret, not the KV +> `env-global` backup.** The KV copy will drift until the next full deploy. +> If you want to keep KV in sync, also run: +> ```bash +> az keyvault secret set --vault-name copyrit-{instance-name}-kv \ +> --name env-global --file ./updated.env +> ``` +> The KV is locked down (`publicNetworkAccess=Disabled`); this command must +> run from Azure Cloud Shell or with public access temporarily re-enabled. + +> **Anti-patterns to avoid:** +> +> - `az containerapp secret set --secrets "env-file=$ENV_CONTENT"` — exposes +> the value via process arguments (visible in `ps` while the command runs). +> The `@file` form above passes only the path, not the value. +> - `az containerapp secret show --secret-name env-file` — returns the full +> plaintext to your terminal / shell history. Inspect the KV backup +> instead, or use `az containerapp secret list -o table` to confirm the +> secret exists without revealing its value. +> - `python infra/deploy_instance.py ... --env-file ./updated.env` — the +> deploy script is **not** rotation-safe. It runs unconditional `create` +> operations on the Entra app, SQL server, Key Vault, and managed identity, +> most of which fail with "already exists" errors when re-run against an +> existing instance. The Entra app create succeeds and produces a +> duplicate registration, which is worse than a hard failure. + +> **Why inline instead of Key Vault reference?** Azure Container Apps is not on +> Key Vault's "trusted services" allowlist, so a locked-down KV +> (`publicNetworkAccess=Disabled`, required for SFI / NS221 compliance) blocks +> ACA's runtime secret resolver. Passing the secret inline at deploy time +> sidesteps the issue: the value is stored encrypted in the Container App's +> own secrets store, and the Key Vault is locked down with no runtime +> dependency. ## Adding or Removing Users @@ -329,8 +405,13 @@ az containerapp revision list \ Common causes: - **AcrPull role not propagated yet** — RBAC can take a few minutes. The container will retry automatically. -- **Key Vault secret not accessible** — Check that the managed identity has - `Key Vault Secrets User` on the vault. +- **Inline `env-file` secret missing or malformed** — The Container App reads + the `.env` from its own inline secret, not from Key Vault. Verify it exists: + ```bash + az containerapp secret list \ + -n copyrit-{instance-name} \ + -g copyrit-{instance-name} -o table + ``` - **Missing `.pyrit_conf`** — Older container images (before the `.pyrit_conf` guard was added) crash on startup because the `airt` initializer unconditionally reads this file. Use an image built from current `main`. @@ -349,8 +430,19 @@ Common causes: ### Targets not appearing in the GUI -- Check that the `.env` file in Key Vault has the correct endpoint/model/key - variables for each target. +- Confirm the inline `env-file` secret exists on the Container App: + ```bash + az containerapp secret list \ + -n copyrit-{instance-name} \ + -g copyrit-{instance-name} -o table + ``` +- If you suspect the env content is wrong, inspect the Key Vault backup + (`env-global`) instead of the inline secret. The Key Vault snapshot is + written by the deploy script alongside the Container App secret. Reading + from KV via `az keyvault secret show` requires either Cloud Shell or + temporarily re-opening KV public access (`--public-network-access Enabled`). + Avoid `az containerapp secret show --secret-name env-file` — it prints the + full plaintext to terminal/logs. - Check container logs for initializer errors: ```bash az containerapp logs show \ @@ -364,14 +456,12 @@ Common causes: - Verify the SQL contained user was created (step 3) with all three roles (`db_datareader`, `db_datawriter`, `db_ddladmin`). - The deploy script auto-injects `AZURE_SQL_DB_CONNECTION_STRING` into the - `.env` before uploading to Key Vault. If you see a connection string mismatch, - verify the Key Vault secret contents: - ```bash - az keyvault secret show \ - --vault-name copyrit-{instance-name}-kv \ - --name env-global \ - --query value -o tsv | grep AZURE_SQL_DB - ``` + `.env` before passing it to the Container App as an inline secret. If you + see a connection string mismatch, inspect the Key Vault backup + (`env-global`) — it holds the value that was last deployed via the script. + Reading from KV requires Cloud Shell or temporarily re-opening KV public + access. Avoid `az containerapp secret show` — it prints the full plaintext + to terminal/logs. - Verify the Azure SQL firewall allows Azure services (the script configures this, but verify with `az sql server firewall-rule list`). @@ -393,14 +483,18 @@ writing to blob storage: -g copyrit-{instance-name} --query id -o tsv) \ -o table ``` -- Verify the Key Vault secret has the correct - `AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL`: +- Verify the deployed env content has the correct + `AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL`. The safest way to check is + to inspect the Key Vault backup snapshot (after temporarily re-enabling + public access on the KV or running from Cloud Shell): ```bash - az keyvault secret show \ - --vault-name copyrit-{instance-name}-kv \ - --name env-global \ - --query value -o tsv | grep AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL + az keyvault secret show --vault-name copyrit-{instance-name}-kv \ + --name env-global --query value -o tsv | \ + grep AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL ``` + Avoid `az containerapp secret show` — it prints the full plaintext to + terminal/logs. If the value is wrong, rotate the secret using the manual + procedure in [Updating Secrets](#updating-secrets). - RBAC propagation takes ~60 seconds after a fresh deployment; if the role was granted very recently, restart the container revision. diff --git a/infra/README.md b/infra/README.md index 61e442da32..f121c2c148 100644 --- a/infra/README.md +++ b/infra/README.md @@ -83,8 +83,13 @@ Production is opt-in via `deployToProd: true`. Permissions-Policy, and Cache-Control (`no-store` on API routes). Swagger/OpenAPI disabled in production. - **Data**: Azure SQL with managed identity authentication (no passwords) -- **Secrets**: Key Vault with RBAC (existing vault, secrets referenced via - [ACA](https://learn.microsoft.com/en-us/azure/container-apps/) secretRef) +- **Secrets**: The `.env` file contents are passed inline to the Container App + as a `@secure()` Bicep parameter (`envFileContents`) and stored encrypted in + the Container App's secret store. Azure Container Apps is not on Key Vault's + "trusted services" list, so a locked-down KV would block runtime secret + resolution — passing inline avoids that. The Key Vault is still created and + populated with the `.env` content as `env-global` for backup/audit, then + fully locked down (`publicNetworkAccess=Disabled`). - **Images**: Unique tags or digests required — `:latest` is detected by a soft guardrail - **Supply chain**: [ACR](https://learn.microsoft.com/en-us/azure/container-registry/) pull via managed identity RBAC (must be granted manually; see Post-Deployment §2). @@ -287,11 +292,15 @@ echo "containerImage: $ACR_NAME.azurecr.io/pyrit:$COMMIT_SHA" > **Note**: The CI/CD pipeline handles build + push automatically. Manual push is > only needed for the initial bootstrap or if deploying outside the pipeline. -### 6. Key Vault (existing — required) +### 6. Key Vault (existing — required for backup/audit only) Use an existing Key Vault to avoid soft-delete/purge-protection naming conflicts -on redeployment. The managed identity must be granted `Key Vault Secrets User` on -the vault manually (the Bicep template does **not** create RBAC role assignments). +on redeployment. As of the inline-secret migration, the Container App does +**not** read secrets from Key Vault at runtime — the `.env` content is passed +inline via the `envFileContents` Bicep parameter. The vault is still required +because the deploy script writes the `.env` content as `env-global` for +backup/audit, but the managed identity does **not** need `Key Vault Secrets +User` (was previously required, no longer is). ```bash # Create a vault (if your org doesn't provide one) @@ -305,9 +314,11 @@ az keyvault create \ az keyvault show --name --query id -o tsv ``` -> **Note**: The vault should have `enableRbacAuthorization: true` so the managed -> identity can be granted access. Diagnostic settings (AuditEvent logs) should be -> configured on the vault separately by the vault owner. +> **Note**: The vault should have `enableRbacAuthorization: true`. Diagnostic +> settings (AuditEvent logs) should be configured separately by the vault +> owner. The deploy script applies `publicNetworkAccess=Disabled + +> defaultAction=Deny + bypass=AzureServices` after writing the backup secret +> (matches the team standard for SFI/NS221 compliance). ## Preview changes before deploying (recommended) @@ -350,17 +361,19 @@ az deployment group create \ ``` 2. **Grant managed identity RBAC** (required — the Bicep template does **not** create - role assignments; the app will fail to start without AcrPull and KV roles): + role assignments; the app will fail to start without AcrPull): ```bash MI_ID=$(az deployment group show -g -n main \ --query properties.outputs.managedIdentityPrincipalId.value -o tsv) - # Required — app won't start without these + # Required — app won't start without AcrPull # To find acrResourceId: az acr show --name --query id -o tsv az role assignment create --assignee-object-id $MI_ID \ --assignee-principal-type ServicePrincipal --role "AcrPull" --scope - az role assignment create --assignee-object-id $MI_ID \ - --assignee-principal-type ServicePrincipal --role "Key Vault Secrets User" --scope + + # Note: Key Vault Secrets User is NOT required — the Container App reads + # its .env contents from an inline secret (envFileContents), not via a + # Key Vault reference. # Grant based on which services you use (scope as narrowly as possible) az role assignment create --assignee-object-id $MI_ID \ @@ -410,20 +423,92 @@ needed in the container. | `operator` | — | Set per-user in the GUI | | | `operation` | — | Set per-user in the GUI | | -### .env file → Key Vault secret +### .env file → Container App inline secret + +The entire `.env` file is passed to the Bicep template as the `envFileContents` +`@secure()` parameter and stored as an inline `env-file` secret on the +Container App. The template injects it as the `PYRIT_ENV_CONTENTS` env var. +PyRIT parses this at startup to set all endpoint, model, and API key +environment variables. The Key Vault still receives the same content as +`env-global` for backup/audit, but it is **not** read at runtime. + +To rotate the `.env` after deployment, the rotation path depends on which +deploy path you used: -The entire `.env` file is stored as a single Key Vault secret (`env-global` by -default). The template references it via ACA secret and injects it as the -`PYRIT_ENV_CONTENTS` env var. PyRIT parses this at startup to set all endpoint, -model, and API key environment variables. +**For instances deployed via `infra/deploy_instance.py`:** + +Use `az containerapp secret set` with the `@file` form (the file path is on +the command line, not the secret value). + +> **`updated.env` is your local file** — same format as `infra/env.demo.template` +> and the file you passed to `--env-file` during initial deployment, but with +> the new values you want to deploy. The filename is just a convention; you +> can name it anything. + +> ⚠️ **Verify the file exists before running `az`.** The CLI's `@file` +> expansion is silent: if the path is wrong, `az` falls back to storing the +> literal string `@./your-typo.env` as the secret value, with no error. The +> container will then read garbage at startup. -To update the `.env` contents: ```bash -az keyvault secret set --vault-name --name env-global --file ~/.pyrit/.env +# bash +test -f ./updated.env || { echo "ERROR: ./updated.env not found"; exit 1; } +az containerapp secret set \ + -n copyrit-{instance-name} \ + -g copyrit-{instance-name} \ + --secrets "env-file=@./updated.env" + +# Force a new revision (per Microsoft docs, secret updates do NOT auto-restart +# existing revisions — a revision-scoped change is required to pick them up) +az containerapp update \ + -n copyrit-{instance-name} \ + -g copyrit-{instance-name} \ + --set-env-vars "SECRET_UPDATED=$(date +%s)" ``` -> ⚠️ `PYRIT_ENV_CONTENTS` may contain API keys. Ensure application logging does -> **not** dump environment variables or process state. +The `updated.env` file must include the auto-injected +`AZURE_SQL_DB_CONNECTION_STRING` and +`AZURE_STORAGE_ACCOUNT_DB_DATA_CONTAINER_URL` values from initial deploy +(check the KV `env-global` backup or the deploy script's log output). + +To keep the KV backup in sync, also run: +```bash +az keyvault secret set --vault-name copyrit-{instance-name}-kv \ + --name env-global --file ./updated.env +``` +(The KV is locked down — this needs Cloud Shell or temporary public access.) + +**For instances deployed via `gui-deploy.yml` (ADO pipeline):** + +Update the `envFileContents` SECURE variable in the relevant ADO library +variable group (`copyrit-gui-test` or `copyrit-gui-prod`) with the new +content, then re-run the pipeline. + +> ⚠️ The pipeline runs `az deployment group create`, which is incremental: +> if only the secret value changed, no revision-scoped property changes, so +> ACA will **not** automatically start a new revision. After the pipeline +> succeeds, manually force a new revision so the running container picks up +> the new secret: +> ```bash +> az containerapp update \ +> -n -g \ +> --set-env-vars "SECRET_UPDATED=$(date +%s)" +> ``` +> Or fold this into a final pipeline step. + +> ⚠️ **Anti-patterns to avoid:** +> - `az containerapp secret set --secrets "env-file=$ENV_CONTENT"` — +> passing the value as a literal CLI argument exposes it via `ps` while +> the command runs. Use the `@file` form above instead. +> - `az containerapp secret show --secret-name env-file` — returns the full +> plaintext to your terminal / shell history. +> - **Do not re-run `infra/deploy_instance.py` against an existing instance +> to rotate secrets.** The script's create steps (Entra app, SQL server, +> Key Vault, managed identity) are not idempotent and will either fail or +> produce duplicate Entra app registrations. + +> ⚠️ `PYRIT_ENV_CONTENTS` may contain API keys. Ensure application logging +> does **not** dump environment variables or process state. Azure services (OpenAI, Content Safety, Speech) support managed identity — when API key env vars are not set, PyRIT auto-falls back to `DefaultAzureCredential`, @@ -439,8 +524,10 @@ Platform, Groq, Google Gemini) require API keys in the `.env`. - **Log Analytics shared key**: `listKeys()` is the standard ACA pattern. The key is used during deployment only, not exposed to the application. - **Workload profiles**: Consumption tier. Defaults to 1 replica (no auto-scale). -- **Key Vault**: Must be an existing vault. RBAC must be granted manually (see - Post-Deployment §2). +- **Key Vault**: Must be an existing vault. Used for `.env` content backup/audit + only — the runtime secret comes from an inline ACA secret. RBAC for AcrPull is + still granted manually (see Post-Deployment §2); Key Vault Secrets User is no + longer required. - **OpenTelemetry**: When `enableOtel=true`, configure the agent post-deploy: ```bash AI_CONN=$(az deployment group show -g -n main \ diff --git a/infra/deploy_instance.py b/infra/deploy_instance.py index 1fb00164c7..737a7b9af6 100644 --- a/infra/deploy_instance.py +++ b/infra/deploy_instance.py @@ -10,8 +10,9 @@ 3. Entra security group (optional — can use existing) 4. Azure SQL server + database 5. Storage account + blob container (auto-injects container URL into .env) - 6. Key Vault + populate .env secret (auto-injects SQL connection string) - 7. Managed identity + RBAC role assignments (AcrPull, KV Secrets User, Storage Blob Data Contributor) + 6. Key Vault + populate .env secret (auto-injects SQL connection string; + applies SFI network lockdown — backup/audit only, NOT read at runtime) + 7. Managed identity + RBAC role assignments (AcrPull, Storage Blob Data Contributor) 7b. AOAI RBAC (optional — Cognitive Services OpenAI User on specified resources) 8. Bicep deployment (Container App, networking, logging) 9. Post-deploy: SPA redirect URI @@ -620,7 +621,25 @@ def create_key_vault( tags: list[str] | None = None, ) -> str: """ - Create a Key Vault and populate it with the .env secret. + Create a Key Vault, populate it with the .env secret as a backup/audit + snapshot, then apply the SFI network lockdown. + + The Container App does NOT read this secret at runtime — the .env contents + are passed inline to the Container App via the Bicep `envFileContents` + @secure() parameter. Azure Container Apps is not on Key Vault's + "trusted services" list, so a locked-down KV would block runtime secret + references. By passing the secret inline, we keep the runtime path + independent of KV network access. + + The vault still receives the .env content as `env-global` to: + - Provide a backup snapshot if the Container App is recreated. + - Preserve the audit trail for the deployed configuration. + + The vault is created with public network access enabled so that the + deployer (running from a corp network or dev container) can write the + initial secret. After the secret is uploaded, the vault is locked down + to publicNetworkAccess=Disabled + defaultAction=Deny + bypass=AzureServices + to satisfy the S360 / NS221 Secure PaaS alert. Args: resource_group (str): The resource group name. @@ -734,6 +753,28 @@ def create_key_vault( if tmp_path: Path(tmp_path).unlink(missing_ok=True) + # Apply SFI network lockdown after the backup secret is written. Safe + # because the Container App does NOT reference this KV at runtime + # (the .env is passed inline via Bicep's envFileContents @secure() param). + # Matches the team standard observed on existing AIRT vaults + # (e.g. airt-chatui-kv, AIRT-Blackhat-KV): publicNetworkAccess=Disabled, + # default-deny ACL, bypass for trusted Azure services. + logger.info("Applying SFI network lockdown to Key Vault: %s", vault_name) + run_az( + args=[ + "keyvault", + "update", + "--name", + vault_name, + "--public-network-access", + "Disabled", + "--default-action", + "Deny", + "--bypass", + "AzureServices", + ] + ) + return kv_id @@ -749,11 +790,18 @@ def deploy_bicep( sql_database_name: str, kv_resource_id: str, acr_name: str, + env_file_contents: str, owner_tag: str = "", ) -> dict: """ Deploy the Bicep template. + The .env contents are passed via a temp parameters file (not inline + --parameters key=value) because the value is multi-line, contains '=' + characters, and is marked @secure() in Bicep — passing it inline is + fragile and can leak the value into shell history. The temp file is + deleted after deployment. + Args: resource_group (str): The resource group name. app_name (str): The Container App name. @@ -763,42 +811,64 @@ def deploy_bicep( group_ids (str): Comma-separated group object IDs. sql_server_fqdn (str): The SQL server FQDN. sql_database_name (str): The SQL database name. - kv_resource_id (str): The Key Vault resource ID. + kv_resource_id (str): The Key Vault resource ID (kept for the + keyVaultName output; not referenced at container runtime). acr_name (str): The ACR name. + env_file_contents (str): The prepared .env content to inject as + the Container App's `env-file` secret. owner_tag (str): Value for the Owner tag on Bicep-managed resources. Returns: dict: The deployment outputs. """ logger.info("Deploying Bicep template to resource group: %s", resource_group) - params = [ - "deployment", - "group", - "create", - "--resource-group", - resource_group, - "--template-file", - str(BICEP_TEMPLATE), - "--parameters", - f"appName={app_name}", - f"containerImage={container_image}", - f"entraTenantId={tenant_id}", - f"entraClientId={client_id}", - f"allowedGroupObjectIds={group_ids}", - f"sqlServerFqdn={sql_server_fqdn}", - f"sqlDatabaseName={sql_database_name}", - f"keyVaultResourceId={kv_resource_id}", - f"acrName={acr_name}", - "enablePrivateEndpoint=false", - ] + + parameters: dict = { + "appName": {"value": app_name}, + "containerImage": {"value": container_image}, + "entraTenantId": {"value": tenant_id}, + "entraClientId": {"value": client_id}, + "allowedGroupObjectIds": {"value": group_ids}, + "sqlServerFqdn": {"value": sql_server_fqdn}, + "sqlDatabaseName": {"value": sql_database_name}, + "keyVaultResourceId": {"value": kv_resource_id}, + "acrName": {"value": acr_name}, + "enablePrivateEndpoint": {"value": False}, + "envFileContents": {"value": env_file_contents}, + } if owner_tag: - bicep_tags = {"Service": "pyrit-gui", "Owner": owner_tag} - params.append(f"tags={json.dumps(bicep_tags)}") - params += [ - "--query", - "properties.outputs", - ] - return run_az_json(args=params) + parameters["tags"] = {"value": {"Service": "pyrit-gui", "Owner": owner_tag}} + + parameters_doc = { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": parameters, + } + + params_path: str | None = None + try: + with tempfile.NamedTemporaryFile(mode="w", suffix=".parameters.json", delete=False, encoding="utf-8") as tmp: + params_path = tmp.name + json.dump(parameters_doc, tmp) + + return run_az_json( + args=[ + "deployment", + "group", + "create", + "--resource-group", + resource_group, + "--template-file", + str(BICEP_TEMPLATE), + "--parameters", + f"@{params_path}", + "--query", + "properties.outputs", + ] + ) + finally: + if params_path: + Path(params_path).unlink(missing_ok=True) def post_deploy( @@ -834,7 +904,6 @@ def create_managed_identity_and_grant_roles( resource_group: str, location: str, identity_name: str, - kv_resource_id: str, acr_name: str, storage_account_id: str, tags: list[str] | None = None, @@ -842,12 +911,15 @@ def create_managed_identity_and_grant_roles( """ Create a user-assigned managed identity and grant it pre-deploy RBAC roles. - The MI must have KV Secrets User, AcrPull, and Storage Blob Data Contributor - before the Bicep deployment can provision the container (it needs to pull - the image, read the KV secret, and access blob storage at startup). - Creating the MI here and granting roles before Bicep avoids the race - condition of Bicep creating the MI but the container needing roles that - don't exist yet. + The MI must have AcrPull and Storage Blob Data Contributor before the Bicep + deployment can provision the container (it needs to pull the image and + access blob storage at startup). Creating the MI here and granting roles + before Bicep avoids the race condition of Bicep creating the MI but the + container needing roles that don't exist yet. + + Note: Key Vault Secrets User is intentionally NOT granted. The Container + App reads its .env contents from an inline secret passed via the Bicep + `envFileContents` parameter, not from a Key Vault secret reference. Bicep's MI resource declaration is idempotent — it will adopt the existing MI. @@ -855,7 +927,6 @@ def create_managed_identity_and_grant_roles( resource_group (str): The resource group name. location (str): The Azure region. identity_name (str): The managed identity name. - kv_resource_id (str): The Key Vault resource ID for Secrets User role. acr_name (str): The ACR name for AcrPull role. storage_account_id (str): The storage account resource ID for Storage Blob Data Contributor role. @@ -892,24 +963,6 @@ def create_managed_identity_and_grant_roles( ] ) - # Grant KV Secrets User - logger.info("Granting Key Vault Secrets User to managed identity") - run_az( - args=[ - "role", - "assignment", - "create", - "--assignee-object-id", - mi_principal_id, - "--assignee-principal-type", - "ServicePrincipal", - "--role", - "Key Vault Secrets User", - "--scope", - kv_resource_id, - ] - ) - # Grant AcrPull acr_id = run_az_json( args=[ @@ -1225,7 +1278,7 @@ def main(args: list[str] | None = None) -> int: storage_container_url=storage["container_url"], ) - # Step 7: Create Key Vault + upload .env + # Step 7: Create Key Vault + upload .env (backup snapshot) + apply SFI lockdown kv_id = create_key_vault( resource_group=rg_name, location=parsed.location, @@ -1240,7 +1293,6 @@ def main(args: list[str] | None = None) -> int: resource_group=rg_name, location=parsed.location, identity_name=mi_name, - kv_resource_id=kv_id, acr_name=parsed.acr_name, storage_account_id=storage["account_id"], tags=resource_tags, @@ -1257,7 +1309,7 @@ def main(args: list[str] | None = None) -> int: ) logger.info("Granted Cognitive Services OpenAI User on %d/%d AOAI resources", aoai_granted, len(aoai_names)) - # Step 9: Deploy Bicep + # Step 9: Deploy Bicep (passes .env content inline as @secure() param) outputs = deploy_bicep( resource_group=rg_name, app_name=app_name, @@ -1269,6 +1321,7 @@ def main(args: list[str] | None = None) -> int: sql_database_name=sql["database_name"], kv_resource_id=kv_id, acr_name=parsed.acr_name, + env_file_contents=env_content, owner_tag=parsed.owner_tag, ) diff --git a/infra/main.bicep b/infra/main.bicep index d4efac8d99..5f2158a7ec 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -4,9 +4,14 @@ // Deploys the CoPyRIT GUI as an Azure Container App with: // - Workload profiles environment with public ingress + optional IP restriction // - MSAL PKCE authentication (frontend) + FastAPI JWT middleware (backend) -// - User-assigned managed identity for Azure SQL, ACR, Azure OpenAI, Key Vault +// - User-assigned managed identity for Azure SQL, ACR, Azure OpenAI // - Azure SQL (existing) via managed identity — no passwords -// - Key Vault for secrets (referenced via ACA secretRef, not embedded) +// - .env contents passed inline via the @secure() envFileContents parameter +// and stored as the Container App's encrypted `env-file` secret. (Previously +// sourced from Key Vault via secretRef, but ACA isn't on Key Vault's +// "trusted services" list, so locking down KV broke runtime resolution.) +// - Key Vault is still required (operator-supplied) and used by the deploy +// script as a backup/audit snapshot of the .env content; not read at runtime. // - Centralized logging via Log Analytics (configurable retention) // - No storage account keys, no embedded secrets, no :latest tags // @@ -19,15 +24,7 @@ // az deployment group create \ // --resource-group \ // --template-file infra/main.bicep \ -// --parameters appName=pyrit-gui \ -// containerImage=.azurecr.io/pyrit: \ -// entraClientId= \ -// entraTenantId= \ -// allowedGroupObjectIds= \ -// allowedCidr='' \ -// sqlServerFqdn=.database.windows.net \ -// sqlDatabaseName= \ -// keyVaultResourceId= +// --parameters @ // ============================================================================ // --- Parameters --- @@ -71,8 +68,9 @@ param sqlDatabaseName string @description('PyRIT initializer to run. Default "target airt" registers target configs + attack defaults.') param pyritInitializer string = 'target airt' -@description('Key Vault secret name containing the .env file contents (all endpoints, models, and API keys). The secret is mounted as an env var and PyRIT parses it at startup.') -param envSecretName string = 'env-global' +@secure() +@description('Contents of the .env file (endpoints, models, API keys). Passed inline to the Container App as a secure secret. Marked @secure() so the value is stripped from deployment history.') +param envFileContents string @description('Container CPU cores') param cpuCores string = '1.0' @@ -237,10 +235,19 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023- } // ============================================================================ -// Key Vault (existing — avoids soft-delete/purge-protection redeployment issues) -// All auth uses managed identity (Azure SQL, ACR, AOAI). The vault is for -// downstream API keys or sensitive config added as ACA Key Vault secret -// references. Ensure the vault has RBAC authorization enabled. +// Key Vault (existing — created and locked down by deploy_instance.py). +// +// IMPORTANT: As of this template, the Container App does NOT reference the +// Key Vault at runtime. The .env contents are passed inline via the +// envFileContents @secure() parameter (see "secrets" block below). This +// removes the runtime dependency on Key Vault, which lets the deploy script +// fully lock down the vault (publicNetworkAccess=Disabled) without breaking +// the Container App. Azure Container Apps is not on Key Vault's +// "trusted services" list, so KV-backed secret references would otherwise +// fail at container startup against a locked-down vault. +// +// The KV is still referenced here for the keyVaultName output (operator +// convenience) and serves as a backup/audit copy of the .env content. // ============================================================================ // Extract KV name and resource group from the resource ID. // keyVaultResourceId format: /subscriptions/.../resourceGroups//providers/.../vaults/ @@ -249,9 +256,9 @@ var keyVaultName = last(split(keyVaultResourceId, '/')) // ============================================================================ // RBAC role assignments are NOT managed by this template. // Grant the following roles to the UAMI manually before first deployment: -// - Key Vault Secrets User on the Key Vault // - AcrPull on the ACR // See Post-Deployment in infra/README.md for commands. +// (Key Vault Secrets User is no longer required — see the KV note above.) // ============================================================================ // ============================================================================ @@ -370,8 +377,10 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { '${managedIdentity.id}': {} } } - // RBAC roles (AcrPull, KV Secrets User) must be granted manually before - // the first deployment — see infra/README.md Post-Deployment §2. + // AcrPull RBAC must be granted manually before the first deployment — + // see infra/README.md Post-Deployment §2. (Key Vault Secrets User is no + // longer required — the Container App reads its env content from an inline + // secret, not a Key Vault reference.) dependsOn: [] properties: { managedEnvironmentId: acaEnvironment.id @@ -404,12 +413,16 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { } ] - // Key Vault secret reference for the .env file contents + // Inline secret containing the .env file contents. + // Previously sourced from Key Vault via secretRef, but Azure Container + // Apps is not on Key Vault's "trusted services" list, so locking down + // the KV (publicNetworkAccess=Disabled) breaks runtime secret resolution. + // The value is passed at deploy time via the @secure() envFileContents + // parameter and stored encrypted in the Container App's secrets store. secrets: [ { name: 'env-file' - keyVaultUrl: 'https://${keyVaultName}${environment().suffixes.keyvaultDns}/secrets/${envSecretName}' - identity: managedIdentity.id + value: envFileContents } ] } @@ -441,7 +454,7 @@ resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { name: 'PYRIT_INITIALIZER' value: pyritInitializer } - // .env file contents from Key Vault — PyRIT parses this at startup + // .env file contents — PyRIT parses this at startup { name: 'PYRIT_ENV_CONTENTS' secretRef: 'env-file' diff --git a/infra/main.json b/infra/main.json index 99df00dc68..e4fdb48904 100644 --- a/infra/main.json +++ b/infra/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "16381559389474872325" + "version": "0.42.1.51946", + "templateHash": "18109486080685095591" } }, "parameters": { @@ -81,11 +81,10 @@ "description": "PyRIT initializer to run. Default \"target airt\" registers target configs + attack defaults." } }, - "envSecretName": { - "type": "string", - "defaultValue": "env-global", + "envFileContents": { + "type": "securestring", "metadata": { - "description": "Key Vault secret name containing the .env file contents (all endpoints, models, and API keys). The secret is mounted as an env var and PyRIT parses it at startup." + "description": "Contents of the .env file (endpoints, models, API keys). Passed inline to the Container App as a secure secret. Marked @secure() so the value is stripped from deployment history." } }, "cpuCores": { @@ -429,8 +428,7 @@ "secrets": [ { "name": "env-file", - "keyVaultUrl": "[format('https://{0}{1}/secrets/{2}', variables('keyVaultName'), environment().suffixes.keyvaultDns, parameters('envSecretName'))]", - "identity": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-identity', parameters('appName')))]" + "value": "[parameters('envFileContents')]" } ] }, @@ -487,6 +485,10 @@ { "name": "AZURE_CLIENT_ID", "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-identity', parameters('appName'))), '2023-01-31').clientId]" + }, + { + "name": "PYRIT_CORS_ORIGINS", + "value": "[format('https://{0}.{1}', parameters('appName'), reference(resourceId('Microsoft.App/managedEnvironments', format('{0}-env', parameters('appName'))), '2024-10-02-preview').defaultDomain)]" } ] } diff --git a/infra/parameters.demo.json b/infra/parameters.demo.json index 65fdf85766..bc6e34753f 100644 --- a/infra/parameters.demo.json +++ b/infra/parameters.demo.json @@ -1,7 +1,8 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", - "_usage": "This template is for manual 'az deployment group create' runs. The deploy_instance.py script passes parameters directly via CLI args and does not use this file.", + "_usage": "This template is for manual 'az deployment group create' runs. The deploy_instance.py script generates its own temp parameters file at runtime and does not use this file.", + "_envFileContents_warning": "envFileContents is a @secure() parameter — do NOT commit a real .env value to this file. Pass it at deploy time via 'az deployment group create --parameters envFileContents=@./my.env' or via a CI/CD secure variable. The deploy_instance.py script handles this automatically by writing a temp parameters file. WARNING: Azure CLI silently falls back to the literal string if the @file path is wrong — always run 'test -f ./my.env' (bash) or 'Test-Path .\\my.env' (PowerShell) first.", "parameters": { "appName": { "value": "copyrit-INSTANCE_NAME" @@ -30,8 +31,8 @@ "pyritInitializer": { "value": "target airt" }, - "envSecretName": { - "value": "env-global" + "envFileContents": { + "value": "PYRIT_REPLACE_ME=set-via-secure-channel" }, "logRetentionDays": { "value": 90 diff --git a/infra/parameters.example.json b/infra/parameters.example.json index e82a5a0e0a..ac2bfdb6eb 100644 --- a/infra/parameters.example.json +++ b/infra/parameters.example.json @@ -1,6 +1,8 @@ { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", + "_envFileContents_warning": "envFileContents is a @secure() parameter — do NOT commit a real .env value to this file. Pass it at deploy time via 'az deployment group create --parameters envFileContents=@./my.env' or via a CI/CD secure variable that writes a parameters file at runtime. WARNING: Azure CLI silently falls back to the literal string if the @file path is wrong — always run 'test -f ./my.env' (bash) or 'Test-Path .\\my.env' (PowerShell) first. A typo would store '@./typoed.env' as the secret value and the container would fail at startup.", + "_optional_params_note": "Below the required block, infrastructureSubnetId / acrName / acrResourceId / logAnalyticsWorkspaceId / logAnalyticsCustomerId / logAnalyticsSharedKey are optional — omit to let the template create resources.", "parameters": { "appName": { "value": "pyrit-gui" @@ -29,8 +31,8 @@ "pyritInitializer": { "value": "target airt" }, - "envSecretName": { - "value": "env-global" + "envFileContents": { + "value": "PYRIT_REPLACE_ME=set-via-secure-channel" }, "logRetentionDays": { "value": 90 @@ -46,8 +48,6 @@ } }, - "_comment_optional": "--- Below are optional: omit to let the template create resources ---", - "infrastructureSubnetId": { "value": "" },