acme-proxy 0.2.0

An ACME (RFC 8555) server that issues from a local CA, relays to an upstream CA, or delegates to a script
Documentation
# EAB Check

`type = "eab"` matches on the External Account Binding credential the
requesting account registered under. It is the multi-tenant lever: mint one
credential per tenant, bind each to its own name space, and no tenant can
request another's names.

```toml
[filter]
rules = ["tenant-a", "tenant-b"]

[filter.check.is-tenant-a]
type  = "eab"
allow = ["tenant-a"]

[filter.check.tenant-a-names]
type  = "identifiers"
allow = ["*.tenant-a.example.com"]

[filter.rule.tenant-a]
when = "is-tenant-a and tenant-a-names"
then = "allow"
```

Identifier stage only — at the connection stage no account has been
authenticated, so there is no credential to ask about.

## Why the label and not the account

The obvious handle for "which client is this" would be the account id, and it
is the wrong one. An account id is a UUID generated when the account is
created, so a policy naming one can only be written *after* the fact, and you
would be editing configuration in response to a client registering.

An EAB credential is the other way round: `acme-proxy eab create --label
tenant-a` mints it **before any account exists**, and you choose the label. It
also survives what the account does next — a key rollover keeps the same
account, and a client re-registering under the same credential lands in the
same tenant. Credentials are deliberately reusable, so one label
naturally covers a whole team.

Blocking a single misbehaving account is a different job and already has a
lever: `acme-proxy account deactivate <id>`.

## No credential means refused

An account registered without EAB has no credential, and this check refuses it.
That is the only defensible reading of a question about *which* credential
authorised the account — "none" cannot satisfy a tenant rule.

It follows that an `eab` check under a profile whose `eab.enabled` is `false`
could never do anything but refuse, so that combination is a **startup error**
rather than a policy.

## Matching

`allow`/`deny` are globs over the **label**, with the usual semantics described
in [Checks](checks.md#allow-and-deny) — `deny` first and winning, an empty
`allow` imposing no constraint. `allow_regex`/`deny_regex` take anchored
regexes and union with them.

`kids` pins credentials by their `kid` instead, for an operator who would
rather not rely on labels being unique. It is a **second allow source** rather
than a separate gate: either the kid being listed or the label matching is
enough.

```toml
[filter.check.is-tenant-a]
type  = "eab"
allow = ["tenant-a"]
kids  = ["4f1c…"]     # this exact credential, whatever its label says
```

A credential minted with no label cannot match a label allowlist. That is the
same rule seen from the other side, and it is why `eab create` is worth always
giving a `--label`.

## Making revocation retroactive

Revoking an EAB credential stops new *registrations*. Accounts already created
under it keep issuing, for ever — which is what the credential's role as a
registration-time authorisation implies, and what
`accounts.eab_kid`'s own migration meant by calling it an audit trail.

`require_active = true` changes that for this check: the credential must still
be `active`, so `acme-proxy eab revoke <kid>` reaches existing accounts too.

```toml
[filter.check.live-credential]
type           = "eab"
require_active = true
```

Off by default, because turning it on retroactively changes what `eab revoke`
means for a deployment. It is the lever to reach for when a tenant's credential
leaks — and it is a usable policy on its own, with no labels at all: "any
tenant, but not one whose credential we have withdrawn".

## Cost

Resolving the credential is two indexed reads, and they happen **only when the
policy contains an `eab` check**. A deployment without one pays nothing.

### Reference

**`filter.check.<name>.kids`** (`Array`) — *Default: `[]` | Env: `ACME_PROXY_FILTER__CHECK__<NAME>__KIDS`*

Credential `kid`s matched exactly, beside the label globs in `allow`. A second
allow source, not a separate gate.

**`filter.check.<name>.require_active`** (`Boolean`) — *Default: `false` | Env: `ACME_PROXY_FILTER__CHECK__<NAME>__REQUIRE_ACTIVE`*

Also require the credential to still be active, so revoking it refuses accounts
already registered under it.

A check that sets none of `allow`, `deny`, `kids` or `require_active` only asks
whether the account used EAB at all, which the `[eab]` section already
guarantees. That is a startup error rather than a check that always passes.