# Cloud Accounts

> Connect AWS, GCP, and Azure accounts to ZopNight. Supported auth methods, permission levels, billing upgrade, cluster registration, and OAuth flow.

Source: https://zop.dev/developer-docs/cloud-setup/cloud-accounts

---

Connect your AWS, GCP, or Azure accounts to enable resource discovery and scheduling.

## Supported Providers

### Amazon Web Services (AWS)

| Auth Method | ID | Description |
|---|---|---|
| **Static Access Keys** | `static_keys` | IAM access key ID and secret access key |
| **Assume Role** | `assume_role` | Cross-account IAM role assumption via STS |
| **Temporary Credentials** | `temporary_credentials` | Short-lived STS credentials |

### Google Cloud Platform (GCP)

| Auth Method | ID | Description |
|---|---|---|
| **Service Account** | `service_account` | JSON key file for a GCP service account |
| **OAuth 2.0** | `oauth` | Three-step OAuth flow with encrypted token exchange |

### Microsoft Azure

| Auth Method | ID | Description |
|---|---|---|
| **Service Principal** | `service_principal` | App registration with client ID, secret, and tenant ID |
| **Workload Identity Federation** | `workload_identity_federation` | Federated OIDC identity — no secrets stored |

## Permission Levels

| Level | Discovery | Scheduling | Execution |
|---|---|---|---|
| `read_only` | <span style={{ color: '#059669' }}>Yes</span> | <span style={{ color: '#059669' }}>Yes</span> | <span style={{ color: '#ef4444' }}>No</span> |
| `read_write` | <span style={{ color: '#059669' }}>Yes</span> | <span style={{ color: '#059669' }}>Yes</span> | <span style={{ color: '#059669' }}>Yes</span> |

**Warning**

Cloud accounts with `read_only` permission can discover resources and have schedules attached, but the executor will skip actions for those resources. Set `read_write` to enable actual start/stop operations.

## Connected & last-synced visibility

Each cloud account card shows the date it was first connected and the most recent successful sync — broken down per provider service. Useful for spotting accounts where a credential rotation or a permissions drop has silently halted discovery.

## Billing upgrade flow

If you connected an account in `read_only` mode without billing access, you can upgrade in three clicks without re-running setup. ZopNight detects the gap and surfaces a banner on the Dashboard with a per-account **Fix** button.

- **Provider-specific guide** — the upgrade drawer renders the exact IAM/role JSON for AWS or Azure based on your existing auth method.
- **AWS one-click CloudFormation** — for accounts using `assume_role`, ZopNight generates a CloudFormation template that adds `ce:GetCostAndUsage` and `ce:GetCostAndUsageWithResources` to your existing role. No manual policy editing.
- **Async verify** — the verify step polls the backend until a terminal state is reached, with a real-time spinner during the check.
- **GCP excluded** — billing is configured at the billing-account level, not the project, so the upgrade flow doesn't apply.

### POST /cloud-accounts/{accountID}/billing/verify

Trigger a verify-only billing sync. Returns 202 with a verifyID; poll /verify/{verifyID} until terminal.

### GET /cloud-accounts/{accountID}/billing/verify/{verifyID}

Poll the status of a verify operation. Terminal states: success, failed (with reason).

**Tip**

Cloud Accounts page and tenant subscription cards both show billing status badges with the same Fix button. RBAC: 403 if the caller doesn't have `cloudaccount:update`; 404 across orgs.

## Create Cloud Account

### POST /cloud-accounts

Connect a new cloud account.

#### AWS Example (Static Keys)

```bash
curl -X POST https://zopnight.com/api/cloud-accounts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "aws",
    "cloudAccountID": "123456789012",
    "name": "Production AWS",
    "authMethod": "static_keys",
    "permissionLevel": "read_write",
    "credentials": {
      "accessKeyID": "AKIAIOSFODNN7EXAMPLE",
      "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE"
    }
  }'
```

#### AWS Example (Assume Role)

```bash
curl -X POST https://zopnight.com/api/cloud-accounts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "aws",
    "cloudAccountID": "123456789012",
    "name": "Production AWS",
    "authMethod": "assume_role",
    "permissionLevel": "read_write",
    "credentials": {
      "roleArn": "arn:aws:iam::123456789012:role/ZopNightRole",
      "externalId": "zopnight-external-id"
    }
  }'
```

#### GCP Example (Service Account)

```bash
curl -X POST https://zopnight.com/api/cloud-accounts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "gcp",
    "cloudAccountID": "my-gcp-project-id",
    "name": "Production GCP",
    "authMethod": "service_account",
    "permissionLevel": "read_write",
    "credentials": {
      "serviceAccountJSON": "{...service account key JSON...}"
    }
  }'
```

#### Azure Example (Service Principal)

```bash
curl -X POST https://zopnight.com/api/cloud-accounts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "azure",
    "cloudAccountID": "subscription-id",
    "name": "Production Azure",
    "authMethod": "service_principal",
    "permissionLevel": "read_write",
    "credentials": {
      "clientID": "app-client-id",
      "clientSecret": "app-client-secret",
      "tenantID": "azure-tenant-id"
    }
  }'
```

```json title="Response"
{
  "data": {
    "id": "ca_abc123",
    "provider": "aws",
    "cloudAccountID": "123456789012",
    "name": "Production AWS",
    "authMethod": "static_keys",
    "permissionLevel": "read_write",
    "status": "active",
    "createdAt": "2025-01-15T10:30:00Z",
    "updatedAt": "2025-01-15T10:30:00Z"
  }
}
```

## List Cloud Accounts

### GET /cloud-accounts

List all connected cloud accounts.

## Get Cloud Account

