Files
The-transit-Project/.github/agents/azure-deploy/scaffold/references/bicep-patterns-security.md
T
Sven laptop 635a130f0b
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
──────
### Summary of Changes
  1. Documentation Consolidation:
      • Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
      DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
      SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
      • Updated document links in README.md.
  2. AI Agent Project Map:
      • Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
      schemas, environment variables, and developer workflows.
      • Added PROJECT_MAP.md to .gitignore.
  3. Monitoring UI & Infrastructure Integration:
      • Added monitoring_ui service to docker-compose.yml (port 5175).
      • Configured Gitea workflow docker-build.yml for monitoring_ui builds.
      • Added CORS middleware headers in index.js.
      • Added nginx.conf copy step in Dockerfile.

  ──────
  ### Recommended Commit Message

    docs: consolidate project documentation into monitoring_ui and add agent project map

    - Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
    - Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
    - Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
    - Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
    - Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
    - Configure CORS handling in employee_api for monitoring dashboard access
    ──────
  ### Alternative Short Commit Message

    refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
2026-09-20 23:33:55 +02:00

12 KiB

Important: This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.

Bicep Patterns — Security Defaults

Mandatory security configuration for all AppOnboard-generated Bicep. Read during IaC generation before writing resource definitions. Apply during scaffold — never defer to deploy.

For core patterns (file structure, skeleton, naming, tagging), see bicep-patterns.md. For data module templates (PostgreSQL, Redis), see subagent-iac-gen.md Step 6.

No Key Vault — App-Internal Secret Storage

⛔ Do NOT create a Key Vault. AppOnboard-generated IaC MUST NOT emit Microsoft.KeyVault/vaults (or any KV secret / KV RBAC resource). Azure resource auth (database, cache, storage, Cosmos, queues) is managed-identity + token — there is nothing to store. App-internal secrets that are NOT an Azure resource credential (e.g. SECRET_KEY, JWT signing key, third-party API keys) are stored directly on the compute resource, never in Key Vault:

  • App Service / Functions: pass each secret as an @secure() Bicep param → siteConfig.appSettings. The platform encrypts app settings at rest. The value is generated at deploy time and passed via CLI (never committed); it does not appear in ARM deployment history because it's @secure().
  • Container Apps: pass each secret as an @secure() param → the Container App's native secrets: [{ name, value }] array → referenced by secretRef. This is the Container Apps secret store, NOT Key Vault (keyVaultUrl is never used).

⛔ No @Microsoft.KeyVault(...) references, no keyVaultUrl secretRefs, no KV role assignments (Key Vault Secrets Officer/Secrets User), no deployerObjectId KV param.

Security Defaults

Source: Adapted from Azure security best practices. See Azure security baseline for updates.

Identity — Managed Identity Everywhere (MANDATORY, NO EXCEPTIONS)

⛔ Every compute resource MUST have a managed identity. There is NO SKU, tier, or service combination that is exempt. Managed identity is the ONLY sanctioned way for app code to authenticate to any Azure resource. Shared keys, admin logins, and connection-string passwords are BLOCKED (see bicep-patterns-data.md and the conformance gate).

Condition Include MI?
Any compute (App Service, Container Apps, Functions) YES — always
Any database, storage, queue, cache, Cosmos, or ACR access YES — always
None of the above YES (default secure)
  • System-assigned managed identity for all services (default). User-assigned only when shared identity is explicitly needed.
  • ⛔ Compute floor is B1 (App Service) / Standard (Static Web Apps). F1/D1/Free tiers are NOT offered — the free-tier MI sidecar OOMs and cannot host managed identity, and free SWA lacks the config (CORS, custom auth) these apps need. SKU selection MUST NOT emit F1, D1, or Free. See sku-matrix.md.
  • ⛔ Never generate administratorLogin, administratorLoginPassword, access keys, or connection-string passwords for ANY data service (SQL, PostgreSQL, MySQL, Redis, Storage, Cosmos DB, Service Bus, Event Hubs) — including inside conditional branches. Use Entra/MI-only auth (see the data-service patterns below and in bicep-patterns-data.md).
  • App-to-service auth: managed identity + RBAC role assignments (or data-plane role assignments for Cosmos/Redis). Zero shared-key or password auth to any Azure resource.
  • App-internal secrets that are NOT an Azure resource credential (e.g. Django SECRET_KEY, JWT signing key, third-party API keys) are stored directly on the compute resource — App Service app settings or Container Apps native secrets, from an @secure() param generated at deploy. ⛔ No Key Vault (see § No Key Vault — App-Internal Secret Storage).
