# Auto-TLS

Bootgly's HTTP Server can manage its own HTTPS certificate. Pass a typed `AutoTLS` config in the `AutoTLS:` parameter of the server Configs and the server obtains a certificate from **Let's Encrypt** (ACME v2, RFC 8555) by itself — no certbot, no cron, no third-party package:

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\AutoTLS;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Configs as ServerConfigs;

$Server = new HTTP_Server_CLI;
$Server->configure(
   new ServerConfigs(
      host: '0.0.0.0',
      port: 443,
      workers: 8,
      AutoTLS: new AutoTLS(
         domains: ['example.com', 'www.example.com'],
         email: 'admin@example.com'
      ),
      user: 'www-data',
      group: 'www-data'
   )
);
```

That is the whole setup. The same Configs still accepts the raw SSL context array (`local_cert` / `local_pk`) in its `secure:` parameter exactly as before — `AutoTLS:` is the managed alternative, and the two are mutually exclusive: passing both throws `InvalidArgumentException` at construction. Every WPI project scaffold (and the shipped Web projects) carries an Auto-TLS block commented in its `configure()` — set your domain and uncomment. The scaffolded block ships with `staging: true`: validate the flow against the staging CA first, then flip it to `false` for the real certificate.

> [!NOTE]
> On the Web platform, `App->configure()` keeps its own flat signature and takes the very same instance in its `secure:` parameter (`secure: new AutoTLS(...)`) — it builds the server Configs for you.

## What happens on first boot

1. **The server binds immediately** on a temporary self-signed certificate — no waiting on the CA, no downtime window.
2. A background **certifier** process registers an ACME account, places the order and answers the `HTTP-01` challenge natively.
3. The issued certificate is installed and **hot-swapped** into every live worker — new TLS handshakes present the real certificate, without a restart.
4. A supervision tick in the master **renews automatically** (~30 days before expiry, checked every 12h, lock-guarded), hot-swapping again on success.

If the CA is unreachable, the server keeps serving on its current certificate and retries with backoff (60s → 5m → 15m → 1h → 6h). Issuance failures never take the server down.

**Startup is a barrier.** The server does not advertise itself as started until *every* worker has bound its socket, activated the certificate and acknowledged it. A worker that cannot activate its credential refuses traffic and exits, and the launcher reports the failure with a non-zero exit — you never get a half-started server silently listening with the wrong identity.

## Port 80

`HTTP-01` validation always arrives over plain HTTP on port 80, so the server needs it answered. Auto-TLS covers the three deployments:

- **Only the 443 server (default)** — the master binds port 80 before dropping privileges and keeps a tiny **helper** process on it: ACME tokens are answered and every other request is redirected to HTTPS (`308`), like Caddy does.
- **A Bootgly server already on port 80** — opt it in explicitly by pointing it at the shared token directory (the route is never reserved implicitly, so existing user routes or another ACME client keep working):

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\ACME_Client\Challenges;

Challenges::configure('/path/to/storage/security/tls/challenges/');
```

  With it set, that instance answers `/.well-known/acme-challenge/` before any middleware or route — nothing you configure can break a validation.
- **Another server (nginx, etc.) on port 80** — proxy `/.well-known/acme-challenge/` to this host; Auto-TLS logs a notice and keeps working through the proxy.

### Privileged ports without root

Prefer granting PHP the bind capability and running the whole server as a regular user — no root, no demotion, no ownership split:

```bash :toolbar="true";
sudo setcap 'cap_net_bind_service=+ep' "$(readlink -f "$(which php)")"
```

> [!NOTE]
> The capability makes every PHP process non-dumpable, so `lsof -i`, `ss -p` and
> `fuser` stop resolving PHP sockets to their owning process. `bootgly project
> <name> stop` is unaffected — it reads the kernel lock table (`/proc/locks`).

Starting as root also works: pass `user:`/`group:` (required with Auto-TLS) and the privileged boot hands the whole credential store — including its parent directories — to that runtime identity before demoting. A startup that cannot reach the store fails naming the exact path and owner, on the terminal that launched it.

## Renewal and the reload fallback

Renewal is automatic — nothing to schedule. The swap applies to **new** handshakes; established connections keep the old certificate until they reconnect.

A swap only counts as applied once **every live worker acknowledges the exact certificate it activated**. Each worker copies the validated bytes into a private artifact, probes it and reports back; the master keeps the generation pending until all of them converge. If they do not, the master retries and then falls back on its own to a bounded graceful reload. You can also force a full re-read from disk at any time:

```bash :toolbar="true";
bootgly project reload   # SIGUSR2 — graceful re-exec, re-reads the secure config
```

## Staging and local testing

Let's Encrypt rate-limits production issuance. Use the staging environment while wiring things up:

```php
AutoTLS: new AutoTLS(
   domains: ['example.com'],
   email: 'admin@example.com',
   staging: true
)
```

Staging certificates are stored apart (`certificates/example.com-staging-{identity}/`) and never satisfy production checks.

