diff --git a/docs/POC-ACA-Proxy-Security-Authorization.md b/docs/POC-ACA-Proxy-Security-Authorization.md new file mode 100644 index 0000000..07920a8 --- /dev/null +++ b/docs/POC-ACA-Proxy-Security-Authorization.md @@ -0,0 +1,201 @@ +# POC: ACA Proxy Security and Authorization + +**Purpose:** Validate inbound OAuth 2.0 authentication and authorization for the SimpleL7Proxy Container App (ACA), including Entra app registration setup, ACA auth configuration, and caller validation. + +## TL;DR (< 5 minutes) + +1. **Most important rule: tokens sent to ACA must have `aud = api://`.** +2. Create two Entra apps for this hop: ACA API app and client caller app. +3. Enable ACA authentication and test both success and failure paths. + +## What you will observe + +- A token minted for `api://` is accepted by ACA. +- Requests without a token or with the wrong audience are rejected. +- The client identity can be restricted through allowed applications and app role assignment. + +## Reference + +| Setting | Value in this POC | Unit | Set in | Takes effect | +| :--- | :--- | :--- | :--- | :--- | +| ACA API app registration | `SimpleL7Proxy-ACA-API` | name | Entra App registrations | immediate | +| Client app registration | `SimpleL7Proxy-Client` | name | Entra App registrations | immediate | +| ACA audience | `api://` | URI | ACA auth config | config save | +| ACA scope | `api.access` | scope value | ACA API app | token issuance | +| App role value | `API.Caller` | claim value | ACA API app | token issuance | +| Client secret requirement | ACA API app: `Yes` when used in ACA auth config; Client app: `Yes` for client credentials flow | flag | Entra App registrations | immediate | + +> [!NOTE] +> Units used in this doc: IDs are GUIDs, audience values are URI strings, and roles/scopes are string claims. + +## Setup + +### 1) Create ACA API app registration in Entra + +**Rule: the ACA proxy must expose its own audience and scope for inbound client tokens.** + +```text +Name: SimpleL7Proxy-ACA-API +Application ID URI: api:// +Scope: api.access +App role: API.Caller +``` + +Portal steps (repo-aligned): + +1. Entra ID -> App registrations -> New registration. +2. Name: `SimpleL7Proxy-ACA-API`. +3. Expose an API -> Set Application ID URI -> `api://`. +4. Expose an API -> Add scope: + - Scope value: `api.access` + - Who can consent: `Admins only` + - Admin consent display name: `Admin Access` + - Admin consent description: `Access the API` + - State: `Enabled` +5. App roles -> Create app role: + - Display name: `Caller` + - Allowed member types: `Users/Groups` and `Applications` + - Value: `API.Caller` + - Enable app role: `Yes` +6. Enterprise applications -> corresponding service principal -> set assignment required to `Yes`. +7. Save IDs: + - `ACA_APP_ID` + - `ACA_API_SERVICE_PRINCIPAL_OBJECT_ID` + - `ACA_API_CALLER_ROLE_ID` + +### 2) Create client app registration in Entra + +**Rule: the caller must be able to request a token for ACA scope and present it to ACA.** + +```text +Name: SimpleL7Proxy-Client +Permission: ACA API delegated permission api.access +Credential: client secret (or certificate) +``` + +Portal steps: + +1. Entra ID -> App registrations -> New registration. +2. Name: `SimpleL7Proxy-Client`. +3. API permissions -> Add permission -> My APIs -> `SimpleL7Proxy-ACA-API`. +4. Add delegated permission `api.access`. +5. Grant admin consent if required by tenant policy. +6. Certificates and secrets -> New client secret. +7. Save IDs/secrets: + - `CLIENT_APP_ID` + - `CLIENT_SERVICE_PRINCIPAL_OBJECT_ID` + - `CLIENT_SECRET` + +### 3) Configure ACA authentication + +**Rule: ACA auth must validate the ACA audience and only allow authorized caller applications.** + +Script-aligned configuration using repo flow: + +```bash +./scripts/enableContainerAppAuth.sh \ + -g \ + -n \ + -t \ + -c \ + -s \ + -a +``` + +Equivalent portal checks: + +1. Container Apps -> your app -> Authentication. +2. Identity provider: Microsoft. +3. Tenant: ``. +4. Client ID: ``. +5. Client secret: ACA app secret. +6. Allowed token audiences includes `api://`. +7. Allowed applications includes ``. +8. Unauthenticated requests set to `401`/reject. + +> [!WARNING] +> Use tenant ID for `-t`; do not use a service principal object ID in this field. + +### 4) Optional app-role assignment for client service principal + +**Rule: if you enforce role-based app auth, assign `API.Caller` to the client service principal on ACA API.** + +```powershell +Connect-MgGraph -TenantId "" -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" +$clientSpId = "" +$acaResourceSpId = "" +$acaRoleId = "" +New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $clientSpId -PrincipalId $clientSpId -ResourceId $acaResourceSpId -AppRoleId $acaRoleId +``` + +## Full flow + +```mermaid +flowchart LR + A["Client App
SimpleL7Proxy-Client"] -->|"Bearer token aud=api://ACA_APP_ID"| B["ACA Ingress + Easy Auth"] + B -->|"Validate audience + app constraints"| C["SimpleL7Proxy in ACA"] + D["ACA API App
SimpleL7Proxy-ACA-API"] -. "Defines audience, scope, and optional role" .-> B +``` + +## Worked example + +| Step | Example value | Result | +| :--- | :--- | :--- | +| Create ACA API app | `appId = 22222222-2222-2222-2222-222222222222` | ACA audience is `api://22222222-2222-2222-2222-222222222222` | +| Create client app | `appId = 33333333-3333-3333-3333-333333333333` | Client can request ACA token | +| Enable ACA auth | Allowed audience set to ACA URI | ACA validates incoming bearer tokens | +| Send valid token | `aud` matches ACA URI | Request succeeds | +| Send token with wrong audience | `aud` is APIM URI | Request fails with `401` | + +## Test ACA authorization + +**Rule: run one success test and two failure tests to validate auth controls.** + +Set variables: + +```bash +ACA_FQDN="https://" +ACA_RESOURCE="api://" +``` + +Positive test: + +```bash +TOKEN="$(az account get-access-token --resource "$ACA_RESOURCE" --query accessToken -o tsv)" +curl -i "$ACA_FQDN/health" -H "Authorization: Bearer $TOKEN" +``` + +Expected: success response from ACA. + +Negative test (no token): + +```bash +curl -i "$ACA_FQDN/health" +``` + +Expected: `401 Unauthorized`. + +Negative test (wrong audience token): + +```bash +BAD_TOKEN="$(az account get-access-token --resource api:// --query accessToken -o tsv)" +curl -i "$ACA_FQDN/health" -H "Authorization: Bearer $BAD_TOKEN" +``` + +Expected: `401 Unauthorized` due to audience mismatch. + +## Verify + +- [ ] ACA API app exists with `api://` identifier URI. +- [ ] ACA API app exposes scope `api.access`. +- [ ] Client app has delegated permission to `api.access`. +- [ ] ACA auth is enabled and allowed audience includes ACA URI. +- [ ] Allowed applications includes client app ID. +- [ ] Valid ACA token succeeds; missing/wrong-audience token fails. + +## Related docs + +- [scripts/README.md](../scripts/README.md) +- [scripts/console2caSetup.sh](../scripts/console2caSetup.sh) +- [scripts/enableContainerAppAuth.sh](../scripts/enableContainerAppAuth.sh) +- [POC-APIM-Security-Authorization.md](POC-APIM-Security-Authorization.md) diff --git a/docs/POC-APIM-Security-Authorization.md b/docs/POC-APIM-Security-Authorization.md new file mode 100644 index 0000000..f4a8b82 --- /dev/null +++ b/docs/POC-APIM-Security-Authorization.md @@ -0,0 +1,257 @@ +# POC: APIM Security and Authorization + +**Purpose:** Validate OAuth 2.0 authentication and authorization at APIM for calls coming from ACA, including Entra app registration, app-role assignment, APIM OAuth interface setup, and `validate-jwt` enforcement. + +## TL;DR (< 5 minutes) + +1. **Most important rule: APIM must only accept tokens with `aud = api://` and `roles = API.Caller`.** +2. Create an APIM API app registration and assign `API.Caller` to ACA managed identity. +3. Configure APIM `validate-jwt` and test both success and failure paths. + +## What you will observe + +- APIM accepts calls with valid bearer tokens minted for APIM audience and role. +- APIM rejects calls with no token, wrong audience, or missing role. +- OAuth server config in APIM enables interactive authorizing, while policy still enforces acceptance. + +## Reference + +| Setting | Value in this POC | Unit | Set in | Takes effect | +| :--- | :--- | :--- | :--- | :--- | +| APIM API app registration | `SimpleL7Proxy-APIM-API` | name | Entra App registrations | immediate | +| App role value | `API.Caller` | claim value | APIM API app | token issuance | +| APIM audience | `api://` | URI | APIM `validate-jwt` policy | policy save | +| ACA caller identity | ACA managed identity service principal | principal | ACA + Entra | immediate | +| OAuth server client secret | secret from client app used in APIM OAuth server UI | secret | Entra + APIM | save | + +> [!NOTE] +> Units used in this doc: IDs are GUIDs and audience values are URI strings. + +## Setup + +### 0) Enable system-assigned managed identity on ACA + +**Rule: ACA managed identity `principalId` is the identity that must receive `API.Caller` on the APIM API app.** + +```bash +RG="" +CA_NAME="" + +az containerapp identity assign -g "$RG" -n "$CA_NAME" --system-assigned + +ACA_MANAGED_IDENTITY_OBJECT_ID="$(az containerapp show -g "$RG" -n "$CA_NAME" --query "identity.principalId" -o tsv)" +echo "$ACA_MANAGED_IDENTITY_OBJECT_ID" +``` + +### 1) Create APIM API app registration in Entra + +**Rule: APIM policy audience must match this app's identifier URI.** + +```text +Name: SimpleL7Proxy-APIM-API +Application ID URI: api:// +App role: API.Caller +``` + +Portal steps (repo-aligned): + +1. Entra ID -> App registrations -> New registration. +2. Name: `SimpleL7Proxy-APIM-API`. +3. Expose an API -> Set Application ID URI -> `api://`. +4. App roles -> Create app role: + - Display name: `Caller` + - Allowed member types: `Users/Groups` and `Applications` + - Value: `API.Caller` + - Enable app role: `Yes` +5. Enterprise applications -> corresponding service principal -> set assignment required to `Yes`. +6. Save IDs: + - `APIM_APP_ID` + - `APIM_API_SERVICE_PRINCIPAL_OBJECT_ID` + - `APIM_API_CALLER_ROLE_ID` + +### 2) Assign ACA managed identity to APIM role + +**Rule: ACA managed identity must have `API.Caller` role on APIM API enterprise app.** + +```powershell +Connect-MgGraph -TenantId "" -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" +$acaSpId = "" +$apimResourceSpId = "" +$apimRoleId = "" +New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $acaSpId -PrincipalId $acaSpId -ResourceId $apimResourceSpId -AppRoleId $apimRoleId +``` + +> [!WARNING] +> Use service principal object IDs from Enterprise applications, not app registration app IDs. + +### 3) Configure APIM inbound `validate-jwt` policy + +**Rule: APIM must validate issuer, audience, and role before backend routing.** + +```xml + + + + + + api:// + + + + https://sts.windows.net// + https://login.microsoftonline.com//v2.0 + + + + API.Caller + + + + +``` + +> [!TIP] +> If your APIM endpoint accepts delegated user tokens, validate `scp` instead of `roles`. + +> [!NOTE] +> Match the issuer to the token you actually receive. Depending on flow and token version, `iss` may be `https://sts.windows.net//` or `https://login.microsoftonline.com//v2.0`. + +### 4) Configure OAuth 2.0 in APIM interface + +**Rule: OAuth server settings support authorize/testing UX, while `validate-jwt` policy remains the true gate.** + +1. Azure portal -> APIM instance. +2. Security -> OAuth 2.0 + OpenID Connect -> Add OAuth 2.0 server. +3. Configure: + - Display name: `EntraOAuth` + - Grant types: `Authorization code` and optionally `Client credentials` + - Client ID: `` + - Client secret: `` + - Authorization endpoint: `https://login.microsoftonline.com//oauth2/v2.0/authorize` + - Token endpoint: `https://login.microsoftonline.com//oauth2/v2.0/token` + - Default scope: `api:///.default` (or your API scope) +4. Save. +5. API -> Settings -> attach OAuth server if Developer Portal Authorize is needed. +6. API -> Design -> verify `validate-jwt` is present in inbound policy. + +## Full flow + +```mermaid +flowchart LR + A["ACA Managed Identity"] -->|"Bearer token aud=api://APIM_APP_ID/.default"| B["APIM validate-jwt"] + B -->|"Checks issuer + audience + roles=API.Caller"| C["APIM backend routing"] + D["APIM API App
SimpleL7Proxy-APIM-API"] -. "Defines audience and API.Caller role" .-> B +``` + +## Worked example + +| Step | Example value | Result | +| :--- | :--- | :--- | +| Create APIM API app | `appId = 11111111-1111-1111-1111-111111111111` | APIM audience is `api://11111111-1111-1111-1111-111111111111` | +| Assign ACA managed identity role | `API.Caller` granted | ACA token can satisfy APIM role check | +| Apply validate-jwt policy | audience + role checks active | Unauthorized tokens are blocked | +| Send valid APIM token | `aud` and `roles` match | Request succeeds | +| Send ACA audience token | `aud` mismatch | Request fails with `401` | + +## Test APIM authorization policy + +**Rule: run one positive and three negative tests to confirm policy enforcement.** + +Set variables: + +```bash +APIM_BASE="https://.azure-api.net/" +APIM_SUB_KEY="" +APIM_APP_ID="" +``` + +Positive test: + +```bash +TOKEN="$(az account get-access-token --resource "api://$APIM_APP_ID" --query accessToken -o tsv)" +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $TOKEN" +``` + +Expected: success response. + +Negative test (no token): + +```bash +curl -i "$APIM_BASE/health" -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" +``` + +Expected: `401 Unauthorized`. + +Negative test (wrong audience): + +```bash +BAD_TOKEN="$(az account get-access-token --resource "https://management.azure.com/" --query accessToken -o tsv)" +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $BAD_TOKEN" +``` + +Expected: `401 Unauthorized` due to audience mismatch. + +Negative test (missing role): + +```bash +NO_ROLE_TOKEN="" +curl -i "$APIM_BASE/health" \ + -H "Ocp-Apim-Subscription-Key: $APIM_SUB_KEY" \ + -H "Authorization: Bearer $NO_ROLE_TOKEN" +``` + +Expected: `401 Unauthorized` due to missing required claim. + +> [!NOTE] +> Role assignment changes are not retroactive to already-issued tokens. If you revoke a role, old tokens may continue to work until token expiry. + +## Optional: proxy -> APIM with managed identity + +**Rule: when proxy host config uses `usemi=true`, set `audience` so ACA requests the correct APIM token.** + +```bash +Host1="host=https://.azure-api.net;usemi=true;audience=api://;probe=/health" +``` + +Runtime behavior: + +- Proxy acquires a managed identity token for `api://`. +- Proxy forwards requests to APIM with `Authorization: Bearer `. +- APIM `validate-jwt` evaluates that token before backend routing. + +> [!WARNING] +> If `usemi=true` and `audience` is missing or incorrect, APIM receives an invalid or missing token and returns `401`. + +## Verify + +- [ ] APIM API app registration exists with identifier URI `api://`. +- [ ] App role `API.Caller` exists and allows `Applications`. +- [ ] ACA managed identity is assigned `API.Caller` on APIM API enterprise app. +- [ ] APIM inbound policy validates issuer, audience, and role. +- [ ] Valid APIM token succeeds. +- [ ] Missing token, wrong audience, and missing role all fail with `401`. + +## Troubleshooting + +| Symptom | Likely cause | Check | +| :--- | :--- | :--- | +| `401` with token that seems valid | Token audience mismatch | Decode token and verify `aud` equals `api://` or configured accepted value | +| `401` with correct audience | Missing `roles` claim | Verify role assignment to ACA managed identity on APIM API service principal | +| `401` only from ACA path but local test succeeds | Wrong ACA principal or missing role assignment | Confirm `identity.principalId` on ACA matches assigned principal | +| `401` with signature or issuer validation issues | Issuer mismatch in policy | Compare token `iss` to policy `` entries | +| Role removed but calls still succeed | Old token still valid | Wait for token expiry, then retest with a new token | + +## Related docs + +- [scripts/README.md](../scripts/README.md) +- [scripts/ca2apimSetup.sh](../scripts/ca2apimSetup.sh) +- [APIM-Policy/readme.md](../APIM-Policy/readme.md) +- [POC-ACA-Proxy-Security-Authorization.md](POC-ACA-Proxy-Security-Authorization.md) diff --git a/docs/POC-Security-OAuth-Configuration.md b/docs/POC-Security-OAuth-Configuration.md new file mode 100644 index 0000000..3250103 --- /dev/null +++ b/docs/POC-Security-OAuth-Configuration.md @@ -0,0 +1,25 @@ +# POC: Security and OAuth 2.0 Configuration (Split Index) + +**Purpose:** This index replaces the previous combined security POC and points to two focused runbooks. + +## TL;DR + +1. **Most important rule: use the doc that matches the security boundary you are configuring.** +2. Use the ACA document for client -> ACA authentication and authorization. +3. Use the APIM document for ACA -> APIM token validation and policy enforcement. + +## Use these documents + +- [POC-ACA-Proxy-Security-Authorization.md](POC-ACA-Proxy-Security-Authorization.md) + - Focus: securing and authorizing the ACA proxy ingress. + - Includes: ACA app registration, client app registration, ACA auth setup, and ACA validation tests. + +- [POC-APIM-Security-Authorization.md](POC-APIM-Security-Authorization.md) + - Focus: securing and authorizing APIM. + - Includes: APIM app registration, ACA managed identity role assignment, APIM OAuth interface setup, `validate-jwt` policy, and APIM policy tests. + +## Why the split + +- ACA and APIM have different enforcement points and token audiences. +- Splitting reduces setup confusion and makes validation steps clearer. +- Each document now has a dedicated diagram, worked example, and verification checklist.