Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions docs/ci-cd/authentication-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,31 @@ apiops-cli needs these Azure RBAC roles on the APIM service instance:
| Role | Scope | Purpose |
|------|-------|---------|
| **API Management Service Contributor** | APIM service resource | Read and write APIM resources (APIs, products, policies, etc.) |
| **Reader** | Resource group | List resources in the resource group |

**Reader** on the resource group is not required. Before publishing, `apiops publish` checks that the resource group exists so it can report a missing resource group clearly. If the identity cannot read the resource group (HTTP 403), the CLI skips that check and checks the APIM service directly.

### Key Vault pre-flight check

When a named value references a Key Vault secret (`keyVault.secretIdentifier`), `apiops publish` first checks that the APIM managed identity can read that secret. It uses the CLI's identity to make these ARM reads:

| Read | Why | Permission | Covered by |
|------|-----|------------|------------|
| The APIM service | Find its managed identity | `Microsoft.ApiManagement/service/read` | API Management Service Contributor on the APIM service |
| Resources in the APIM service's subscription, filtered to Key Vaults with the vault's name | Find the vault | Read on the vault; the [Resources - List](https://learn.microsoft.com/rest/api/resources/resources/list) API documents no specific permission, or what it returns when access is narrower than the subscription | Reader on the subscription |
| The Key Vault | See whether it uses RBAC or access policies | `Microsoft.KeyVault/vaults/read` | Reader on the vault |
| RBAC vaults: role assignments for the APIM identity on the vault | Find its roles | `Microsoft.Authorization/roleAssignments/read` on the vault | Reader on the vault |
| RBAC vaults with custom roles: the role definitions | Check their data actions | `Microsoft.Authorization/roleDefinitions/read` on the role definition's scope, usually the subscription | Reader on the subscription |

Reader on the subscription that contains the APIM service and the vault covers all of these reads. With narrower access, a failed read (for example HTTP 403) of the APIM service, the vault search, the vault or its role assignments, or a vault that isn't found, makes the CLI log a warning and skip the check; the named value is still published. The reads go to the ARM endpoint of the cloud selected with `--cloud`.

The check fails the named value when:

- the secret identifier is not a valid Key Vault secret URI;
- the APIM service has no managed identity (or not the user-assigned identity named by `keyVault.identityClientId`);
- on an RBAC vault, no role assignment it can see grants the identity secret read access: the identity has no role assignments on the vault, or none of its roles includes the `Microsoft.KeyVault/vaults/secrets/getSecret/action` data action (or a wildcard that covers it). A custom role definition that can't be read is skipped, so when no other role grants the data action, the check fails;
- on a vault that uses access policies, no access policy it can see grants the identity `get` (or `all`) on secrets, including when the identity has no access policy.

Known limitation: the check sees only role assignments and access policies made to the APIM identity itself, not those made to a group it belongs to (an access policy can name a group, see [Assign a Key Vault access policy](https://learn.microsoft.com/azure/key-vault/general/assign-access-policy)). An identity whose access comes only through a group fails the check.

### Assign RBAC

Expand All @@ -315,11 +339,6 @@ az role assignment create \
--assignee {principal-id} \
--role "API Management Service Contributor" \
--scope "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.ApiManagement/service/{apim-name}"

az role assignment create \
--assignee {principal-id} \
--role "Reader" \
--scope "/subscriptions/{sub-id}/resourceGroups/{rg}"
```

> **Principle of least privilege:** Scope the Contributor role to the specific APIM instance, not the entire resource group or subscription.
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/environment-overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,8 @@ If you add a new backend in dev but forget to add it to your override files, pub

When using Key Vault references, the APIM managed identity needs access to the Key Vault. A common failure mode: overrides reference a Key Vault but APIM lacks the `Key Vault Secrets User` role on that vault.

`apiops publish` checks this before publishing the named value. The check reads the vault and its role assignments with the CLI's own identity (Reader on the subscription covers every read it makes); if it can't read the APIM service, find or read the vault, or list the vault's role assignments, it logs a warning and is skipped. It fails the named value when no role assignment (on an RBAC vault) or access policy (on a vault that uses access policies) it can see grants the APIM identity secret read access, including when the identity has none; a custom role definition it can't read is skipped, which can lead to that failure. Known limitation: it sees only grants made to the APIM identity itself, not through a group it belongs to, so access granted only through a group fails the check. See [Key Vault pre-flight check](../ci-cd/authentication-patterns.md#key-vault-pre-flight-check).

### Gotcha: Auto-generated named values for loggers

When you create a logger in APIM (e.g., for Application Insights), APIM auto-generates a named value to store the credential. These have 24-character hex names (e.g., `<24-char-hex-id>`). During extract, these are captured as artifacts. During publish:
Expand Down
6 changes: 4 additions & 2 deletions docs/troubleshooting/common-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ See [Pipeline Recovery](pipeline-recovery.md) for CI/CD-specific recovery steps.

**Solution:**

- The CLI automatically retries on 429 responses with the delay specified in the `Retry-After` header
- The CLI automatically retries a 429 response up to 3 times. Like the Azure SDK, it waits for the `retry-after-ms`, `x-ms-retry-after-ms` or `Retry-After` header (seconds or an HTTP date); `0` or a date already passed retries at once. When none of these headers is present or readable, it waits 1, 2 and then 4 seconds instead. A 503 that sends one of these headers is retried after that wait too
- If retries are exhausted, reduce the number of resources being processed (use filters for extract or incremental publish)
- Wait a few minutes and re-run

Expand All @@ -208,7 +208,9 @@ See [Pipeline Recovery](pipeline-recovery.md) for CI/CD-specific recovery steps.
**Solution:**

1. Test connectivity: `curl -I https://management.azure.com`
2. If behind a proxy, set `HTTPS_PROXY` environment variable
2. If behind a proxy, set the `HTTPS_PROXY` environment variable. The CLI reads the proxy settings the way `@azure/identity` reads them for its sign-in requests: every request uses the first of `HTTPS_PROXY`, `ALL_PROXY` and `HTTP_PROXY` that is set (each name in upper case before lower case), and a user name and password in the proxy URL are sent to the proxy
- For hosts that must bypass the proxy, set `NO_PROXY` to a comma-separated list of exact host names (`management.azure.com`) or domains with a leading dot (`.azure.com`, which also matches `azure.com`). Other forms, such as `*` or `azure.com` for its subdomains, are not supported
- When you sign in with the Azure CLI, Azure Developer CLI or Azure PowerShell, that tool requests the token itself, using its own proxy settings
3. If using private endpoints, ensure you're on the correct network

---
Expand Down
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"commander": "^14.0.3",
"he": "^1.2.0",
"js-yaml": "^4.1.1",
"simple-git": "^3.35.2"
"simple-git": "^3.35.2",
"undici": "^7.30.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
4 changes: 4 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { Command, Option } from 'commander';
import { logger, parseLogLevel } from '../lib/logger.js';
import { applyProxyFromEnv } from '../lib/proxy.js';
import { createExtractCommand } from './extract-command.js';
import { createPublishCommand } from './publish-command.js';
import { createInitCommand } from './init-command.js';
Expand Down Expand Up @@ -52,6 +53,9 @@ program.hook('preAction', (thisCommand) => {

logger.configure({ level: parseLogLevel(opts.logLevel ?? 'info') });

// Send ARM requests through HTTPS_PROXY / HTTP_PROXY when set.
applyProxyFromEnv();

// Set DefaultAzureCredential env vars from explicit auth flags.
// This ensures the ServicePrincipal credential is tried first,
// avoiding interactive browser prompts in CI/CD pipelines.
Expand Down
1 change: 1 addition & 0 deletions src/cli/publish-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ async function executePublish(
// Build publish config
const publishConfig: PublishConfig = {
service: context,
cloud: cloudConfig,
sourceDir: options.source,
filter: filterConfig,
includeTransitive: options.transitive,
Expand Down
Loading