For fully local end-to-end testing, point Auto-TLS at [Pebble](https://github.com/letsencrypt/pebble) (Let's Encrypt's own test CA):

```bash
docker run --rm --net=host -e PEBBLE_VA_NOSLEEP=1 \
   ghcr.io/letsencrypt/pebble -config test/config/pebble-config.json
```

```php
AutoTLS: new AutoTLS(
   domains: ['localhost'],
   email: 'dev@example.com',
   directory: 'https://localhost:14000/dir',   // Pebble
   port: 5002,        // Pebble validates HTTP-01 on 5002 (unprivileged)
   verify: false,     // Pebble uses its own test root
   allowPrivate: true // private/test-CA egress is opt-in
)
```

The repository ships this exact scenario as an opt-in suite: `BOOTGLY_ACME_E2E=1 bootgly test`.

## Storage

Everything lives under `storage/security/tls/` (overridable via `path:`), plain PEM guarded by file permissions — inspectable with `openssl x509` and portable to other tooling:

```text
storage/security/tls/
├── account/{ca-host}-{service}/    ← account key (0600) + registered URL —
│                                      one per ACME SERVICE (full directory URL)
├── certificates/{domain}-{id128}/  ← one store per configuration identity
│   ├── bootstrap.pem               ← temporary self-signed (with the SAN set)
│   ├── current.json                ← atomic manifest (the commit point)
│   └── {issued-ts}/                ← versioned: fullchain.pem, certificate.pem,
│                                      chain.pem, key.pem (0600)
├── challenges/                     ← HTTP-01 tokens (shared across processes)
└── renew.lock                      ← cross-process renewal lock
```

The `{id128}` suffix (128 bits of the identity digest) and the manifest carry a **configuration identity** (the sorted, deduplicated SAN set plus the ACME directory URL): adding a domain or switching CAs never silently reuses a certificate issued for another configuration — and the readable domain label is truncated, so even a maximal 253-byte hostname never overflows filesystem name limits. Installs are validated before the manifest commit — certificate/key match, every chain block parsed, validity window and SAN coverage; the credential snapshot is all-or-nothing (a certificate whose key went missing is never served).

Wildcards are not supported — they require the `DNS-01` challenge (deferred; `HTTP-01` only for now). Use explicit SANs: `domains: ['example.com', 'www.example.com', 'api.example.com']`.

## Reference

### AutoTLS

```php
public function __construct (
   array $domains,
   string $email,
   bool $staging = false,
   null|string $directory = null,
   null|string $path = null,
   null|string $challenges = null,
   int $threshold = 30,
   int $bits = 2048,
   bool $agreement = true,
   int $port = 80,
   bool $verify = true,
   array $options = [],
   array $authorities = [],
   bool $allowPrivate = false
)
```

Validates the whole configuration at construction (`InvalidArgumentException` on any invalid value — misconfiguration never reaches the CA). `domains` is the SAN set; `domains[0]` is the Common Name and names the certificate directory. `directory` overrides `staging`. `threshold` is the renew-when-fewer-days-remain trigger (1–89). `bits` sizes the RSA account and certificate keys (≥ 2048). `agreement` is the RFC 8555 Terms-of-Service agreement — configuring Auto-TLS implies it (the Caddy model), so it defaults to `true` and passing `false` throws. `port` is the HTTP-01 validation port the CA connects to. `verify` controls TLS peer verification toward the ACME directory. `challenges` overrides the HTTP-01 token spool directory — instances sharing one validation port must point at the same spool. `options` are extra SSL context options merged into the server socket context (explicit options win over managed values) — except the credential-selecting keys `local_cert`, `local_pk`, `passphrase` and `SNI_server_certs`, which are managed by Auto-TLS and rejected at construction: no accepted option can serve a certificate outside the validated, acknowledged generation. `authorities` lists extra `https://` origins (no path, query, credentials or fragment) trusted for CA-delegated ACME endpoints — the directory's own origin is always trusted. `allowPrivate` is the explicit opt-in for reaching a private or test CA (each origin still pins its first resolved IP).

```php
public function check (): bool
```

Whether an installed (non-bootstrap) certificate exists and has not expired.

```php
public function forge (): void
```

Ensures a servable certificate exists: reuses the current one while unexpired, else generates the temporary self-signed bootstrap. The server calls it during `configure()`.

```php
public function renew (): bool
```

Threshold-, lock- and backoff-guarded issuance: registers the account when needed, orders, installs. Returns `true` when a new certificate was installed (the hot-swap signal), `false` when nothing was due or another process holds the renewal lock. Failures record the backoff and are rethrown as `Exceptioning` (the ACME exception marker).

```php
public private(set) array $context
```

The SSL stream-context options for the server socket — the installed certificate when committed, else the bootstrap. Never cached: the underlying manifest changes under a live server.

### HTTP Server

```php
public function configure (Bootgly\ABI\Configs ...$Configs): self
```

Takes one Configs per concern, in any order — Auto-TLS belongs to the server Configs. See [HTTP Server CLI](/manual/WPI/HTTP/HTTP_Server_CLI/#reference) for the whole configuration model.

```php
new Bootgly\WPI\Nodes\HTTP_Server_CLI\Configs(
   host: '0.0.0.0',
   port: 443,
   workers: 8,
   AutoTLS: new AutoTLS(domains: ['example.com'], email: 'admin@example.com'),
   user: 'www-data',
   group: 'www-data'
)
```

`AutoTLS` hands the certificate lifecycle to the server (bootstrap, background issuance, hot swap, renewal); `secure` keeps the raw SSL context array. They are mutually exclusive — one certificate source, never two — and passing both throws `InvalidArgumentException`. In both forms the server socket never requests a **client** certificate: `verify_peer` and `verify_peer_name` default to `false` server-side (PHP's inherited `true` would make browsers prompt for mTLS). Enable mTLS deliberately via the AutoTLS `options: ['verify_peer' => true, 'cafile' => ...]` — explicit options always win. Named arguments only: the Configs constructor's first slot is the `Bootgly\ABI\Argument` guard, so a positional call raises a `TypeError`.

```php
public function swap (array $secure): bool
```

Low-level hot swap (inherited from `TCP_Server_CLI`): replaces the SSL context options of the live listening socket. Subsequent handshakes present the new credentials; established connections keep the old ones. Auto-TLS drives it automatically — direct use is only needed for custom certificate tooling.
