> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Certificate permissions

> Every IAM action the certificate service checks, which call needs it, and the resources a policy can scope it to.

Every certificate endpoint checks one IAM action before it does anything. This
is the whole list — there are no others, and no endpoint skips the check.

<Info>
  The API reference shows the action on each endpoint's own page, so you do not
  have to come back here to look one up. Both come from the same place: the
  authorization call in the service, read at build time.
</Info>

## The actions

| Action                               | Call                                             |
| ------------------------------------ | ------------------------------------------------ |
| `certificate:ListCertificates`       | `GET /v1/certificates`                           |
| `certificate:CreateCertificate`      | `POST /v1/certificates`                          |
| `certificate:GetCertificate`         | `GET /v1/certificates/{certificate_id}`          |
| `certificate:GetCertificateMaterial` | `GET /v1/certificates/{certificate_id}/material` |
| `certificate:DeleteCertificate`      | `DELETE /v1/certificates/{certificate_id}`       |
| `certificate:RevokeCertificate`      | `POST /v1/certificates/{certificate_id}/revoke`  |

The mapping is one endpoint to one action, which is not true of every service
here. Three things are still worth saying, because none of them follow from the
endpoint names.

### Reading a certificate does not get you its private key

`certificate:GetCertificate` returns everything except the key. The key comes
only from the material endpoint, behind its own
`certificate:GetCertificateMaterial`, so a credential that can read and list
your certificates still cannot extract what makes them usable.

That separation is the **only** thing standing between a credential and the
key. The endpoint accepts the same credentials as every other call and does not
care what kind of principal you are, so anyone holding the action gets the
private key. Grant it deliberately, and only alongside a resource scope you
mean. See [Certificate material](/certificates/material).

### Issuing authorizes on the name you asked for

Create is checked against the CRN of the certificate **about to exist** —
`certificate/<name>`, built from the `name` in the request, before any row is
written. A policy scoped to `certificate/prod-*` therefore gates issuance the
same way it gates the reads that follow, rather than being a rule you can only
apply after the fact.

Both create paths use the same action: issuing through the DNS challenge and
uploading material you already hold are both `certificate:CreateCertificate`.
If you need to allow one and not the other, that distinction is not available
in the policy — the endpoint is the same.

### Attaching to a listener is not a certificate action

Putting a certificate on an HTTPS listener needs
`loadbalancer:AttachListenerCertificate` on **the load balancer's** CRN. There
is no certificate action in that path at all.

Ownership is enforced separately and does not depend on your policy: the CRN
must name a certificate your account owns, and its account slot must match your
own. A certificate belonging to another account is refused even if you can name
it exactly.

## Resources

Certificate actions are checked against one resource shape:

```
crn:certificate::<account>:certificate/<name>
```

The region slot is empty because certificates are not region-bound — the same
certificate can back load balancers in any region.

The CRN is built from the **name**, not from an id, which is what makes a
policy able to follow a naming convention:
`crn:certificate::my-account:certificate/prod-*` covers every certificate whose
name starts with `prod-`, including ones issued later.

<Note>
  `name` is unique per account and matches
  `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,253}$`. Because it lands in the CRN, choosing
  names with a common prefix per environment is what makes the wildcard above
  worth writing.
</Note>

## Conditions

Create carries the tags from the request, and every other action carries the
tags already on the certificate, so policy conditions work either side of
issuance:

* `basalt:RequestTag/<key>` — what a caller may label a certificate **as**, at
  create.
* `basalt:ResourceTag/<key>` — which existing certificates an action may touch.

## Writing a policy

Read-only across every certificate in the account. Note what this deliberately
leaves out:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "ReadCertificates",
      "effect": "allow",
      "actions": [
        "certificate:ListCertificates",
        "certificate:GetCertificate"
      ],
      "resources": ["crn:certificate::my-account:certificate/*"]
    }
  ]
}
```

Full control of one naming convention, without the private keys:

```json theme={null}
{
  "version": "2024-01-01",
  "statements": [
    {
      "sid": "ManageProdCertificates",
      "effect": "allow",
      "actions": [
        "certificate:CreateCertificate",
        "certificate:GetCertificate",
        "certificate:DeleteCertificate",
        "certificate:RevokeCertificate"
      ],
      "resources": ["crn:certificate::my-account:certificate/prod-*"]
    }
  ]
}
```

<Warning>
  **`certificate:*` includes the private key.** A wildcard on the action grants
  `certificate:GetCertificateMaterial` along with everything else, which is
  rarely what is meant by "let this team manage certificates". Nothing
  downstream will catch the mistake — there is no second check on the material
  endpoint. List the actions explicitly unless the credential is meant to hold
  the key.
</Warning>

`certificate:ListCertificates` is checked against the account rather than a
named certificate, so it cannot be scoped to one. Granting it grants the
ability to see that every certificate in the account exists, though not to read
the material of any of them.

## What a denial looks like

A failed check answers `403`:

```json theme={null}
{
  "error": {
    "code": "ACCESS_DENIED",
    "message": "You don't have permission to perform this action",
    "request_id": "..."
  }
}
```

**It does not tell you which action was missing**, deliberately — the message
is the same for every denial, so it cannot be used to map out what a credential
can and cannot reach. That is why the table above exists: look up the call you
made, and the action it needs is the one to add.

<Note>
  A `404` is not a disguised `403`. Ownership and authorization are separate
  checks, and ownership is resolved first: a certificate belonging to another
  account answers `404` because it is not yours to see, and one you own but
  lack the action for answers `403`. If you are getting `404` on a certificate
  you believe you own, check the account the credential belongs to before
  checking the policy.
</Note>