identity: {
  type: 'SystemAssigned'
}

Data Services — Entra / Managed-Identity-Only Authentication

⛔ Local/shared-key/password authentication MUST be disabled on every data service. The app authenticates with its managed identity; the deploying principal is granted an Entra admin/data role so migrations and seeding can run token-based. Full module templates (auth config, Entra admin child resources, MI data-plane roles) live in bicep-patterns-data.md.

Service How local auth is disabled App/MI access
Azure SQL azureADOnlyAuthentication: true (pattern below) Entra admin + CREATE USER [<mi>] FROM EXTERNAL PROVIDER
PostgreSQL Flexible authConfig: { passwordAuth: 'Disabled', activeDirectoryAuth: 'Enabled' } Entra admin child resource + MI role via pgaadauth_create_principal
MySQL Flexible Entra admin child resource, aad_auth_only config MI mapped as an AAD login
Redis disableAccessKeyAuthentication: true + accessPolicyAssignments Data Owner/Contributor access policy to the MI
Storage allowSharedKeyAccess: false Storage Blob/Queue Data role to the MI
Cosmos DB disableLocalAuth: true sqlRoleAssignments (data plane) — see rbac-roles.md
Service Bus / Event Hubs disableLocalAuth: true Data Sender/Receiver role to the MI

SQL Server — Entra-Only Authentication

param principalId string
param principalName string
@allowed(['User', 'Group', 'Application'])
param principalType string = 'User'

// Preview API required — azureADOnlyAuthentication via administrators block
// is not available in GA API versions (GA path uses a separate child resource).
resource sqlServer 'Microsoft.Sql/servers@2024-05-01-preview' = {
  name: '${resourcePrefix}-sql-${uniqueHash}'
  location: location
  properties: {
    administrators: {
      administratorType: 'ActiveDirectory'
      principalType: principalType
      login: principalName
      sid: principalId
      tenantId: subscription().tenantId
      azureADOnlyAuthentication: true
    }
    minimalTlsVersion: '1.2'
  }
}

⚠️ If deploying from CI/CD with a service principal, set principalType to 'Application'. The default 'User' only works for interactive deployments.

Secrets — App-Internal, Stored On-Compute (No Key Vault)

App-internal secrets (e.g. SECRET_KEY, JWT_SECRET, API_KEY, session secrets) are stored directly on the compute resource. ⛔ No Key Vault, no @Microsoft.KeyVault(...), no keyVaultUrl.

⛔ No plaintext/committed secrets. Values MUST NOT be hardcoded in Bicep or committed to main.parameters.json — pass them as @secure() params generated at deploy time. Never use uniqueString() for secrets (deterministic/predictable). @secure() params do NOT appear in ARM deployment history.

❌ NEVER: { name: 'SECRET_KEY', value: 'hard-to-guess-string' } or value: 'change-me'; any @secure() password param for a database/cache/storage account (those use managed identity — see § Data Services).

// App Service / Functions
@secure()
param secretKey string   // generated at deploy time, passed via CLI --parameters, never committed

