# Azure Setup

Azure authentication, RBAC permissions, and configuration guide.

- **Quick Start:** See [README.md](../README.md)
- **Rules Reference:** See [rules.md](rules.md)
- **CI/CD Integration:** See [ci.md](ci.md)

---

## Quick Setup

Get OIDC running in 4 steps:

```bash
# 1. Create App Registration and Service Principal
az ad app create --display-name "CleanCloudScanner"
az ad sp create --id <APP_ID>

# 2. Add federated identity credential
az ad app federated-credential create \
  --id <APP_ID> \
  --parameters '{
    "name": "CleanCloudGitHub",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<YOUR_ORG>/<YOUR_REPO>:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"]
  }'

# 3. Assign Reader role
az role assignment create \
  --assignee <APP_ID> \
  --role "Reader" \
  --scope /subscriptions/<SUBSCRIPTION_ID>

# 4. Add AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID as GitHub secrets
#    (repo → Settings → Secrets and variables → Actions)
```

> ⚠️ **Common mistake:** The federated credential subject must exactly match your workflow trigger. Branch push, PR, and GitHub Environment each send a different subject claim — using the wrong one causes a silent auth failure (`AADSTS70021`). See [OIDC subject mismatch](#oidc-subject-claim-mismatch).

Full walkthrough → [Azure OIDC setup](#1-azure-oidc-with-workload-identity-recommended-for-cicd)

---

## Authentication Methods

CleanCloud supports multiple Azure authentication methods:

### 1. Azure OIDC with Workload Identity (Recommended for CI/CD)

Microsoft Entra ID Workload Identity Federation — no client secrets, temporary tokens only.

#### Setup Steps

**Step 1: Create App Registration**

```bash
az ad app create --display-name "CleanCloudScanner"
# Note the Application (client) ID
```

**Step 2: Create Service Principal**

```bash
az ad sp create --id <APP_ID>
```

**Step 3: Configure Federated Identity Credential**

Choose the subject format that matches how your GitHub Actions workflow runs:

| Workflow trigger | Subject claim to use |
|---|---|
| Branch push (e.g. `main`) | `repo:<ORG>/<REPO>:ref:refs/heads/main` |
| Pull request | `repo:<ORG>/<REPO>:pull_request` |
| GitHub Environment | `repo:<ORG>/<REPO>:environment:<ENV_NAME>` |

> ⚠️ **Common mistake:** If your workflow uses `environment: production`, GitHub sends the `environment` subject claim — not the `ref` one. Using the wrong format causes silent auth failures. See [OIDC subject mismatch](#oidc-subject-claim-mismatch) in Troubleshooting.

**For branch-based workflows:**

```bash
az ad app federated-credential create \
  --id <APP_ID> \
  --parameters '{
    "name": "CleanCloudGitHub-Branch",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<YOUR_ORG>/<YOUR_REPO>:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"]
  }'
```

**For GitHub Environment workflows:**

```bash
az ad app federated-credential create \
  --id <APP_ID> \
  --parameters '{
    "name": "CleanCloudGitHub-Env",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<YOUR_ORG>/<YOUR_REPO>:environment:<YOUR_ENV_NAME>",
    "audiences": ["api://AzureADTokenExchange"]
  }'
```

> 💡 **Tip:** You can add multiple federated credentials to the same App Registration. If you use both branch pushes and environments, create one credential for each. Azure allows up to 20 federated credentials per App Registration.

**Step 4: Assign Reader Role**

```bash
az role assignment create \
  --assignee <APP_ID> \
  --role "Reader" \
  --scope /subscriptions/<SUBSCRIPTION_ID>
```

**Step 5: Add GitHub Secrets**

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

| Secret | Value |
|---|---|
| `AZURE_CLIENT_ID` | App registration application ID |
| `AZURE_TENANT_ID` | Azure tenant ID |
| `AZURE_SUBSCRIPTION_ID` | Subscription to scan |

No `AZURE_CLIENT_SECRET` needed — OIDC uses federated credentials.

#### Validate Your Setup

Once credentials are configured, verify everything works:

```yaml
permissions:
  id-token: write
  contents: read

jobs:
  cleancloud:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Validate Azure permissions
        run: |
          pip install cleancloud
          cleancloud doctor --provider azure
```

For the complete production workflow with enforcement flags, scheduling, and artifact upload: **[CI/CD guide →](ci.md)**

---

### 2. Service Principal with Environment Variables (Local Development)

Quick setup for local testing and evaluation.

**Step 1: Create Service Principal**

```bash
az ad sp create-for-rbac --name "CleanCloudLocal" --role "Reader" \
  --scopes /subscriptions/<SUBSCRIPTION_ID>
```

This outputs:

```json
{
  "appId": "12345678-1234-1234-1234-123456789abc",
  "displayName": "CleanCloudLocal",
  "password": "your-client-secret",
  "tenant": "87654321-4321-4321-4321-987654321dcb"
}
```

**Step 2: Set Environment Variables**

```bash
export AZURE_CLIENT_ID="12345678-1234-1234-1234-123456789abc"
export AZURE_TENANT_ID="87654321-4321-4321-4321-987654321dcb"
export AZURE_CLIENT_SECRET="your-client-secret"
export AZURE_SUBSCRIPTION_ID="<SUBSCRIPTION_ID>"

cleancloud scan --provider azure
```

> ⚠️ Not recommended for CI/CD — use OIDC (Method 1) to avoid storing secrets.

---

### 3. Azure CLI (Local Development)

Recommended for interactive local development.

```bash
# Login
az login

# Scan all accessible subscriptions (default)
cleancloud scan --provider azure

# Scan specific subscription
cleancloud scan --provider azure --subscription <SUBSCRIPTION_ID>

# Scan multiple subscriptions
cleancloud scan --provider azure \
  --subscription <SUBSCRIPTION_ID_1> \
  --subscription <SUBSCRIPTION_ID_2>
```

CleanCloud automatically uses your active Azure CLI session.

---

## RBAC Permissions

### Reader Role (Recommended)

Built-in Reader role provides all required permissions:

```bash
az role assignment create \
  --assignee <APP_ID> \
  --role "Reader" \
  --scope /subscriptions/<SUBSCRIPTION_ID>
```

**What Reader allows (all 14 permissions CleanCloud uses):**

| Permission | Rule |
|---|---|
| `Microsoft.Compute/disks/read` | Unattached managed disks |
| `Microsoft.Compute/snapshots/read` | Old snapshots |
| `Microsoft.Compute/virtualMachines/read` | Stopped (not deallocated) VMs |
| `Microsoft.Network/publicIPAddresses/read` | Unused public IPs |
| `Microsoft.Network/loadBalancers/read` | Empty load balancers |
| `Microsoft.Network/applicationGateways/read` | Empty app gateways |
| `Microsoft.Network/virtualNetworkGateways/read` | Idle VNet gateways |
| `Microsoft.Network/connections/read` | Gateway connection status |
| `Microsoft.Web/serverfarms/read` | Empty App Service Plans |
| `Microsoft.Web/serverfarms/sites/read` | Empty App Service Plans (app count) |
| `Microsoft.Sql/servers/read` | SQL server discovery |
| `Microsoft.Sql/servers/databases/read` | Idle SQL databases |
| `Microsoft.Insights/metrics/read` | SQL connection metrics |
| `Microsoft.Resources/subscriptions/read` | Subscription discovery |
| `Microsoft.Resources/resources/read` | Resource discovery |

**What Reader does NOT allow:**

- Delete operations (`*/delete`)
- Modification operations (`*/write`)
- Tagging operations (`Microsoft.Resources/tags/*`)
- Billing data access (`Microsoft.CostManagement/*`)

### Custom Role (Optional Least-Privilege)

```json
{
  "Name": "CleanCloud Scanner",
  "Description": "Minimal read-only access for CleanCloud",
  "Actions": [
    "Microsoft.Compute/disks/read",
    "Microsoft.Compute/snapshots/read",
    "Microsoft.Network/publicIPAddresses/read",
    "Microsoft.Web/serverfarms/read",
    "Microsoft.Web/serverfarms/sites/read",
    "Microsoft.Network/loadBalancers/read",
    "Microsoft.Network/applicationGateways/read",
    "Microsoft.Network/virtualNetworkGateways/read",
    "Microsoft.Compute/virtualMachines/read",
    "Microsoft.Network/connections/read",
    "Microsoft.Sql/servers/read",
    "Microsoft.Sql/servers/databases/read",
    "Microsoft.Insights/metrics/read",
    "Microsoft.Resources/subscriptions/read",
    "Microsoft.Resources/resources/read"
  ],
  "NotActions": [],
  "AssignableScopes": [
    "/subscriptions/<SUBSCRIPTION_ID>"
  ]
}
```

```bash
az role definition create --role-definition cleancloud-role.json

az role assignment create \
  --assignee <APP_ID> \
  --role "CleanCloud Scanner" \
  --scope /subscriptions/<SUBSCRIPTION_ID>
```

---

## Subscription Scanning

### Single Subscription (Default)

```bash
export AZURE_SUBSCRIPTION_ID="12345678-1234-1234-1234-123456789abc"
cleancloud scan --provider azure
```

### All Accessible Subscriptions

```bash
unset AZURE_SUBSCRIPTION_ID
cleancloud scan --provider azure
```

### Subscription Filtering

```bash
# Scan specific subscription
cleancloud scan --provider azure --subscription <SUBSCRIPTION_ID>

# Scan multiple subscriptions
cleancloud scan --provider azure \
  --subscription <SUB_1> \
  --subscription <SUB_2>
```

**When to use subscription filtering:**

- **Enterprise scale:** Organizations with 50+ subscriptions
- **Team ownership:** Scan only your team's subscriptions
- **CI/CD pipelines:** Different pipelines for different subscriptions
- **Testing:** Test on dev subscriptions before production
- **Performance:** Faster scans targeting specific subscriptions

CleanCloud validates that specified subscriptions are accessible:

```bash
cleancloud scan --provider azure --subscription invalid-sub-id
# Warning: 1 subscription(s) not accessible:
#   - invalid-sub-id
#
# Error: None of the specified subscriptions are accessible
```

---

## Region Filtering

```bash
# Scan only East US resources
cleancloud scan --provider azure --region eastus

# Scan only West Europe resources
cleancloud scan --provider azure --region westeurope
```

> Note: Region is an optional filter on results — not required like AWS.

---

## Validate Setup

```bash
cleancloud doctor --provider azure
```

**What it checks:**

- Azure credentials are valid
- Authentication method (OIDC, Service Principal, Azure CLI, Managed Identity)
- Security grade (EXCELLENT / GOOD / ACCEPTABLE / POOR)
- CI/CD readiness and compliance compatibility
- Token acquisition and expiry
- Accessible subscriptions
- Required RBAC permissions

**Example output:**

```
======================================================================
AZURE ENVIRONMENT VALIDATION
======================================================================

Step 1: Azure Credential Resolution
----------------------------------------------------------------------
Authentication Method: OIDC (Workload Identity Federation)
  Lifetime: 1 hour (temporary)
  Rotation Required: No
[OK] Uses Secret: No (secretless)

[OK] Security Grade: EXCELLENT
[OK]   - No client secrets stored
[OK]   - Temporary credentials
[OK]   - Auto-rotated

[OK] CI/CD Ready: YES
[OK]   Suitable for production CI/CD pipelines

[OK] Compliance: SOC2/ISO27001 Compatible

Step 2: Credential Acquisition
----------------------------------------------------------------------
[OK] Azure credentials acquired successfully
  Token expires in: ~58 minutes

Step 3: Subscription Access Validation
----------------------------------------------------------------------
[OK] Accessible subscriptions: 2
  • Production (a1b2c3d4-e5f6-7890-abcd-ef1234567890)
  • Staging (f9e8d7c6-b5a4-3210-fedc-ba0987654321)

Step 4: Permission Validation
----------------------------------------------------------------------
[OK] Subscription read access confirmed
  Reader role provides all required permissions

======================================================================
VALIDATION SUMMARY
======================================================================
Authentication: OIDC (Workload Identity Federation)
Security Grade: EXCELLENT
Subscriptions: 2 accessible

[OK] AZURE ENVIRONMENT READY FOR CLEANCLOUD
======================================================================
```

---

## Output Formats

```bash
# Human-readable (default)
cleancloud scan --provider azure

# JSON (machine-readable, includes evidence and full metadata)
cleancloud scan --provider azure --output json --output-file results.json

# CSV (spreadsheet-friendly, 11 core columns)
cleancloud scan --provider azure --output csv --output-file results.csv
```

JSON schema, examples, and CSV column reference: See [ci.md](ci.md)

---

## Troubleshooting

### OIDC Subject Claim Mismatch

**Symptom:** Azure login step fails with `AADSTS70021: No matching federated identity record found` or authentication silently fails even though the App Registration and federated credential exist.

**Cause:** The subject claim in your federated credential does not match what GitHub actually sends in the JWT token. GitHub generates different subject claims depending on how your workflow is triggered.

**The three subject formats:**

| Workflow uses | GitHub sends | Federated credential subject |
|---|---|---|
| Branch push to `main` | `repo:org/repo:ref:refs/heads/main` | `repo:<ORG>/<REPO>:ref:refs/heads/main` |
| Pull request trigger | `repo:org/repo:pull_request` | `repo:<ORG>/<REPO>:pull_request` |
| `environment: production` | `repo:org/repo:environment:production` | `repo:<ORG>/<REPO>:environment:production` |

**Fix — check what subject your workflow is sending:**

```bash
# List existing federated credentials on your App Registration
az ad app federated-credential list --id <APP_ID>
```

Then check your workflow — if it has `environment:` set:

```yaml
jobs:
  cleancloud:
    environment: production   # ← this changes the subject claim
```

You need a matching federated credential:

```bash
az ad app federated-credential create \
  --id <APP_ID> \
  --parameters '{
    "name": "CleanCloudGitHub-Env-Production",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<YOUR_ORG>/<YOUR_REPO>:environment:production",
    "audiences": ["api://AzureADTokenExchange"]
  }'
```

**Multiple triggers — create one credential per subject:**

You can attach multiple federated credentials to the same App Registration. If your pipeline runs on both branch pushes and with a GitHub Environment, create one credential for each:

```bash
# Credential 1: branch push
az ad app federated-credential create \
  --id <APP_ID> \
  --parameters '{
    "name": "CleanCloudGitHub-Branch",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<YOUR_ORG>/<YOUR_REPO>:ref:refs/heads/main",
    "audiences": ["api://AzureADTokenExchange"]
  }'

# Credential 2: GitHub Environment
az ad app federated-credential create \
  --id <APP_ID> \
  --parameters '{
    "name": "CleanCloudGitHub-Env",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:<YOUR_ORG>/<YOUR_REPO>:environment:production",
    "audiences": ["api://AzureADTokenExchange"]
  }'
```

> 💡 GitHub Environments are the recommended approach for production pipelines — they add deployment protection rules, required reviewers, and environment-scoped secrets on top of OIDC.

---

### "Missing Azure environment variables"

**For OIDC (GitHub Actions):**

```yaml
secrets:
  AZURE_CLIENT_ID
  AZURE_TENANT_ID
  AZURE_SUBSCRIPTION_ID
```

**For Service Principal (local):**

```bash
export AZURE_CLIENT_ID="..."
export AZURE_TENANT_ID="..."
export AZURE_CLIENT_SECRET="..."
export AZURE_SUBSCRIPTION_ID="..."
```

**For Azure CLI:**

```bash
az account show
```

---

### "Azure authentication failed"

**For OIDC** — verify the federated credential subject matches your workflow:

```bash
az ad app federated-credential list --id <APP_ID>
```

See [OIDC subject claim mismatch](#oidc-subject-claim-mismatch) above.

**For Service Principal:**

```bash
# Test authentication manually
az login --service-principal \
  -u $AZURE_CLIENT_ID \
  -p $AZURE_CLIENT_SECRET \
  --tenant $AZURE_TENANT_ID
```

**For Azure CLI:**

```bash
az login
```

---

### "No accessible subscriptions"

```bash
# Check role assignments
az role assignment list --assignee <APP_ID>

# Assign Reader role
az role assignment create \
  --assignee <APP_ID> \
  --role "Reader" \
  --scope /subscriptions/<SUBSCRIPTION_ID>

# Wait 5-10 minutes for RBAC propagation
```

---

### "Missing permission: Microsoft.Compute/disks/read"

```bash
# Verify Reader role is assigned
az role assignment list \
  --assignee <APP_ID> \
  --scope /subscriptions/<SUBSCRIPTION_ID>

# Wait 5-10 minutes for RBAC propagation
```

---

## Azure vs AWS Differences

| Aspect | AWS | Azure |
|---|---|---|
| OIDC Setup | IAM role trust policy | Federated identity credential |
| Permissions | IAM policies | RBAC roles |
| Regions | Must specify explicitly | All locations scanned by default |
| Resource Scope | Per-region | Per-subscription |
| Auth Methods | OIDC, AWS CLI, env vars | OIDC, Azure CLI, service principal |
| Local Development | Environment variables | Service principal or Azure CLI |

---

## Performance

| Subscriptions | Resources | Scan Time |
|---|---|---|
| 1 subscription | ~500 resources | 30–60 sec |
| 1 subscription | ~2,000 resources | 2–3 min |
| 3 subscriptions | ~6,000 resources | 5–8 min |

> API calls are all free — read-only operations have no cost.

---

## Security Best Practices

**DO:**
- Use OIDC for CI/CD (no stored secrets)
- Use Reader role (least privilege)
- Restrict federated credential to specific repo/branch or environment
- Use GitHub Environments for production pipelines
- Monitor Azure Activity Log for CleanCloud actions
- Use separate service principals per environment

**DON'T:**
- Use client secrets in CI/CD
- Grant Contributor role
- Share credentials across teams
- Commit credentials to repositories

---

## Supported Azure Clouds

- Azure Commercial ✅
- Azure Government (not tested)
- Azure China (not tested)

---

**Next:** [AWS Setup →](aws.md) | [Rules Reference →](rules.md) | [CI/CD Guide →](ci.md)