### GET /cloud-accounts/{accountID}

Get a specific cloud account by ID.

## Update Cloud Account

### PUT /cloud-accounts/{accountID}

Update cloud account name, permission level, credentials, or authentication method — all in place, without recreating the account.

```bash
curl -X PUT https://zopnight.com/api/cloud-accounts/ca_abc123 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Renamed AWS Account",
    "permissionLevel": "read_write"
  }'
```

## Change the Authentication Method (in place)

You can switch a connected account from one authentication method to another —
for example AWS **access keys → keyless role federation** — **without deleting and
recreating it**. The account keeps the same identity, so its resources, groups,
recommendations, cost history, schedules, and configuration all stay intact.

**Supported switches**

- **AWS** — any-to-any across `static_keys`, `temporary_credentials`, and `assume_role`.
- **Azure** — `service_principal` ↔ `workload_identity_federation` (a tenant-scoped
  parent cascades the change to its subscription children).
- **GCP** — not yet available in place; `oauth` needs an interactive Google consent
  flow, so switching to/from it still uses disconnect + reconnect.

**In the UI**, open the account's **Edit** drawer → **Update credentials**. This
opens a guided 3-step wizard — **Auth Method** → **Setup Guide** (create the
credentials, including AWS one-click CloudFormation) → **Enter Credentials** — the
same steps used by the Connect Cloud Account wizard. Step 1 preselects the current
method (marked **Current**): keep it and save to **rotate** the credentials, or
pick a different method to **switch**. Saving re-verifies and re-keys the same
account. (GCP and Databricks keep an inline credential form instead of the wizard,
since they have no in-place method change.)

**Via the API**, send `authMethod` alongside the new `credentials` on the same
`PUT` endpoint:

```bash
curl -X PUT https://zopnight.com/api/cloud-accounts/ca_abc123 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "authMethod": "assume_role",
    "credentials": {
      "roleArn": "arn:aws:iam::123456789012:role/ZopNightRole"
    }
  }'
```

**Same-account safety.** The new credentials must resolve to the **same** cloud
identity as the account (AWS 12-digit account ID via STS; Azure subscription
membership; same tenant for tenant-scoped parents). If they point at a different
account the request is rejected with `400` and nothing changes — an account can
only be **re-authenticated**, never silently repointed. After a successful change
ZopNight re-fetches credentials and re-runs the permission probe automatically.

**Rotation is verified too.** A same-method credential update (rotation) runs the
exact same live check before storing — the new credentials must authenticate and
resolve to the same account. This means a wrong or foreign credential is rejected
with `400` instead of silently overwriting the working credential and breaking
discovery.

## Delete Cloud Account

### DELETE /cloud-accounts/{accountID}

Disconnect a cloud account. Associated resources will be soft-deleted.

## GCP OAuth Flow

For GCP accounts, ZopNight supports a three-step OAuth flow as an alternative to service account keys. Tokens are encrypted with AES-256-GCM and are never exposed in plaintext.

**1. Get OAuth URL**

### GET /connect/gcp/oauth-url

Returns a Google OAuth consent URL. Redirect the user to this URL.

**2. Exchange Code**

### POST /connect/gcp/oauth-exchange

Exchange the authorization code for an encrypted token reference.

**3. Finalize**

### POST /connect/gcp/oauth-finalize

Finalize the connection by selecting a GCP project and setting permissions.

## Cluster Access for Deployment Spaces

Once a cloud account is connected, you can register specific Kubernetes clusters (EKS, GKE, AKS) so ZopNight can mint short-lived kubeconfigs for them. Registering a cluster is the prerequisite for promoting it to a [Deployment Space](https://zop.dev/docs/deployments/deployment-spaces) — the unit ZopNight provisions and deploys against.

### GET /cloud-accounts/{accountID}/clusters

List clusters discovered in this cloud account, with their registration status.

```json title="Response"
{
  "data": [
    {
      "name": "prod-eks",
      "region": "us-east-1",
      "type": "aws-eks",
      "registered": true,
      "registeredAt": "2026-04-15T08:00:00Z"
    },
    {
      "name": "staging-eks",
      "region": "us-east-1",
      "type": "aws-eks",
      "registered": false
    }
  ]
}
```

### PUT /cloud-accounts/{accountID}/clusters/{region}/{clusterName}/access

Register a cluster for kubeconfig access.

```bash title="Request"
curl -X PUT "https://zopnight.com/api/cloud-accounts/ca_abc123/clusters/us-east-1/prod-eks/access" \
  -H "Authorization: Bearer <token>"
```

### DELETE /cloud-accounts/{accountID}/clusters/{region}/{clusterName}/access

Deregister a cluster. Existing deployment spaces that target it will fail to deploy until re-registered.

**Info**

Registration grants ZopNight permission to obtain temporary credentials for the cluster's API server (via STS for EKS, IAM for GKE, AAD for AKS). The kubeconfig is minted on demand and never persisted. See [Deployment Spaces](https://zop.dev/docs/deployments/deployment-spaces) for how registered clusters are used at deploy time.

## Azure Workload Identity Federation

For secretless Azure authentication, ZopNight exposes OIDC discovery endpoints that Azure can use to validate tokens via Workload Identity Federation.

### GET /.well-known/openid-configuration

OIDC discovery document for Azure WIF setup.

### GET /.well-known/jwks.json

JSON Web Key Set containing the RSA public key for token validation.

**Tip**

With Workload Identity Federation, no client secrets are stored in ZopNight. Azure validates identity using the OIDC endpoint, eliminating secret rotation concerns.

See [Cloud Support Matrix](https://zop.dev/docs/cloud-setup/cloud-support) for the complete list of discoverable and executable resource types per provider.