appSettings: [
  // Database via managed identity — plain connection params, NO password:
  { name: 'PGHOST', value: '${pgName}.postgres.database.azure.com' }
  { name: 'PGDATABASE', value: appDbName }
  { name: 'PGUSER', value: appServiceName }   // the MI's DB principal name
  { name: 'PGSSLMODE', value: 'require' }
  // App-internal secret stored directly as an app setting (platform-encrypted at rest):
  { name: 'SECRET_KEY', value: secretKey }
]

For Container Apps, put the secret in the Container App's native secrets: [{ name, value: secretKey }] array (value from an @secure() param) and reference it via secretRef — see bicep-container-apps.md. ⛔ Never emit a Microsoft.KeyVault/vaults resource.

App Settings Are Non-Secret Endpoint Configuration Only

⛔ App settings / container env MUST NOT contain any secret or connection string. With managed identity the app authenticates by token — there is nothing secret to configure. App settings carry ONLY non-secret endpoint configuration that tells the app where and as which identity to connect:

  • Endpoint / host — e.g. PGHOST=myserver.postgres.database.azure.com, AZURE_STORAGE_BLOB_ENDPOINT=https://myacct.blob.core.windows.net
  • Resource name — e.g. database name, container/queue name
  • Identity principal — the MI's DB principal name as the connection user (Postgres/MySQL AAD auth)
  • AZURE_CLIENT_ID — ONLY for a user-assigned MI, so DefaultAzureCredential selects the right identity (omit for system-assigned)
  • Non-secret app flags — e.g. NODE_ENV, PGSSLMODE=require, CORS origins

❌ NEVER put in app settings / env: a full connection string (DATABASE_URL=postgres://user:pass@host/db, AccountKey=..., SharedAccessKey=...), a password, an access key, a SAS token, or any other credential. If a value would authenticate you, it does not belong here — use managed identity instead. The ONLY secret-like value permitted anywhere is a genuinely app-internal secret (e.g. SECRET_KEY) that is NOT an Azure resource credential, and even that is passed as an @secure() param, never as a plain app-setting literal.

Transport — HTTPS Only

All web-facing resources:

// App Service
httpsOnly: true
siteConfig: {
  minTlsVersion: '1.2'
}

// Storage
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'

App Service / Premium Functions — Publishing Credential Lockdown

⛔ Every App Service and every Premium (EP1) Functions app MUST include both basicPublishingCredentialsPolicies child resources. Missing these means deploy cannot toggle SCM auth post-deployment — the REST API call targets a resource that doesn't exist in ARM.

⛔ Exception — Functions on Flex Consumption (the default Functions floor): do NOT add these policies. Flex has no SCM basic-auth publishing model; it deploys through its deployment storage container. Adding scm/ftp basicPublishingCredentialsPolicies to a Flex site is wrong, and the deploy phase must not toggle them. See bicep-functions-flex.md.

// SCM — allow: true for deploy phase (deploy re-disables via REST API after code upload)
resource scmAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
  parent: appService
  name: 'scm'
  properties: {
    allow: true
  }
}

// FTP — always disabled
resource ftpAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
  parent: appService
  name: 'ftp'
  properties: {
    allow: false
  }
}

Deploy lifecycle: Scaffold sets scm.allow: true so az webapp deploy works. After code upload + health check, deploy phase runs az rest --method put .../basicPublishingCredentialsPolicies/scm with allow: false to re-harden. If scaffold omits these resources, deploy's Step 7 SCM re-disable REST API call fails silently.

Cosmos DB — Data Plane RBAC

⛔ Cosmos DB uses its own role system — see rbac-roles.md § Cosmos DB for role IDs and behavioral rules. Do NOT use Microsoft.Authorization/roleAssignments for Cosmos data access.

RBAC — Deterministic Role Assignments

For the common roles GUID table, see rbac-roles.md.

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(scopeResourceId, principalId, roleDefinitionId)
  scope: targetResource
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
    principalId: managedIdentity.properties.principalId
    principalType: 'ServicePrincipal'  // REQUIRED — prevents AAD graph lookup delays
  }
}