# ACME clientsUsing standard ACME clients with the internal CA — what is the same as Let's Encrypt, what is different.

## What stays the same as Let's Encrypt

If you have ever obtained a Let's Encrypt certificate, you can
already use the internal CA. The protocol is identical — ACME
RFC 8555. The clients you already know work without modification.

Same:

- HTTP-01 and DNS-01 challenges.
- Account registration and key management.
- 90-day default certificate lifetime.
- Automatic renewal at 60 days.
- Wildcard certificates via DNS-01.

## What is different

Two small differences, both one-time setup steps:

1. **The ACME directory URL** points to the internal CA, not
   `https://acme-v02.api.letsencrypt.org/directory`.
2. **The root certificate** of the internal CA must be trusted
   by the requesting client. This is normally arranged via
   configuration management (Ansible, Puppet, MDM) at host
   provisioning.

That is the entire difference. Everything else is the same Let's
Encrypt workflow you already know.

## Pointing your client at the internal CA

The internal ACME endpoint is published at a stable internal URL
(typically `https://acme.example.internal/acme/acme/directory`,
but the exact path depends on your deployment). Below are
common-client recipes.

### `certbot`

```sh
certbot --server https://acme.example.internal/acme/acme/directory \
        --email ops@example.internal \
        --no-eff-email \
        --agree-tos \
        certonly --standalone -d service.example.internal
```

For renewal, the `--server` flag is persisted in `renewal/*.conf`,
so the periodic `certbot renew` cron will pick it up
automatically.

### `acme.sh`

```sh
acme.sh --register-account \
        --server https://acme.example.internal/acme/acme/directory \
        -m ops@example.internal

acme.sh --issue \
        --server https://acme.example.internal/acme/acme/directory \
        --standalone \
        -d service.example.internal
```

### `lego`

```sh
lego --server=https://acme.example.internal/acme/acme/directory \
     --email=ops@example.internal \
     --domains=service.example.internal \
     --http run
```

### Caddy

In your `Caddyfile`:

```caddy
{
  acme_ca https://acme.example.internal/acme/acme/directory
  email ops@example.internal
}

service.example.internal {
  reverse_proxy localhost:8080
}
```

Caddy will automatically obtain and renew the certificate.

### Traefik

In your dynamic configuration:

```yaml
certificatesResolvers:
  internal:
    acme:
      caServer: "https://acme.example.internal/acme/acme/directory"
      email: ops@example.internal
      storage: /etc/traefik/acme.json
      httpChallenge:
        entryPoint: web
```

Reference `certResolver: internal` from your router definitions.

## Installing the root certificate on clients

For the certificates the CA issues to be trusted without warnings,
the CA's root must be in the system trust store of every host
that will see the certificates.

### Linux (Debian/Devuan family)

```sh
sudo cp internal-ca-root.crt /usr/local/share/ca-certificates/internal-ca-root.crt
sudo update-ca-certificates
```

### Linux (RHEL family)

```sh
sudo cp internal-ca-root.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
```

### OpenBSD / FreeBSD

```sh
# OpenBSD
sudo install -m 0644 internal-ca-root.crt /etc/ssl/internal-ca-root.crt
sudo cat /etc/ssl/cert.pem internal-ca-root.crt > /etc/ssl/cert.pem.new
sudo mv /etc/ssl/cert.pem.new /etc/ssl/cert.pem
```

### macOS

```sh
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain internal-ca-root.crt
```

### Windows

```cmd
certutil.exe -addstore -f "ROOT" internal-ca-root.crt
```

For browsers that maintain their own trust stores (Firefox), the
root must additionally be imported via Settings → Privacy →
Certificates → Authorities.

This installation is normally handled by configuration
management. Doing it by hand is fine for a one-off but does not
scale to a fleet.

## Wildcard certificates

Wildcards are issued via the DNS-01 challenge — the client
proves control of a domain by creating a TXT record. This works
for internal-only domains because the internal CA can validate
TXT records on any DNS server you control.

Example with `acme.sh` using the BIND nsupdate provider:

```sh
acme.sh --issue \
        --server https://acme.example.internal/acme/acme/directory \
        --dns dns_nsupdate \
        -d '*.example.internal' \
        -d 'example.internal'
```

You will need the nsupdate provider to be configured against
your internal DNS — see the `acme.sh` documentation for the
specific environment variables.

## Renewal

Renewal works the same way as with Let's Encrypt: the client
checks remaining validity and renews when below the threshold
(60 days for a 90-day cert).

For most deployments, the existing renewal cron (`certbot renew`,
`acme.sh --cron`, Caddy's automatic renewal) just works.

If you see renewal failures:

1. **Is the directory URL still reachable?** A network change
   may have broken the path from the client to the CA.
2. **Has the root certificate expired or been rotated?** Roots
   are typically valid for 10 years; intermediates for shorter
   periods. A rotation that did not propagate to all clients
   will manifest as renewal failures.
3. **Is the challenge type still appropriate?** A service that
   used to be reachable for HTTP-01 may now sit behind a
   load balancer that breaks the challenge response.

## Limits and policies

The internal CA enforces per-account policies — what domains
each account may request certificates for, what maximum lifetime
is allowed, what challenge types are accepted.

If a request that worked yesterday suddenly fails with a
`policy denied` error, the policy has changed (or your account
binding has). Contact your CA operator. The
[Operations](/services/support/) service is the escalation path
if your CA operator is us.
