# Certeasy Documentation Full Export Source: https://docs.certeasy.tech/ --- --- # administration/antivirus-edr.md --- sidebar_position: 8 title: Antivirus & EDR --- # Antivirus & EDR Certeasy on Windows ADCS spawns `certreq.exe`, writes transient CSR and certificate files, reads ADCS template metadata, and binds an HTTPS listener. Endpoint Detection and Response (EDR) products (Microsoft Defender for Endpoint, CrowdStrike Falcon, SentinelOne, ESET, Sophos, …) and traditional antivirus tools sometimes interpret this combination as suspicious activity, because the same primitives appear in credential-theft and lateral-movement playbooks. This page lists what Certeasy does on the host, what to allow-list before deploying, and how to react if the EDR blocks something unexpectedly. The list below is a best-effort baseline — Certeasy is not certified against any specific EDR product, and your security team owns the final policy. :::info Planned for 1.0: no more `certreq.exe` spawn Certeasy 0.9 talks to ADCS by spawning `certreq.exe` / `certutil.exe` for each submit / retrieve. Even though these binaries ship with Windows, strict EDRs flag the parent-child chain because the same pattern appears in offensive ADCS tooling (Certify, Certipy). The 1.0 release replaces these spawns with in-process MS-WCCE (DCOM/RPC) calls — no more child processes, no more LOLBin behavioral signature. Track the work on the [public roadmap](../intro/roadmap.md) (*Native ADCS bridge*) — until then, the exclusions below are the supported mitigation. ::: ## What Certeasy does on the host | Activity | When | Why an EDR may flag it | |---|---|---| | Spawns `certreq.exe -submit`, `-retrieve`, `-config` | Every ACME order finalize, every status poll, every revocation | New parent → `certreq.exe` chains are uncommon outside auto-enrolment, EDRs often score them | | Writes `*.csr`, `*.cer`, `*.req` to `/adcs/` | Transient, deleted within seconds of each issuance | File-creation+deletion bursts of certificate-looking content | | Reads `HKLM\SYSTEM\CurrentControlSet\Services\CertSvc` keys (indirectly via `certutil`) | Boot and per-request | Registry reads on PKI services trigger some heuristics | | Binds `0.0.0.0:443` (or configured port) | Boot | A non-IIS process binding `:443` on a Windows server is unusual | | Writes to the SQLite database file in `` | Continuous | Large write rate to an opaque file format | | Appends to `/audit.log` | Every protocol event | Log file growth is usually benign | ## Recommended exclusions Add the following to your EDR/AV real-time scanning exclusions **before** starting Certeasy. Replace `C:\Program Files\Certeasy\` and the workdir path with your actual install path. ### Process exclusions - `C:\Program Files\Certeasy\certeasy.exe` — the Certeasy binary itself. - `C:\Windows\System32\certreq.exe` — invoked by Certeasy. Usually already trusted by Defender, but third-party EDRs may not whitelist it by default in non-standard parent-child relationships. ### Path exclusions - `\` — the entire Certeasy work directory. Subpaths to focus on if blanket exclusion is not acceptable: - `\adcs\` — transient CSR / certificate scratch space (high file-creation rate). - `\db.sqlite`, `\db.sqlite-wal`, `\db.sqlite-shm` — SQLite database files (frequent writes). - `\audit.log` — append-only audit log. - `\server-certificate-cache\` — TLS certificate bundles for the ACME endpoint. ### Network exclusions If your EDR has an outbound-connection monitor, allow: - The ACME listening port (default `:443` or whatever you configured under `server.port`). - Traffic to the ADCS CA host (typically port `135` for RPC + dynamic high ports for the actual call — same configuration as a normal `certreq`). - Traffic to your DNS resolver(s) configured under `dns-validation-profiles`. ## Windows SmartScreen / Application Control If your environment uses Windows SmartScreen or AppLocker: - The Certeasy binary is currently distributed **unsigned**. SmartScreen will prompt the operator on the first launch (`Windows protected your PC`), and AppLocker will block it unless an explicit publisher or path rule is added. - Recommended: add a path rule in AppLocker pointing at your install directory, or wait for a signed release (planned). - Defender SmartScreen "warn but allow" can be unblocked by an administrator via *Properties → Unblock* on the binary right-click menu. ## If your EDR blocks Certeasy Symptoms to look for: - Certeasy exits immediately at startup with `access denied` errors on its workdir or on `certreq.exe`. - ACME orders fail at finalize with a backend error mentioning `certreq` not found or terminated; the audit log will show repeated `certificate.issue` failures with the same reason. - A long latency on every order (the EDR is intercepting and analysing each `certreq.exe` spawn before letting it run). To diagnose: 1. Pull the EDR's quarantine / detection log for the host and filter on `certeasy.exe` and `certreq.exe`. The detection name and the rule ID tell your security team which heuristic fired. 2. Add the [recommended exclusions](#recommended-exclusions) and restart Certeasy. 3. If detections continue, capture a Certeasy stderr trace (`APP_LOG_LEVEL=debug`) covering one failed order and share it with your EDR vendor along with the rule ID — that is enough for them to issue an exception or a tuned signature. ## Linux Linux deployments of Certeasy do not invoke `certreq.exe` — the equivalent activity is local-only (SQLite + audit log + ACME network traffic). If your Linux host runs an EDR agent, the recommended exclusions reduce to the workdir and the listening port; the process exclusion is rarely needed because Linux EDRs do not generally weight `:443` binders the same way. ## What is NOT a sign of EDR interference These behaviours are normal and should not be reported to your security team as a Certeasy issue: - Brief CPU bursts on the host during a batch of finalize calls — each `certreq.exe` spawn does cryptographic work. - A new `\adcs\` file appearing and disappearing within a second during issuance — the file is the live CSR, deleted as soon as the ADCS response is parsed. - An audit-log line per protocol event — the audit log is append-only by design and meant to grow. --- # administration/audit.md --- sidebar_position: 6 title: Audit Log --- # Audit Log Certeasy writes a tamper-evident audit log of business decisions and security-relevant events: account lifecycle, order creation, authorization outcomes, certificate issuance and revocation, rate-limit denials, and license transitions. Each line is HMAC-chained to the previous one so any insertion, deletion, or modification is detectable after the fact with the bundled `certeasy audit verify` command. The audit log is **enabled by default**: a silent opt-out would be a compliance hole. Disable it explicitly with `enabled: false` if you have a specific reason. ## Configuration ```yaml audit: enabled: true path: "" rotate: max-size-mb: 0 max-backups: 0 ``` Omitting the `audit` block applies the defaults shown above. | Field | Default | Meaning | |---|---|---| | `enabled` | `true` | Set to `false` to disable the audit writer entirely. No file is created and `Audit()` calls are dropped silently. | | `path` | `""` | Absolute path to the audit log file. Empty resolves to `/audit.log`. | | `rotate.max-size-mb` | `0` | When greater than zero, Certeasy rotates the file in-process once it exceeds this size (using the same writer as `logs.rotate`). `0` means "no in-process rotation" — let the OS handle it (logrotate on Linux, Task Scheduler on Windows). | | `rotate.max-backups` | `0` | Number of rotated backups to keep when in-process rotation is active. `0` deletes the previous file on rotation. Ignored when `max-size-mb` is `0`. | ### Choosing in-process vs OS-managed rotation In-process rotation is convenient on Windows where logrotate is not available out of the box. It uses the same `logx.RotatingFileWriter` as the application log: rotated files are renamed `.1`, `.2`, …, with `.1` being the most recent backup. OS-managed rotation is the recommended setup on Linux: drop a logrotate snippet that uses `copytruncate` so Certeasy's append-mode handle is not invalidated mid-rotation. Either way, `certeasy audit verify` walks rotated files automatically — the chain is preserved across rotations. ## What Is Logged The audit log captures decisions and security events. The list is intentionally narrow: nonce churn, individual JWS verifications, GET requests on the directory, and `/metrics` scrapes are explicitly excluded — they would drown the signal in noise without forensic value. The tables below list every recorded event, what triggers it, and what extra context is attached in the `details` field. Common fields (`ts`, `account_id`, `source_ip`, `user_agent`) are described in [Line Format](#line-format) and are not repeated here. ### Account events | Event | When | Details | |---|---|---| | `account.create` | A new ACME account is created. The dedupe path (existing key reused) is silent. | `contact`: list of contact URIs declared by the client | | `account.keychange` | An account's JWK is rotated successfully via `keyChange`. | `old_thumbprint`, `new_thumbprint` | | `account.deactivate` | An account is set to `deactivated` by its owner. | _(none)_ | ### Order and authorization events | Event | When | Details | |---|---|---| | `order.create` | A new order is accepted and persisted. | `order_id`, `dns_names` (canonical: lowercased, sorted, deduped, wildcards preserved) | | `order.finalize` | A `finalize` request has been accepted and the issuance job is enqueued. Represents the client's accepted finalize, not the actual cert issuance. | `order_id`, `policy_id`, `dns_names` | | `order.invalid` | An order transitions to `invalid`. Emitted at most once per order — retries do not duplicate the event. The free-form `reason` field on the line carries the failure message. | `order_id`, `source`: `"challenge"` or `"pki"` | | `authorization.validate` | An authorization transitions to `valid` (`decision: allow`) or `invalid` (`decision: deny`). Deny is emitted only when the authorization actually flips to invalid (i.e. all challenges of that authz failed). | `authz_id`, `identifier`, `chall_type` | | `challenge.validate` | A challenge succeeds. **Failures are not recorded per challenge** — they would be too noisy. The chain "these challenges passed → this cert was issued" remains reconstructible from the order, authorization, and certificate events. | `chall_id`, `chall_type`, `identifier`, `authz_id` | ### Certificate events | Event | When | Details | |---|---|---| | `certificate.issue` | A certificate has been issued and persisted. | `cert_id`, `order_id`, `pki_request_id`, `serial`, `dns_names` | | `certificate.revoke` | A certificate is revoked via the `revoke-cert` endpoint. | `cert_id`, `fingerprint`, `reason` (RFC 5280 reason code; JSON `null` when the client did not provide one) | ### Rate limiting | Event | When | Details | |---|---|---| | `ratelimit.deny` | A request is refused with HTTP 429. Emitted once per refusal. | `type`: one of `global`, `account-creation`, `order-creation`, `duplicate-certificate`, `failed-validation`, `pending-authorizations`; `retry_after` (seconds) | ### License | Event | When | Details | |---|---|---| | `license.change` | The license state transitions between `valid`, `grace`, `expired`, `revoked`, or `no_license`. The boot baseline is not emitted — only subsequent transitions are. | `prev`, `next` | ## Line Format The log is JSONL — one self-contained JSON object per line. Field order is fixed by the schema version (currently `"1"`). ```json {"ts":"2026-05-08T14:32:11.123Z","schema":"1","seq":12345,"prev_mac":"hmac-sha256:abcd...","event":"order.create","account_id":"acct_abc","source_ip":"10.0.0.5","user_agent":"certbot/2.9","decision":"allow","details":{"order_id":"ord_xyz"},"mac":"hmac-sha256:1234..."} ``` | Field | Always present | Description | |---|---|---| | `ts` | yes | RFC 3339 timestamp with millisecond precision, in UTC | | `schema` | yes | Schema version string. Currently `"1"`. Bumped only on incompatible layout changes. | | `seq` | yes | Strictly increasing per installation. Resumes across restarts and rotations. | | `prev_mac` | yes | The `mac` of the previous line, prefixed with `hmac-sha256:`. The first line uses the **genesis MAC** anchored on the installation ID. | | `event` | yes | Dot-namespaced event name (`order.create`, `certificate.revoke`, …) | | `account_id` | optional | ACME account identifier when the event is account-scoped | | `source_ip` | optional | Client IP (see RGPD note below) | | `user_agent` | optional | Client User-Agent string | | `decision` | optional | `allow` or `deny` for events that involve a policy check | | `reason` | optional | Human-readable reason — empty when not applicable | | `details` | optional | Event-specific payload, encoded as a JSON object | | `mac` | yes | `hmac-sha256:` of all preceding fields. Always the last field on the line. | Optional fields are omitted from the line when empty (`omitempty`). ## How the Chain Works On the very first install, Certeasy generates a 32-byte random secret and stores it in the database (`audit_state` table). The secret is never logged, never exposed via any API, and never rotated. The first line of every installation is anchored to a **genesis MAC** computed from the secret and the stable installation identifier: ``` genesis_mac = HMAC-SHA-256(secret, "certeasy-audit-v1|" + installation_id) ``` Each subsequent line carries the `mac` of the previous line in its `prev_mac` field, and computes its own `mac` as `HMAC-SHA-256(secret, line_bytes_without_mac)`. Tampering with any line invalidates that line's MAC; tampering with the chain (insertion, deletion, reordering) invalidates the next `prev_mac`. The genesis anchor matters: an audit file restored on a different installation (different `installation_id`) will not validate, even if the secret matches. This is intentional — it prevents a stolen audit file from being passed off as evidence on another system. ### Why HMAC and not plain SHA-256 A plain hash chain seeded from a publicly known value (the installation ID is visible in logs and the license portal) would let anyone with write access to the file rebuild the chain after modifying a line. HMAC requires the secret stored in the database — without DB access, the chain cannot be reforged. The threat model: filesystem compromise alone does not allow forgery. DB compromise is a higher bar; if an attacker has read/write access to the database, the audit log is no longer the weakest link. ## Verifying the Chain The chain is only useful if you actually verify it. Run: ```sh certeasy audit verify -f /etc/certeasy/config.yml ``` The command walks rotated predecessors (`audit.log.1`, `.2`, …) chronologically, then the active file. It validates: 1. Each line's `mac` against `HMAC(secret, line_bytes_without_mac)`. 2. Each `prev_mac` against the previous line's `mac` (or the genesis MAC for line 1). 3. That `seq` is strictly increasing. Exit codes: | Code | Meaning | |---|---| | `0` | Chain valid (or no audit lines exist yet — fresh install) | | `1` | Chain broken. The first break is reported on stderr with file, line number, and reason. | You can override the file path: ```sh certeasy audit verify -f /etc/certeasy/config.yml --path /backups/2026-05/audit.log ``` This still requires the database (the secret and the installation ID live there), so the override is for verifying a copy of the file alongside the live DB — not for verifying a backup on a different machine. To verify a snapshot offline, restore the DB backup alongside the audit file first. ### When to verify - **Periodically** (e.g. nightly via cron / Task Scheduler) — catch silent corruption early. - **After every restore** — confirm the audit file and the database secret are consistent. - **When investigating an incident** — confirm the timeline you are reading was not modified after the fact. ## Listing nodes that wrote to the chain Each audit line carries the `server_id` of the node that wrote it (see [Node identity](./deployment-topology#node-identity)). To list every node that has ever booted against this database — useful when investigating who wrote which lines, or before decommissioning a host: ```sh certeasy audit list-servers -f /etc/certeasy/config.yml ``` Output columns: `server_id`, `hostname`, `first_seen`, `last_seen` (UTC, RFC 3339). Sorted by `last_seen` descending so the most recently active node appears first. ## Storage and Backups The HMAC secret lives in the database. **Backing up the database is required** for the audit log to remain verifiable: the audit file alone is useless without the secret. Cover both in the same backup procedure — see the [Backup](../administration/backup) page. Loss of the secret means loss of verifiability for all earlier lines (the file is still readable, just no longer cryptographically anchored). New writes cannot resume the old chain; on a fresh install, the writer starts a new chain from a new genesis. ## Personal Data and RGPD `source_ip` and `user_agent` are potential personal data. Retention and the user-facing notice are the responsibility of the operator who installs and runs Certeasy: - **Retention** — Certeasy does not delete audit lines on its own. The retention policy is whatever your OS rotation rule keeps. - **Notice** — Mention the audit log in your service's privacy notice. - **Access controls** — The audit file is created with `0644` permissions by default. Restrict the directory if your hosting model requires it. There is no PII redaction option in v1: the goal of the audit log is forensic, and redacted entries would defeat that goal. Operators who cannot retain IPs should disable the audit log entirely (`enabled: false`) and accept the loss of forensic capability. ## Operational Notes - **Failures do not block business flow.** If a write to the audit file fails (full disk, permission error), the failure is logged via the `audit` log service and the operation continues. Audit gaps are detected by `audit verify`, not by ACME clients. - **Line size cap.** Lines are capped at 1 MiB. Events that would produce a larger line are dropped with a log entry. The cap is a defence against a misbehaving event source — legitimate events are well below 1 KiB. - **No auto-purge.** Even when in-process rotation is enabled, Certeasy never deletes lines based on age. Retention is purely a function of how many rotated backups you keep (`max-backups` or your OS rotator). --- # administration/backup.md --- sidebar_position: 4 title: Backup & restore --- # Backup & restore Certeasy stores all persistent state in a single database plus a small set of files in the workdir. This page covers SQLite, the default driver. Postgres and SQL Server users should follow their standard DBA tooling — see the [Other databases](#other-databases) section. ## What to back up | Path | Contents | Backup method | |---|---|---| | `db.sqlite` (+ `-wal`, `-shm`) | Default SQLite database | `certeasy backup create` (NOT raw copy) | | `/server-certificate-cache/` | Certmanager TLS bundles for the ACME endpoint | File copy | | `/fakepki/` | Fake PKI CA + key (only if running the fake authority) | File copy | | `` (or `/audit.log`) | Audit log (when enabled) | File copy | | `config.yml` | Server configuration | File copy | | Let's Encrypt cache | `tls-certificate-manager.lets-encrypt.cache-dir`, may be outside workdir | File copy if configured | The `/adcs/` directory is **transient** (CSRs and certutil scratch files recreated on demand) and must not be in your backup set. The DB file itself must never be copied raw with the server running: the `-wal` and `-shm` companion files contain uncommitted writes and the result is corrupt. Always use `certeasy backup create`, which calls SQLite's `VACUUM INTO` to produce a self-contained snapshot. ## `certeasy backup create` ``` certeasy backup create -f --output [--check none|quick|full] (default: quick) [--allow-incomplete] ``` The command: 1. Loads the same config as the running server. 2. Runs the integrity check selected by `--check`. 3. If the check fails and `--allow-incomplete` is not set: exits non-zero without producing a file. 4. If the check fails and `--allow-incomplete` is set: prints a warning, produces the backup, and exits with code `2` (distinguishes from a clean success). 5. Runs `VACUUM INTO` against the destination path. SQLite refuses to overwrite an existing destination — pick a fresh filename. ### Choosing `--check` | Level | SQL | Catches | Cost | |---|---|---|---| | `none` | (skip) | nothing | ~0 | | `quick` | `PRAGMA quick_check` | B-tree, page, header corruption (~95% of issues) | 30–50% of full | | `full` | `PRAGMA integrity_check` | + cross-checks indexes/data, FK validity | seconds on a nominal DB | **No hard write-lock during the check.** In WAL mode an integrity check is a read transaction — writers continue working. The actual cost is *WAL pressure*: the checkpoint is deferred while the check runs, so the `-wal` file grows, marginally slowing writes after several hundred MB. Negligible for a nominal Certeasy database. Recommended cadence: - `quick` every 4 hours during business hours - `full` once a day (typically overnight) - `none` only when paired with `backup verify` run asynchronously on the produced file ### `--allow-incomplete` Use this when a database is degrading and you want a snapshot for forensic purposes even if integrity checks fail. The exit code `2` is the signal that the backup exists but is suspect — your scheduled task should treat it as a distinct outcome from `0` (clean success) and `1` (no backup produced). ## `certeasy backup verify` ``` certeasy backup verify --input [--check quick|full] (default: full) [--schema] ``` Runs against a backup file directly, without needing the server's config: - Opens the file in read-only mode. - Runs the integrity PRAGMA selected by `--check`. - With `--schema`: also checks that the canonical Certeasy tables exist (catches "this is not a Certeasy DB" or partial backup). - Exits `0` on success, non-zero with a message on stderr otherwise. This enables a fast-backup pattern: run `backup create --check none` for speed, then verify the produced file in the background: ```bash certeasy backup create -f config.yml --output backup.sqlite --check none certeasy backup verify --input backup.sqlite --check full --schema ``` ## Procedure example (Windows Task Scheduler) ```bat 1. certeasy.exe backup create -f C:\certeasy\config.yml ^ --output D:\backups\db.sqlite 2. xcopy C:\certeasy\workdir\server-certificate-cache D:\backups\server-certificate-cache /E /I /Y 3. xcopy C:\certeasy\workdir\fakepki D:\backups\fakepki /E /I /Y 4. copy C:\certeasy\workdir\audit.log D:\backups\audit.log 5. copy C:\certeasy\config.yml D:\backups\config.yml ``` Steps 3–4 are conditional: skip step 3 if you are not using the fake PKI, skip step 4 if no audit log is configured. If your Let's Encrypt cache is outside the workdir, copy it too. ## Restore (manual procedure) There is no `restore` subcommand in v1: the procedure is a few file moves. 1. Stop the service: `Stop-Service Certeasy`. 2. Move the failed `db.sqlite`, `db.sqlite-wal`, `db.sqlite-shm` aside (do **not** delete them yet). 3. Copy the backup's `db.sqlite` to the workdir. 4. **Delete any stale `-wal` and `-shm` files** at the destination — those from the failed instance are no longer consistent with the restored DB. 5. Restore `server-certificate-cache/` and (if applicable) `fakepki/` from the backup. 6. Restore `audit.log` to its configured path. 7. Start the service: `Start-Service Certeasy`. 8. Tail the startup logs to confirm the license, schema migrations, and first request all succeed. 9. (When implemented, ROADMAP #1) `certeasy audit verify` to confirm the audit log MAC chain. ## 3-2-1-1 baseline A safe backup posture for a production Certeasy: - **3** copies of the data - on **2** different storage media - with **1** copy offsite - and **1** copy offline or immutable A test restore on a regular cadence (quarterly minimum) is the only way to prove your backup chain actually works. A backup that has never been restored is a hope, not a backup. ## Other databases ### PostgreSQL Out of scope for `certeasy backup`. Use standard PostgreSQL tooling: - `pg_dump` for logical, point-in-time snapshots that are portable across PG versions. - `pg_basebackup` plus WAL archiving for physical backups with PITR. The workdir (certificate cache, fakepki, audit log, config) still needs the file-copy procedure above; only step 1 changes. ### SQL Server Out of scope for `certeasy backup`. Use standard SQL Server tooling: - `BACKUP DATABASE` T-SQL with maintenance plans driven by SQL Server Agent. - Full / differential / log backup chains depending on your RPO. The workdir procedure is identical to SQLite. ## PII and retention The Certeasy database and audit log contain potentially personal data (account contact addresses, validation source IPs, user agents). Backups are subject to the same data-protection obligations as the live data — encrypt them at rest, restrict access, and apply a retention policy that matches your compliance requirements. --- # administration/deployment-topology.md --- sidebar_position: 1 title: Deployment topology --- # Deployment topology Certeasy is designed and supported as a **single-instance** deployment. This page documents which topologies are supported today, and which ones will silently break your installation if you try. ## Supported ### Single instance (recommended) One Certeasy process on one host, with its own database. This is the production-ready topology. ``` ┌──────────────┐ HTTPS ┌──────────────┐ RPC ┌──────────┐ │ ACME clients │ ──────────► │ Certeasy │ ───────────► │ ADCS │ └──────────────┘ └──────────────┘ └──────────┘ │ ▼ ┌──────────────────┐ │ Database │ │ SQLite / PG / │ │ SQL Server │ └──────────────────┘ ``` This covers the vast majority of enterprise PKI volumes. A single Certeasy instance on a modest Windows Server processes several certificate orders per second. ### Cold Active / Passive (manual switchover) You can install Certeasy on two hosts for failover, **as long as only one instance is running at a time**. The standby is fully stopped (process not running, port not bound). The administrator switches manually : stop the active node, then start the standby. ``` ┌──────────────────────────────────────────┐ │ VIP / LB │ └──────────────────┬───────────────────────┘ │ HTTPS ┌──────────┴──────────┐ ▼ ▼ ┌────────┐ ┌──────────┐ │ Node A │ │ Node B │ │ ACTIVE │ │ STOPPED │ └────┬───┘ └──────────┘ │ ▼ ┌────────────────────────────────────┐ │ Shared DB (PostgreSQL / SQL Server)│ └────────────────────────────────────┘ ``` Requirements for this topology : - **Database must be PostgreSQL or SQL Server.** SQLite is **NOT supported** for any multi-host setup: its file-level locking is not reliable across hosts on shared filesystems (NFS, SMB, etc.) and corruption is a matter of when, not if. - Each node has its **own local work directory** (TLS cache, transient files, audit log). The work directory does not need to be shared. - The administrator owns the switchover discipline: **never start the standby before the active is fully stopped.** Starting two instances against the same database is the unsupported Active / Active topology described below — pathologies will appear silently. Switchover procedure: 1. Stop Certeasy on the active node (graceful shutdown drains in-flight ACME requests). 2. Start Certeasy on the standby node. 3. Update your load balancer to route to the new active node. Expected switchover time: typically under a minute, including the standby's boot probe. ### Load balancer in front of a single instance A reverse proxy or load balancer in front of a single Certeasy instance — for TLS termination, IP filtering, geo-routing, etc. — is fully supported. Forward the `Host` header and preserve the client IP if your audit log relies on it. ## Node identity Each Certeasy instance has a stable identifier called `server_id`. It is materialised on first boot as a UUID v4 stored in `/server_id` (file permissions `0o600`) and registered in the `servers` table of the database. Every subsequent boot of the same node reads back the same `server_id` and updates the `last_seen` timestamp; a background heartbeat refreshes it once per minute while the instance runs. Two operator-visible consequences: - **Each line of the audit log carries the `server_id` of the node that wrote it.** When a node opens its audit log at boot, it checks the last line. If that line was written by a different `server_id`, the node **refuses to start** with an explicit error message naming both identifiers. This is intentional: it prevents an operator from accidentally pointing two nodes at the same audit file and silently splicing two histories. - **Cold Active / Passive works naturally with this model.** Each node has its own workdir, its own `/server_id`, and its own `/audit.log`. The database is shared, but the audit chain is per-node. The `servers` table will accumulate one row per host that has ever booted against this database — useful for operators tracking which nodes participated in the cluster over time. Do **not** copy a workdir from one host to another. Each new host should generate its own `server_id` on first boot — that is the point of the marker file. If you ever need to inspect or decommission a known node, list them with: ``` certeasy audit list-servers -f config.yml ``` ## NOT supported — Active / Active Running two or more Certeasy instances **concurrently** against the same database **is not supported** in the current release. Several core mechanisms hold in-process state that does not coordinate across nodes: | Subsystem | What breaks under Active / Active | |---|---| | **ACME nonces** | Each instance generates nonces with its own secret and tracks them in local memory. A client whose first request lands on node A and second request lands on node B is rejected with `badNonce`, forcing a fresh `newNonce` call every two requests. Replay protection is also local per node — the same nonce can be rejected by one node and accepted by another within its 2-minute TTL. | | **Rate limiting** | Several limits (failed-validation back-off, account-creation throttling, order-creation throttling, global) are in-memory per instance. A client hitting two nodes can effectively double its quota. | | **License enforcement** | Per-instance counters for `max_managed_servers`. Two instances can both believe they are under the limit while the cluster as a whole has already exceeded it. | | **PKI health checks** | Each instance pings the configured CAs independently. Operationally noisy in logs, not corruption-inducing. | | **TLS certificate manager** | Each instance maintains its own server-certificate cache on disk. Two instances behind the same hostname will fetch or issue their TLS cert independently — risk of double-consuming a Let's Encrypt quota or returning different chains depending on which node a client lands on. | **Failure modes are silent and intermittent.** Clients see sporadic `badNonce` errors, rate limits feel inconsistent, license counters drift, certificate behaviour depends on which node terminates the TLS handshake. Diagnosing these in production after the fact is painful. ### Why sticky sessions do not fix this A load balancer with session affinity (cookie-based) does **not** solve the nonce problem in practice : the standard ACME clients (lego, certbot, acme.sh, native Go clients) do not enable an HTTP cookie jar for their ACME requests, so a `Set-Cookie` from the load balancer is ignored. Source-IP affinity is more reliable but breaks the moment clients sit behind a NAT, a corporate proxy, or a CGNAT. If you need true multi-node availability today, use **cold Active / Passive** above and accept the manual switchover. True warm Active / Passive and Active / Active deployments are tracked on the [public roadmap](../intro/roadmap.md) for V2.0 Enterprise. ## Database backend behind a single instance Independently of the Certeasy topology, the database tier can run its own HA setup: - **SQLite WAL** — concurrent readers + single writer. Adequate for single-instance Certeasy. Not usable across hosts. - **PostgreSQL with replication** — primary + read replicas for backup and reporting. Certeasy only writes to the primary. Database-level failover (e.g. `pg_auto_failover`, Patroni) is transparent to Certeasy as long as the connection string resolves to the new primary after the cut. - **SQL Server with Always On / mirroring** — same principle. Certeasy connects to one target ; failover at the database tier is handled by the listener / cluster role. In all cases, scaling the database tier does **not** unlock Active / Active for Certeasy itself. --- # administration/license-enforcement.md --- sidebar_position: 7 title: License enforcement --- # License enforcement Certeasy enforces the limits associated with your active license at two moments: - **At boot**, against your configuration and the current state of the database (number of declared authorities, database driver in use, number of distinct accounts already serving certificates). - **At runtime**, on every new order, against the live state. If your configuration exceeds what the active plan allows, the server refuses to start until you explicitly acknowledge the situation. This is intentional: a silent downgrade (for example after an automatic license renewal moves you to a smaller plan) would otherwise only show up later as ACME clients receiving `403` responses. ## What is checked | Limit | Source | Boot check | Runtime check (new orders) | |---|---|---|---| | Allowed database drivers | Plan | ✅ vs `database.driver` | ✅ | | Maximum authorities | Plan | ✅ vs number of declared authorities | ✅ | | Maximum managed servers | Plan | ✅ vs distinct active accounts | ✅ | A "managed server" is a distinct ACME account with at least one active (non-expired, non-revoked) certificate. Re-issuances and renewals from the same account do not consume additional quota. See the [Plans page](../intro/plans.md) for the per-plan limits. ## Boot behaviour Three outcomes are possible at startup, depending on whether the configuration matches the license: ### Conforming — silent boot Everything is within the plan. The server starts normally and any previous acknowledgement (see below) is cleared. ### Cold start (no license yet) If you start with `--grace` and have no license installed yet, Certeasy applies a temporary cold-start plan (defaulting to **Free**) chosen with `--cold-start-plan`: ``` certeasy serve --grace --cold-start-plan starter -f config.yml ``` In this mode, the boot only emits warnings — never refuses to start — because cold start is meant to give you time (96 hours) to install your license: ``` certeasy license install -f config.yml /path/to/your.lic ``` ### Degraded — boot refused until acknowledged If a license is installed and the configuration exceeds it, the server refuses to start with a message similar to: ``` LICENSE DEGRADED — server refuses to start. Reasons: max_cas_exceeded, db_not_allowed. Run 'certeasy license acknowledge-degraded -f ' to acknowledge and persist this state, or fix your configuration / upgrade your plan. ``` You have two options: 1. **Fix the underlying problem** — reduce the number of authorities, change the database driver, or upgrade your plan, then restart. 2. **Acknowledge the degraded state** — run the command shown in the message. The server will then start, with a clearly visible warning, and continue serving renewals (see [Runtime behaviour](#runtime-behaviour) below). ## Acknowledging a degraded state ``` certeasy license acknowledge-degraded -f config.yml ``` This command: - Reads the active license and the relevant configuration. - Refuses if the configuration is **not** in a degraded state — there is nothing to acknowledge in that case. - Refuses if no license is installed yet — install your license first, then re-run the command. - Otherwise persists the acknowledgement in the database and prints a summary, including the list of reasons and the timestamp. The acknowledgement is **not** a permanent waiver. It is bound to the specific combination of plan limits and configuration that triggered it. The acknowledgement expires automatically when **any** of the following changes: - The plan changes (license renewal moves you to a larger or smaller plan). - The number of declared authorities changes. - The database driver changes. A change in the **number of managed servers** alone does **not** invalidate the acknowledgement. Counts naturally fluctuate with certificate expirations and revocations; if a previously degraded count drops back under the cap, the next boot is silent and the acknowledgement is cleared on its own. If a future boot is conforming, the stored acknowledgement is removed so that any new degradation later requires a fresh, explicit acknowledgement. ## Runtime behaviour Even after a degraded boot has been acknowledged, the server still enforces the license on every new order: - **Renewals are always accepted.** A renewal is identified as a new order for the same canonical FQDN set already covered by an active certificate for that account. This guarantees existing clients keep functioning while you fix your configuration. - **New certificates are refused** with HTTP 403 and an audit log entry (`license.deny`) when: - The active database driver is not allowed by the plan. - The number of declared authorities exceeds `MaxCAs`. - The new order would create a managed server beyond `MaxManagedServers`. - The plan grants no managed-server entitlement at all. The audit event includes the reason, the account identifier, the source IP, and a `details` payload describing the limit that fired. See the [Audit log page](audit.md) for how to query and verify the audit chain. ## Reasons reference | Reason | When it fires | |---|---| | `db_not_allowed` | The configured `database.driver` is not in the plan's allowed list. | | `max_cas_exceeded` | More authorities are declared in configuration than the plan allows. | | `managed_servers_exceeded` | The number of distinct accounts with active certificates is above the plan's cap. | | `max_managed_servers_zero` | The active license grants no managed-server entitlement at all. | ## Audit events Every license-enforcement decision is recorded in the [audit log](audit.md) so that compliance and forensic review can reconstruct what happened without relying on operator memory or rotated stdout logs. The following events are emitted: | Event | When | Decision | Key details | |---|---|---|---| | `license.boot_refused` | The server refused to start because the configuration is degraded and no acknowledgement matches. | `deny` | `reasons` | | `license.boot_degraded` | The server started in degraded mode against a valid acknowledgement. One event per boot. | `allow` (`reason=ack_active`) | `reasons`, `hash`, `acknowledged_at` | | `license.acknowledge` | An operator ran `certeasy license acknowledge-degraded`. | `allow` (`reason=operator_ack`) | `reasons`, `hash`, `hostname` | | `license.deny` | A new order was refused at runtime because of a license limit. One event per refused request. | `deny` (`reason` = the failing limit) | `reasons` and the offending values (driver, current count, max, etc.) | | `license.change` | The license state transitioned (`valid` ↔ `grace` ↔ `expired` ↔ `revoked` ↔ `no_license`). | `allow` | `from`, `to` | The audit log is tamper-evident (HMAC chain anchored on the installation identifier). Use `certeasy audit verify` to validate it. See the [Audit log page](audit.md) for the file format and rotation behaviour. ## Example acknowledgement output ``` $ certeasy license acknowledge-degraded -f config.yml License degradation acknowledged. Reasons: [db_not_allowed max_cas_exceeded] Hash: 7a3f...e1b9 At: 2026-05-10 14:05:22 UTC The ack will expire automatically if constraints or configuration change. ``` The hash is shown for support and operational diagnostics; you do not need to record it. Operators typically run this command once after a downgrade, then schedule the configuration fix or plan upgrade as a follow-up. --- # administration/logging.md --- sidebar_position: 1 title: Logging --- # Logging Certeasy uses structured logging with configurable level, format, output, and per-service overrides. ## Configuration ```yaml logs: level: info format: json output: file file: "/var/log/certeasy/certeasy.log" rotate: max-size-mb: 100 max-backups: 10 services: DB-Driver: warn Certeasy-acme-server: debug tags: instance: cert-srv-01 region: eu-west ``` ## Fields | Field | Default | Description | |---|---|---| | `level` | `info` | Global log level: `debug`, `info`, `warn`, `error`, `off`. `off` (alias `none`) fully suppresses logs and is most useful as a per-service override. | | `format` | `json` | Log format: `json` or `text` | | `output` | `stderr` | Output destination: `stderr`, `stdout`, or `file` | | `file` | — | Log file path. Required if `output: file`. | | `rotate.max-size-mb` | — | Max log file size in MB before rotation | | `rotate.max-backups` | — | Number of rotated log files to keep | | `services` | empty | Per-service log level overrides | | `tags` | empty | User-defined labels added to every log entry — useful for Grafana/Loki filtering | ## Per-Service Log Levels You can set a different log level for each internal service. This is useful for debugging a specific component without flooding logs with debug output from everything else. ```yaml logs: level: info services: Certeasy-acme-server: debug Async-Acme-Challenges: debug ``` Use `off` (or `none`) to fully silence a service — for example when a chatty driver is generating noise during dev or staging captures: ```yaml logs: services: DB-Driver: off Certeasy-acme-server: warn ``` ### Registered Service Names | Service Name | Description | |---|---| | `DB-Driver` | Database driver and query logs | | `adcs` | ADCS authority operations | | `Certeasy-acme-server` | ACME HTTP request handling | | `Async-Acme-Pki-Handler` | Async PKI job processing | | `Async-Acme-Challenges` | Async challenge validation | | `JWKS` | JWS key validation | | `worker` | Job engine (lease, dispatch, backoff) | | `http-server` | HTTP server lifecycle | ## Tags (Grafana/Loki labels) `logs.tags` is a free-form map of `key: value` pairs added to **every** log entry. Use it to attach environment metadata that your log aggregator (Grafana/Loki, Splunk, Elastic…) can filter on. ```yaml logs: tags: instance: cert-srv-01 region: eu-west role: production ``` Each entry shows up as a top-level field in the JSON output, alongside `time`, `level`, `msg`, etc. There is no fixed list of allowed keys — pick whatever your stack expects. :::note The previous automatic `env` field is no longer added to log entries; it conflicted with the `env=` shown inside license-related log messages (license environment, e.g. `env=dev` / `env=prod`). If you want an environment label, set it explicitly under `tags`. ::: ## Log Rotation Log rotation is supported when `output: file`. Configure `rotate` to limit disk usage: ```yaml logs: output: file file: "C:\\ProgramData\\certeasy\\certeasy.log" rotate: max-size-mb: 100 max-backups: 5 ``` This keeps up to 5 rotated files of 100 MB each (500 MB total). ## Production Recommendations - Use `format: json` for structured log ingestion (Splunk, Elastic, Loki…) - Use `output: file` with rotation to avoid filling disk - Keep global level at `info` and only set `debug` on specific services when troubleshooting - Route logs to your SIEM — the audit log entries contain account IDs, order IDs, and operation details --- # administration/migrations.md --- sidebar_position: 3 title: Migrations --- # Migrations Certeasy manages its database schema automatically. There is nothing to run manually. ## How It Works Migrations are **embedded in the binary** as Go code. At every startup, Certeasy: 1. Connects to the configured database 2. Checks the current schema version 3. Applies any pending migrations in order 4. Proceeds to start If the schema is already up to date, startup proceeds immediately with no changes. ## Supported Databases Migrations are implemented for all three supported drivers: | Driver | Notes | |---|---| | SQLite | Default. File-based, no external setup. | | PostgreSQL | Uses PostgreSQL-specific DDL where applicable. | | SQL Server | Uses T-SQL DDL. | Each driver has its own migration set — Certeasy does not use a generic SQL abstraction layer. ## No Downtime Migrations Migrations are additive by design (new columns, new tables, new indexes). They do not drop or rename existing columns, so upgrading Certeasy does not require a maintenance window in most cases. ## Manual Intervention You should never need to run SQL manually. If a migration fails at startup, Certeasy logs the error and exits. The error message identifies the failing migration. If you need to inspect the schema, use the standard tools for your database driver: ```bash # SQLite sqlite3 /var/lib/certeasy/db.sqlite ".schema" # PostgreSQL psql -U certeasy -d certeasy -c "\d" # SQL Server sqlcmd -S sqlserver01 -d certeasy -Q "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES" ``` ## Backup Before Upgrade Before upgrading Certeasy to a new version, back up your database: ```bash # SQLite cp /var/lib/certeasy/db.sqlite /var/lib/certeasy/db.sqlite.bak # PostgreSQL pg_dump -U certeasy certeasy > certeasy_backup.sql ``` --- # administration/schema.md --- sidebar_position: 2 title: Database Schema --- # Database Schema Certeasy's schema is managed automatically via embedded migrations. This page documents the tables, their purpose, and their lifecycle. ## ACME Flow Overview | Step | Endpoint | Server Action | Tables Affected | |------|----------|---------------|-----------------| | New Account | `POST /acme/newAccount` | Creates a client account | `acme_accounts` | | New Order | `POST /acme/newOrder` | Creates an order with authorizations and challenges | `acme_orders`, `acme_order_identifiers`, `acme_authorizations`, `acme_challenges` | | Challenge Validation | `POST /acme/chall/` | Client responds, server validates asynchronously | `acme_challenges`, `acme_authorizations` | | Finalization | `POST /acme/finalize/` | Client sends CSR → certificate issued | `acme_orders`, `acme_certificates` | | Certificate Retrieval | `GET /acme/cert/` | Client downloads certificate | Read only | | Revocation | `POST /acme/revoke-cert` | Revokes a certificate | `acme_certificates` | | Replay Protection | Automatic | Anti-replay nonces | `acme_nonces` | | Auditing | Automatic | All significant actions | `acme_audit_logs` | --- ## Table: `acme_accounts` Stores registered ACME client accounts (RFC 8555 §7.1.2). | Column | Description | |---|---| | `account_id` | Logical account ID (e.g. `/acme/acct/123`) | | `jwk` | Client public key in canonical JSON | | `contact` | List of `mailto:` addresses | | `status` | `valid`, `deactivated`, `revoked` | | `tos_agreed_at` | Timestamp of Terms of Service acceptance | **Created**: `POST /acme/newAccount` (when JWK thumbprint is new) **State transitions**: - `valid` → `deactivated`: via `POST /acme/acct/{id}` with `"status":"deactivated"` - `valid` → `revoked`: on full account revocation --- ## Table: `acme_nonces` Anti-replay nonces used in JWS request headers. | Column | Description | |---|---| | `nonce` | Random nonce value | | `created_at` | When the nonce was issued | | `used_at` | When the nonce was consumed (null if unused) | Every ACME response generates a new nonce. Once a nonce is used in a valid JWS request, it is marked used and cannot be reused. Expired nonces are cleaned up periodically. --- ## Table: `acme_orders` Represents a certificate order. | Column | Description | |---|---| | `order_id` | Order identifier | | `account_id` | Owning account | | `status` | `pending`, `ready`, `processing`, `valid`, `invalid` | | `csr` | Base64url-encoded CSR (set at finalize) | | `not_before`, `not_after` | Requested validity window | | `expires_at` | Order expiry | | `certificate_id` | Linked certificate (set when issued) | **Created**: `POST /acme/newOrder` **Status flow**: ``` pending → ready (all authorizations valid) → processing (finalize received) → valid (certificate issued) → invalid (challenge or issuance failure) ``` --- ## Table: `acme_order_identifiers` DNS identifiers requested in an order. One row per identifier. An order for `["app.corp.internal", "*.corp.internal"]` creates two rows. **Created**: during `POST /acme/newOrder` --- ## Table: `acme_authorizations` Proof of control for each identifier in an order. | Column | Description | |---|---| | `authz_id` | Public URL | | `identifier_value` | DNS name (e.g. `app.corp.internal`) | | `status` | `pending`, `valid`, `invalid` | | `expires_at` | Authorization expiry | | `wildcard` | Whether this is a wildcard authorization | | `error` | Validation error detail (if failed) | When at least one challenge for an authorization becomes `valid`, the authorization becomes `valid`. When all authorizations for an order are `valid`, the order status moves to `ready`. --- ## Table: `acme_challenges` Validation challenges (DNS-01, HTTP-01, TLS-ALPN-01) for each authorization. | Column | Description | |---|---| | `chall_id` | Challenge identifier | | `type` | `dns-01`, `http-01`, `tls-alpn-01` | | `status` | `pending`, `processing`, `valid`, `invalid` | | `token` | Challenge token | | `key_authorization` | Computed key authorization | | `validated_at` | Timestamp of successful validation | **Created**: automatically with `newOrder` **Updated**: on `POST /acme/challenge/` → moves to `processing`, then `valid` or `invalid` --- ## Table: `acme_certificates` Issued TLS certificates. | Column | Description | |---|---| | `certificate_id` | Certificate identifier | | `account_id` | Owning account | | `order_id` | Originating order | | `pem_chain` | PEM certificate chain (never includes private key) | | `not_before`, `not_after` | Validity window | | `fingerprint` | SHA-256 fingerprint of the leaf certificate | | `revoked_at` | Revocation timestamp (null if active) | | `revoke_reason` | RFC 5280 reason code (0–10, excluding 7) | **Created**: `POST /acme/finalize/` after successful ADCS issuance **Updated**: `POST /acme/revoke-cert` --- ## Table: `acme_audit_logs` Internal audit trail for all significant ACME operations. | Column | Description | |---|---| | `action` | Operation type: `newAccount`, `newOrder`, `challengeRespond`, `finalize`, `revokeCert`, etc. | | `account_id` | Associated account | | `order_id` | Associated order | | `authz_id` | Associated authorization | | `chall_id` | Associated challenge | | `details` | JSON blob with operation details | | `created_at` | Timestamp | Populated on every significant ACME action including errors. --- ## Full Flow Reference ``` POST /acme/newAccount → INSERT acme_accounts POST /acme/newOrder → INSERT acme_orders → INSERT acme_order_identifiers (one per identifier) → INSERT acme_authorizations (one per identifier) → INSERT acme_challenges (one per auth × challenge type) POST /acme/challenge/{id} → UPDATE acme_challenges (status → processing) [async job validates DNS/HTTP/TLS] → UPDATE acme_challenges (status → valid/invalid) → UPDATE acme_authorizations (status → valid if one challenge valid) → UPDATE acme_orders (status → ready if all authorizations valid) POST /acme/finalize/{id} → UPDATE acme_orders (status → processing) [async job submits CSR to ADCS] → INSERT acme_certificates → UPDATE acme_orders (status → valid, certificate_id → ...) GET /acme/cert/{id} → SELECT acme_certificates POST /acme/revoke-cert → UPDATE acme_certificates (revoked_at, revoke_reason) ``` --- # administration/shutdown.md --- sidebar_position: 5 title: Graceful shutdown --- # Graceful shutdown Certeasy stops cleanly on `SIGTERM` (Linux) and on the equivalent stop signal sent by the Windows Service Control Manager. This page describes the behaviour you can rely on, the two timeouts that bound it, and how to tune them. ## What happens on stop signal - The HTTP listener stops accepting new connections. - In-flight HTTP requests are allowed to finish. - Async work already claimed by the jobs engine (challenge validation, PKI polling, ADCS calls) is allowed to finish so its result is persisted. - The audit log and the database stay open for the full drain so late writes are never lost. - Once the drain is complete, the process exits. If a handler or a job is still running when the timeouts below expire, the process exits anyway and that work is interrupted. Jobs that were running at that point are picked up on the next start (the jobs queue is durable). ## The two timeouts | Setting | Default | What it bounds | |---|---|---| | `server.shutdown-timeout` | `30s` | How long Certeasy waits for in-flight HTTP requests to finish before forcing the listener to close. | | `workers.drain-timeout` | `30s` | How long Certeasy waits for in-flight async jobs to finish before forcing them to stop. | ### Invariant `server.shutdown-timeout` must be **less than or equal to** `workers.drain-timeout`. Certeasy refuses to start otherwise: ``` server.shutdown-timeout (45s) must be ≤ workers.drain-timeout (30s): in-flight HTTP handlers can enqueue jobs after the engine has stopped draining ``` If HTTP outlasts the jobs engine, late requests can produce jobs that nobody runs until the next start. Keeping the HTTP timeout at most equal to the jobs timeout closes that window. The defaults (`30s` / `30s`) already satisfy this. ## Configuration ```yaml server: url: - https://acme.example.com listen: 0.0.0.0:8443 shutdown-timeout: 30s workers: workers: 4 drain-timeout: 30s ``` Both fields accept Go duration syntax (`s`, `m`, `h`). ## Tuning - **Slow PKI backend (ADCS, long `certutil` calls).** Raise both timeouts together (e.g. `60s` / `60s`) so a final issuance has time to complete before the engine is forced down. - **Fast rotation, ephemeral instances.** The defaults are appropriate; don't lower them below `10s` or you will routinely interrupt healthy work. - **Stay below your service supervisor's own stop timeout.** Both systemd (`TimeoutStopSec`, default `90s`) and the Windows Service Control Manager send a hard kill after their own deadline. Keep `shutdown-timeout` and `drain-timeout` comfortably below it. ## In-flight ACME requests A long ACME operation (challenge validation, PKI poll) is allowed to keep running for the full `shutdown-timeout`. This is intentional — interrupting mid-request would leave the order in an awkward state for the client. If you need a hard ceiling on individual request duration, use `server.write-timeout` (default `30s`). ## After a restart Run `certeasy audit verify` if the audit log is enabled. The audit chain is designed to resume cleanly across stop/start, but `verify` confirms that no gap was introduced and reports the first break otherwise. Jobs that were still running when the previous instance stopped are picked up automatically once their lease expires (default `30s`). Nothing manual is required. --- # changelog/index.md --- sidebar_position: 1 title: Changelog --- # Changelog ## v0.9.0 - 2026-05-31 Initial public release. ### Features - ACME server (RFC 8555) covering account registration with key rollover, orders, authorizations, challenge validation, finalization, certificate retrieval, and revocation - HTTP-01, DNS-01 and TLS-ALPN-01 challenge validation - Wildcard certificates, including mixed `[apex, *.apex]` orders (RFC 8555 §7.1.4) - ACME Renewal Information endpoint (RFC 9773, read-only) for client-driven renewal scheduling - ADCS authority via `certreq.exe` - Built-in fake PKI authority for local testing - Issuance policies with DNS scope rules and signature constraints - Policy bindings with `first_available` and `round_robin` strategies - Server-side rate limiting per ACME account (duplicate-certificate) - SQLite (default), PostgreSQL and SQL Server backends - Async job engine with persistent retry and exponential backoff - TLS certificate manager for the server's own certificate (`files` and `pki` modes) - Structured logging with per-service level overrides and log rotation - Tamper-evident ACME audit log (JSONL + HMAC chain, validated by the `audit verify` command) - SQLite backup CLI (`backup create` / `backup verify`) - License enforcement with strict boot and acknowledgement of degraded states - Graceful HTTP shutdown - Built-in mitigations against ESC-class attacks: DNS-only identity, Server Authentication EKU only by default ### Interoperability covered by automated tests - ACME clients: certbot, lego, acme.sh, and a built-in protocol client - Backends: ADCS, fake PKI - Databases: SQLite, PostgreSQL, SQL Server - Full clients × challenges × databases × backends matrix --- :::note Certeasy is in **public beta**. Known limitations in this release: - **Revocation is server-side only.** A revoked certificate is marked revoked in Certeasy's database and an audit event is emitted, but the ADCS CRL / OCSP responder is not updated. Clients validating chain status against ADCS will still see the certificate as valid until the CRL is republished. Full propagation lands in v1.0. - **No health or metrics HTTP endpoints yet.** Operational monitoring is limited to log scraping and database introspection in this release; dedicated `/health` and metrics endpoints are planned. - **No automatic data retention or cleanup.** ACME tables (orders, authorizations, challenges, …) grow without bound. Operators running long-lived deployments should plan for manual maintenance until automated retention ships. - **RFC 9773 `replaces` field is accepted but not yet honored.** Clients can supply `replaces` on new orders without error, but the linkage to the previous certificate is not applied. The `renewalInfo` endpoint itself is fully functional. - **External Account Binding (EAB)** is not supported and is not planned for v1.0. Single-tenant enterprise deployments do not need it; see the [roadmap](../intro/roadmap.md) for v2.0 timing. - **Caddy** interoperability has not been formally validated in this release. ::: --- # clients/acme-sh.md --- sidebar_position: 2 title: acme.sh --- # acme.sh [acme.sh](https://github.com/acmesh-official/acme.sh) is a pure shell client — useful when Python (certbot) or Go (lego) aren't available, or simply when minimum runtime footprint matters. This page covers the acme.sh-specific bits; for the general onboarding flow and trust-store setup see [First Certificate](../getting-started/first-certificate.md). :::info Documented for acme.sh **3.x** (tested with the latest `neilpang/acme.sh:latest` Docker image) acme.sh's CLI has been stable for years — `--issue` / `--renew` / `--revoke` and their flags work identically across 2.x and 3.x. If you are on an older version and a flag is missing, upgrade is the recommended path. ::: ## What changes vs certbot - **Key type**: acme.sh generates **RSA 2048** by default. This is **refused** by Certeasy under the default `signature.min-rsa-bits: 3072` policy — always pass `--keylength` explicitly: - `--keylength 3072` or `4096` for RSA - `--keylength ec-256` or `ec-384` for ECDSA - **CSR EKU — read this carefully**: acme.sh's built-in OpenSSL template declares **both `serverAuth` and `clientAuth`** in the CSR's Extended Key Usage, regardless of intended purpose. By default Certeasy rejects this combination, returning `badCSR: EKU 1.3.6.1.5.5.7.3.2 in CSR not allowed by policy`. Two paths to make it work: 1. **Strict (preferred from June 2026)**: drop `clientAuth` from acme.sh's CSR template. The CA/B Forum baseline forbids the `serverAuth + clientAuth` combination on publicly-trusted server certificates from June 2026 onwards — production deployments should align even when fronted by an internal ADCS. The cleanest way is to maintain a private fork of `acme.sh` or to override its OpenSSL config file (`~/.acme.sh/openssl.cnf` if you use `--certhome`). 2. **Pragmatic (existing fleet)**: add `clientAuth` to the policy's `csr.allowed-extra-eku` on the Certeasy side. See [Configuration → Issuance policies → EKU](../configuration/issuance-policies.md). This unblocks acme.sh as-is; plan a migration before June 2026. - **Trust store**: acme.sh uses curl under the hood. Point both `--ca-bundle` and the `CA_BUNDLE` / `CURL_CA_BUNDLE` env vars at your OS bundle: ```bash # Debian / Ubuntu export CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # RHEL / CentOS / Rocky export CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt export CURL_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt ``` `--ca-bundle` is for the ACME server's TLS cert; `CURL_CA_BUNDLE` covers the underlying curl invocations acme.sh uses for some HTTP calls. Setting both is belt-and-braces. - **Built-in scheduler**: acme.sh installs its own daily cron entry via `--install-cronjob`, no systemd timer to write. ## HTTP-01 (standalone) ```bash acme.sh --issue \ --server https://acme.corp.internal/acme/directory \ --ca-bundle /etc/ssl/certs/ca-certificates.crt \ --standalone \ --keylength 3072 \ -d app.corp.internal ``` Cert + key land under `~/.acme.sh/app.corp.internal/`: - `fullchain.cer` — leaf + chain. - `app.corp.internal.cer` — leaf only. - `app.corp.internal.key` — private key. - `ca.cer` — chain only. acme.sh's `--standalone` binds port 80 with a tiny built-in server for the duration of the challenge. ## HTTP-01 (webroot) ```bash acme.sh --issue \ --server https://acme.corp.internal/acme/directory \ --ca-bundle /etc/ssl/certs/ca-certificates.crt \ --webroot /var/www/html \ --keylength 3072 \ -d app.corp.internal ``` acme.sh writes the challenge token under `/var/www/html/.well-known/acme-challenge/`. The web server must serve that path over HTTP without authentication. ## TLS-ALPN-01 ```bash acme.sh --issue \ --server https://acme.corp.internal/acme/directory \ --ca-bundle /etc/ssl/certs/ca-certificates.crt \ --alpn \ --keylength 3072 \ -d app.corp.internal ``` Same constraint as HTTP-01 standalone but on port 443. ## DNS-01 (for wildcards) acme.sh has its own catalogue of DNS plugins under `~/.acme.sh/dnsapi/`. Configure with env vars and pass `--dns dns_`: :::warning Disable the public DNS pre-check on intranet deployments By default, acme.sh resolves the just-installed `_acme-challenge.` TXT record through public DoH resolvers (Cloudflare, Google) before notifying Certeasy. On an internal-only deployment this check **will always fail** — the names do not exist publicly — and worse, the internal domain name is leaked in cleartext to the public resolvers. Pass `--dnssleep ` to skip the pre-check and wait `N` seconds before notifying the server. A short value such as `--dnssleep 1` is enough when the DNS plugin updates the authoritative server synchronously (`dns_nsupdate`, BIND/Knot RFC 2136, MS DNS via `dns_namesilo` etc.); larger values may be required for providers with slow API propagation. ::: ```bash # Example: RFC 2136 / nsupdate export NSUPDATE_SERVER='10.0.0.53' export NSUPDATE_KEY='/etc/acme.sh/keyfile' acme.sh --issue \ --server https://acme.corp.internal/acme/directory \ --ca-bundle /etc/ssl/certs/ca-certificates.crt \ --dns dns_nsupdate \ --dnssleep 1 \ --keylength 3072 \ -d '*.corp.internal' ``` The full list of plugins is in acme.sh's [DNS API documentation](https://github.com/acmesh-official/acme.sh/wiki/dnsapi). ## Renewal acme.sh installs its own daily cron entry. Enable it once after the first issuance: ```bash acme.sh --install-cronjob ``` The cron job runs `acme.sh --cron` daily; certs within 30 days of expiration are renewed automatically. To force a one-off renewal: ```bash acme.sh --renew -d app.corp.internal --force ``` ## Revocation ```bash acme.sh --revoke -d app.corp.internal \ --server https://acme.corp.internal/acme/directory \ --ca-bundle /etc/ssl/certs/ca-certificates.crt ``` --- # clients/certbot.md --- sidebar_position: 1 title: certbot --- # certbot [certbot](https://certbot.eff.org/) is the EFF's reference ACME client, Python-based, with the broadest community documentation and the most mature distro packaging. This page is the comprehensive reference; for a 5-minute onboarding tour see [Getting Started → First Certificate](../getting-started/first-certificate.md). :::info Documented for certbot **3.x** (tested with the latest `certbot/certbot:latest` Docker image) certbot's CLI has been stable since 1.x — the `certonly` / `renew` / `revoke` subcommands and their flags work identically across recent versions. If you are on an older 1.x or 2.x release, the syntax on this page still applies. ::: ## What changes vs lego / acme.sh - **Key type**: certbot generates **RSA 4096** by default. Always passes the `signature.min-rsa-bits: 3072` policy without tweaking — no surprise like acme.sh's `--keylength 2048` rejection. - **CSR EKU**: certbot declares `serverAuth` only — no `clientAuth` smuggling, no need to loosen `csr.allowed-extra-eku` on the policy. - **Trust store**: certbot uses Python's own CA bundle (`certifi`), **not** the OS trust store. You point it at the OS bundle via the `REQUESTS_CA_BUNDLE` env var. Once set, the OS trust store becomes the single source of truth for both system commands and certbot. - **Built-in scheduler**: certbot installs a `certbot.timer` systemd unit on most distros. `systemctl enable --now certbot.timer` is enough to wire renewal. ## Trusting your internal CA Certeasy's HTTPS certificate is signed by your internal ADCS root CA. ACME clients need to trust that CA, otherwise the TLS handshake fails before any certificate request can be made. The recommended approach is to **deploy your root CA to the OS trust store on all Linux servers** — ideally via your configuration management tool (Ansible, Puppet, Chef…). This is good practice regardless of Certeasy: any internal service using TLS with an internal CA benefits from it. ```bash # Debian / Ubuntu sudo cp internal-root-ca.pem /usr/local/share/ca-certificates/internal-root-ca.crt sudo update-ca-certificates # → consolidated bundle at /etc/ssl/certs/ca-certificates.crt # RHEL / CentOS / Rocky sudo cp internal-root-ca.pem /etc/pki/ca-trust/source/anchors/internal-root-ca.pem sudo update-ca-trust # → consolidated bundle at /etc/pki/tls/certs/ca-bundle.crt ``` With Ansible, this becomes a one-liner across your fleet: ```yaml - name: Deploy internal root CA copy: src: internal-root-ca.pem dest: /usr/local/share/ca-certificates/internal-root-ca.crt # adjust for RHEL notify: update-ca-certificates ``` **certbot does not use the OS trust store directly** — it uses Python's own CA bundle (`certifi`). Once your CA is in the OS trust store, you can point certbot to the system bundle file with `REQUESTS_CA_BUNDLE`, so both stay in sync automatically: ```bash # Debian / Ubuntu export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # RHEL / CentOS / Rocky export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt ``` For automated renewal, set this in certbot's systemd service: ```ini # /etc/systemd/system/certbot.service.d/override.conf [Service] Environment="REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt" ``` This way there is a single source of truth: the OS trust store. Update it, and certbot picks up the change automatically. ### `--no-verify-ssl` (testing only) :::danger Do not use in production `--no-verify-ssl` disables TLS certificate verification entirely. The client has no guarantee it is talking to your Certeasy instance — the connection could be intercepted. Acceptable for a quick local test, never for production or automated renewal. ::: ## HTTP-01 (standalone) The simplest approach: certbot spins up a temporary HTTP server on port 80 to answer the challenge. ```bash export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt certbot certonly \ --standalone \ --preferred-challenges http \ --server https://acme.corp.internal/acme/directory \ -d app.corp.internal ``` Certbot opens port 80, Certeasy fetches `http://app.corp.internal/.well-known/acme-challenge/`, and on success submits the CSR to ADCS. The signed certificate is written to `/etc/letsencrypt/live/app.corp.internal/`. ## HTTP-01 (webroot) If a web server is already running on port 80, use `--webroot` instead of `--standalone`: ```bash export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt certbot certonly \ --webroot -w /var/www/html \ --preferred-challenges http \ --server https://acme.corp.internal/acme/directory \ -d app.corp.internal ``` Certbot writes the challenge file under `/var/www/html/.well-known/acme-challenge/`. Your web server must serve that path over HTTP. ## DNS-01 (for wildcards) HTTP-01 and TLS-ALPN-01 cannot validate wildcard names (`*.corp.internal`) — RFC 8555 §8.4 limits them to DNS-01. ```bash export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt certbot certonly \ --manual \ --preferred-challenges dns \ --server https://acme.corp.internal/acme/directory \ -d "*.corp.internal" ``` certbot prompts you interactively to add the `_acme-challenge.corp.internal` TXT record. For automation, install a DNS plugin matching your provider (`certbot-dns-route53`, `certbot-dns-rfc2136`, …) and use `--dns-` flags instead of `--manual`. ## Renewal Once the certificate is issued, certbot can renew it automatically via the `certbot.timer` systemd unit: ```bash # Test renewal export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt certbot renew --dry-run # Enable automatic renewal systemctl enable --now certbot.timer ``` Renewal config is stored under `/etc/letsencrypt/renewal/.conf` and inherits the flags used at the initial `certonly` call. ## Revocation ```bash certbot revoke --cert-name app.corp.internal \ --server https://acme.corp.internal/acme/directory ``` `--cert-name` is the directory name under `/etc/letsencrypt/live/`. Add `--no-delete-after-revoke` if you want to keep the files on disk after revocation. ## With Caddy (automatic, no certbot) If you run Caddy as your reverse proxy, it can handle ACME directly — no certbot involvement, no renewal scripts to write: ``` { acme_ca https://acme.corp.internal/acme/directory acme_ca_root /path/to/your/internal-ca.pem } app.corp.internal { reverse_proxy localhost:8080 } ``` Caddy uses HTTP-01 by default for non-wildcard names. For wildcards, configure a DNS module from [caddy-dns](https://github.com/caddy-dns). --- # clients/lego.md --- sidebar_position: 1 title: lego --- # lego [lego](https://go-acme.github.io/lego/) is a single static Go binary with no runtime dependency — ideal for container images, CI runners, and minimal Linux installs. This page covers the lego-specific bits; for the general onboarding flow and trust-store setup see [First Certificate](../getting-started/first-certificate.md). :::info Documented for lego **5.x** (tested with 5.0.2) lego 5.0 introduced a CLI breaking change: every flag (`--server`, `--email`, `--domains`, `--http`, `--dns`, etc.) is now a **subcommand flag**, not a global flag. The subcommand (`run`, `renew`, `revoke`) must come first. The legacy `renew` and top-level `revoke` are gone — use `run --renew-force` and `certificates revoke`. If you are still on lego 4.x, the global-flag-first syntax of the old documentation applies; consider upgrading. ::: ## What changes vs certbot - **Key type**: lego generates **ECDSA P-256** keys by default for both the ACME account and the certificate. This sidesteps the `signature.min-rsa-bits` policy entirely. To force RSA, pass `--key-type rsa3072` (or `rsa4096`). `rsa2048` will be refused under the default `min-rsa-bits: 3072` policy. - **CSR EKU**: lego declares `serverAuth` only in its CSR — no `clientAuth` smuggling, no need to loosen `csr.allowed-extra-eku` on the policy. - **Trust store**: lego reads a single PEM file pointed at by the env var `LEGO_CA_CERTIFICATES`. Point it at your OS bundle to keep one source of truth: ```bash # Debian / Ubuntu export LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt # RHEL / CentOS / Rocky export LEGO_CA_CERTIFICATES=/etc/pki/tls/certs/ca-bundle.crt ``` - **No built-in scheduler**: unlike certbot's `certbot.timer` or acme.sh's `--install-cronjob`, lego expects you to wire renewal yourself via a systemd timer or cron job. :::note Command-line layout The `run`, `renew`, and `revoke` subcommands come **first**, then the flags that configure them (`--server`, `--email`, `--domains`, `--http`, `--dns`, ...). Only logging flags (`--log.level`, `--log.format`) and `--config` are global and may appear before the subcommand. ::: ## HTTP-01 (standalone) ```bash export LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt lego run \ --server https://acme.corp.internal/acme/directory \ --email ops@corp.internal \ --accept-tos \ --http \ --domains app.corp.internal \ --path /etc/lego ``` Cert + chain + key land under `/etc/lego/certificates/`: - `app.corp.internal.crt` — the full chain (leaf first, then any intermediates). - `app.corp.internal.key` — the private key. - `app.corp.internal.issuer.crt` — issuer cert only. `lego` opens port 80 inside the process to answer the HTTP-01 challenge, then exits. Port 80 must therefore be free at the moment of the call. On systems where another service holds port 80 permanently, see [HTTP-01 webroot](#http-01-webroot) below. ## HTTP-01 (webroot) When a web server is already serving on port 80, use the `webroot` solver instead. lego writes the challenge token to a directory of your choice; the web server only has to serve `/.well-known/acme-challenge/` from there. ```bash lego run \ --server https://acme.corp.internal/acme/directory \ --email ops@corp.internal \ --accept-tos \ --http.webroot /var/www/html \ --domains app.corp.internal \ --path /etc/lego ``` ## TLS-ALPN-01 ```bash lego run \ --server https://acme.corp.internal/acme/directory \ --email ops@corp.internal \ --accept-tos \ --tls \ --domains app.corp.internal \ --path /etc/lego ``` lego binds port 443 with the ACME-specific ALPN protocol; Certeasy probes the IP at port 443 with ALPN `acme-tls/1` to verify ownership. Useful when port 80 is unavailable but port 443 is free. ## DNS-01 (for wildcards) lego ships built-in plugins for ~80 DNS providers — no extra package needed. Pick the one matching your DNS, configure it via env vars, and pass `--dns `. :::warning Point the propagation check at your internal resolver By default, lego waits until the just-installed `_acme-challenge.` TXT record is visible through public resolvers (Cloudflare `1.1.1.1`, Google `8.8.8.8`) before notifying Certeasy. On an internal-only deployment that check **will never succeed** — the names do not exist publicly — and worse, the internal domain name is leaked in cleartext to those resolvers. Use `--dns.resolvers=` to point the propagation probe at your authoritative internal resolver instead. This keeps the sanity check (detects a misconfigured plugin) without leaking anything outside: ```bash lego run \ --dns rfc2136 \ --dns.resolvers '10.0.0.53:53' \ ... ``` If the propagation check is not desired at all (synchronous plugin, very short TTLs, etc.) pass `--dns.propagation.disable-ans` (authoritative nameservers) and/or `--dns.propagation.disable-rns` (recursive resolvers) to skip it entirely. `--dns.propagation.wait ` replaces the check with a fixed sleep when neither flag fits. ::: ```bash export LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt # Example: RFC 2136 / nsupdate against an internal Bind server export RFC2136_NAMESERVER='10.0.0.53' export RFC2136_TSIG_KEY='acme.' export RFC2136_TSIG_SECRET='' export RFC2136_TSIG_ALGORITHM='hmac-sha256.' lego run \ --server https://acme.corp.internal/acme/directory \ --email ops@corp.internal \ --accept-tos \ --dns rfc2136 \ --dns.resolvers '10.0.0.53:53' \ --domains '*.corp.internal' \ --path /etc/lego ``` For internal infrastructures without a public DNS provider, `rfc2136` against your own Bind/PowerDNS is usually the simplest path. The full list of providers is in lego's [DNS providers documentation](https://go-acme.github.io/lego/dns/). ## Renewal lego has no built-in scheduler. `lego run` itself issues or renews depending on whether the cert is already on disk — call it on a schedule (systemd timer, cron) and pass `--renew-days N` to actually do the renew only when the cert is within N days of expiration. Add `--renew-force` if you want to force a renew regardless: ```bash lego run \ --server https://acme.corp.internal/acme/directory \ --email ops@corp.internal \ --accept-tos \ --http \ --domains app.corp.internal \ --path /etc/lego \ --renew-days 30 ``` Minimal systemd setup (`/etc/systemd/system/lego-renew.service` + `lego-renew.timer`): ```ini # lego-renew.service [Unit] Description=Renew certificates via lego [Service] Type=oneshot Environment=LEGO_CA_CERTIFICATES=/etc/ssl/certs/ca-certificates.crt ExecStart=/usr/local/bin/lego run \ --server https://acme.corp.internal/acme/directory \ --email ops@corp.internal \ --accept-tos \ --http \ --domains app.corp.internal \ --path /etc/lego \ --renew-days 30 # lego-renew.timer [Unit] Description=Daily lego renewal check [Timer] OnCalendar=daily Persistent=true RandomizedDelaySec=4h [Install] WantedBy=timers.target ``` Enable with `systemctl enable --now lego-renew.timer`. ## Revocation ```bash LEGO_PATH=/etc/lego lego certificates revoke \ --server https://acme.corp.internal/acme/directory \ --email ops@corp.internal \ --cert.name app.corp.internal ``` `--cert.name` is the certificate's ID/name on disk — by default this is the first domain you passed at issuance. `certificates revoke` does not accept `--path` or `--domains`; pass the storage location via the `LEGO_PATH` environment variable instead. Revoke uses the account key — no need to present the certificate key separately. --- # configuration/adcs.md --- sidebar_position: 2 title: ADCS Configuration --- # ADCS Configuration :::caution Work in progress This page is not yet complete. Content and best practices will be added shortly. ::: This page will cover: - Prerequisites on the ADCS host - Creating a certificate template for ACME enrollment - Setting the correct permissions (enroll rights for the Certeasy service account) - Finding the correct `ca-name` value (`certutil -CA`) - Recommended template settings (key usage, validity, issuance requirements) - Security best practices (least-privilege service account, auditing, etc.) --- # configuration/authorities.md --- sidebar_position: 6 title: Authorities --- # Authorities Authorities are the PKI backends that Certeasy submits certificate requests to. Each authority represents one ADCS instance (or a fake PKI for testing). ## Configuration ```yaml authorities: - name: ca1 type: adcs configuration: ca-name: "PKI\\LAB-RootCA" certificate-template: "ACME-Template-Server" certreq-path: "C:\\Windows\\System32\\certreq.exe" default-timeout: 10m cert-util-timeout: 30s ``` ## Fields | Field | Required | Description | |---|---|---| | `name` | Yes | Unique authority name. Referenced in policy bindings. | | `type` | Yes | Authority type: `adcs` or `fake` | | `policies` | No | Remote authority policy constraints (advanced). If omitted, all local policies are candidates. | | `configuration` | Yes | Type-specific configuration block (see below) | ## ADCS Authority ### Configuration Fields | Field | Default | Description | |---|---|---| | `ca-name` | — | Full CA name as shown by `certutil -CA` (e.g. `PKI\LAB-RootCA`) | | `certificate-template` | — | ADCS certificate template name for ACME issuance | | `certreq-path` | `certreq.exe` | Full path to `certreq.exe` | | `default-timeout` | `10m` | Maximum wait time for ADCS to issue the certificate | | `cert-util-timeout` | — | Timeout for `certutil` operations | ### Finding your CA Name ```powershell certutil -CA ``` The output shows the CA name in the format `Machine\CAName`. Use this exact string in `ca-name`. ### Certificate Template Requirements The ADCS template must: - Allow enrollment by the Certeasy service account - Be configured for **Web Server** or equivalent (Server Authentication EKU) - Not have conflicting subject policies that would override the CSR :::tip Create a dedicated template for Certeasy (e.g. `ACME-Template-Server`) rather than reusing an existing one. This isolates the configuration and simplifies auditing. ::: ## Fake PKI Authority (Testing) The `fakepki` authority type is a built-in self-signed CA for local testing. It does not connect to any external system. ```yaml authorities: - name: test-ca type: fake configuration: common-name: "Certeasy Test CA" password: "testpassword" key-size: 2048 validity: 8760h ``` ### Fake PKI Configuration Fields | Field | Description | |---|---| | `common-name` | CN of the fake CA certificate | | `password` | Password for the CA key store | | `key-size` | RSA key size for the CA | | `validity` | Validity period for issued certificates | :::warning The `fake` authority is for development and testing only. Do not use it in production. ::: ## Multiple Authorities You can define multiple ADCS authorities for redundancy or to serve different policies: ```yaml authorities: - name: adcs-primary type: adcs configuration: ca-name: "PKI\\Primary-CA" certificate-template: "ACME-Server" - name: adcs-backup type: adcs configuration: ca-name: "PKI\\Backup-CA" certificate-template: "ACME-Server" ``` Then reference both in a [policy binding](/configuration/policy-bindings) with `strategy: first_available`. --- # configuration/database.md --- sidebar_position: 2 title: Database --- # Database Certeasy stores all ACME state (accounts, orders, challenges, certificates, audit logs) in a relational database. ## Supported Drivers | Driver | Key | Notes | |---|---|-----------------------------------------------------------------------------------------------------| | SQLite | `sqlite` | Default. No setup required. Recommended for single-node deployments. Do not supports multiple nodes | | PostgreSQL | `postgres` | Recommended for multi node deploymnent. | | SQL Server | `sqlserver` | For environments standardized on Microsoft SQL Server. | ## Configuration ```yaml database: driver: postgres dsn: "postgres://certeasy:secret@db01:5432/certeasy?sslmode=require" ping-timeout-sec: 5 max-idle-conn: 5 max-conn: 10 ``` ### SQLite (default) If `database` is omitted entirely, Certeasy uses SQLite at `%WORKDIR%/db.sqlite`. ```yaml # Explicit SQLite config database: driver: sqlite path: "C:\\ProgramData\\certeasy\\db.sqlite" ``` ### PostgreSQL ```yaml database: driver: postgres dsn: "postgres://certeasy:secret@db01:5432/certeasy?sslmode=require" ``` ### SQL Server ```yaml database: driver: sqlserver dsn: "sqlserver://certeasy:secret@sqlserver01:1433?database=certeasy" ``` ## Fields | Field | Default | Description | |---|---|---| | `driver` | `sqlite` | Database driver: `sqlite`, `postgres`, `sqlserver` | | `dsn` | — | Connection string (PostgreSQL and SQL Server) | | `path` | `%WORKDIR%/db.sqlite` | File path (SQLite only) | | `ping-timeout-sec` | `10` | Timeout for the startup connectivity check | | `max-idle-conn` | `2` (SQLite), `5` (others) | Maximum idle connections | | `max-conn` | `10` | Maximum open connections | ## Migrations Certeasy runs database migrations automatically at startup. Migrations are embedded in the binary — no external SQL files are needed. If the schema is already up to date, startup proceeds immediately. ## Schema Reference See [Schema Reference](/administration/schema) for the full list of tables and their lifecycle. --- # configuration/dns-profiles.md --- sidebar_position: 3 title: DNS Validation Profiles --- # DNS Validation Profiles DNS validation profiles define **how Certeasy resolves and validates DNS challenges**. Each profile controls which DNS zones are in scope, which resolver to use, and which resolved IP addresses are acceptable. ## Configuration ```yaml dns-validation-profiles: - name: internal-default mode: local zones: - suffixes: - "corp.internal" system: true authoritative: false dnssec: false protocol: udp resolved-ip-policy: allow-cidrs: - "10.0.0.0/8" deny-cidrs: - "127.0.0.0/8" - "169.254.0.0/16" - "::1/128" - "fe80::/10" ``` ## Fields ### Profile | Field | Default | Type | Description | |---|---|----------|------|---------------------------------------------| | `name` | — | string | Unique profile name. Referenced by issuance policies. | | `mode` | `local` | string | Validation mode. Only `local` is currently available. | | `timeout` | — | duration | Overall validation timeout. | ### Zones Each zone entry defines which DNS zones this profile handles and how to resolve them. | Field | Default | Type | Description | |---|---|-------------------------------------|--------------------------------------------------| | `suffixes` | — | List string | List of DNS zone suffixes (e.g. `corp.internal`) | | `system` | — | boolean | Use the system resolver for this zone | | `dns-server` | — | List string | Explicit DNS server address (overrides system) | | `authoritative` | — | boolean | Require authoritative responses | | `dnssec` | — | boolean | Require DNSSEC validation | | `protocol` | — | string | DNS protocol: `udp` or `tcp` | ### Resolved IP Policy After a challenge DNS name resolves, Certeasy checks the resulting IP against these rules. | Field | Type | Description | |---|---------------------------------------------------------------------|-------------------------------| | `allow-cidrs` | List string | IP ranges that are acceptable | | `deny-cidrs` | List string | IP ranges that are explicitly rejected (loopback, link-local, etc.) | Deny rules are evaluated first. If an IP matches a deny CIDR, the challenge fails regardless of allow rules. ## Multiple Profiles You can define multiple profiles for different DNS zones or resolution strategies: ```yaml dns-validation-profiles: - name: corp-internal mode: local zones: - suffixes: - "corp.internal" system: true resolved-ip-policy: allow-cidrs: - "10.0.0.0/8" - name: dmz mode: local zones: - suffixes: - "dmz.example.com" dns-server: "192.168.100.1" resolved-ip-policy: allow-cidrs: - "172.16.0.0/12" ``` When multiple profiles exist, each [issuance policy](/configuration/issuance-policies) must explicitly reference the profile to use. --- # configuration/issuance-policies.md --- sidebar_position: 4 title: Issuance Policies --- # Issuance Policies Issuance policies define **which certificate requests Certeasy will accept** and what constraints apply. Every order is evaluated against an issuance policy before any certificate is issued. ## Configuration ```yaml issuance-policies: - name: corp-server dns-validation-profile: internal-default dns: allow: - ".corp.internal/3" - "*.corp.internal" deny: - "=forbidden.corp.internal" signature: allowed-algorithms: - "RSA-SHA256" - "RSA-SHA384" - "RSA-SHA512" - "ECDSA-SHA256" - "ECDSA-SHA384" - "ECDSA-SHA512" - "ED25519" min-rsa-bits: 3072 allowed-ec-curves: - "P-256" - "P-384" ``` ## Fields | Field | Required | Description | |---|---|---| | `name` | Yes | Unique policy name | | `dns-validation-profile` | Conditional | Profile to use for challenge validation. Required if more than one profile exists. | | `dns.allow` | Yes | DNS scope rules (see below). Must not be empty. | | `dns.deny` | No | DNS names to explicitly reject | | `signature.allowed-algorithms` | No | Allowed signing algorithms. Empty = secure defaults. | | `signature.min-rsa-bits` | No | Minimum RSA key size. Default: `3072`. | | `signature.allowed-ec-curves` | No | Allowed EC curves. Empty = secure defaults. | ## DNS Scope Rules The `dns.allow` list controls which DNS names Certeasy will accept in a CSR. Each rule uses a compact grammar. ### Rule: Non-wildcard zone with depth limit **Syntax:** `.zone/N` Allows non-wildcard names under `zone` with at most `N` labels before the zone. ``` .corp.internal/2 ``` Allowed: `app.corp.internal`, `api.app.corp.internal` Rejected: `a.b.c.corp.internal` (3 labels), `*.corp.internal` (wildcard) ### Rule: Wildcard only at zone **Syntax:** `*.zone` Allows only the exact wildcard `*.zone`. Does not allow non-wildcard names. ``` *.corp.internal ``` Allowed: `*.corp.internal` Rejected: `app.corp.internal`, `*.sub.corp.internal` To allow both, combine two rules: ```yaml allow: - ".corp.internal/2" - "*.corp.internal" ``` ### Rule: Wildcard in subzones only **Syntax:** `*..zone/N` Allows wildcards inside subzones of `zone`, but not directly under `zone`. ``` *..corp.internal/2 ``` Allowed: `*.app.corp.internal` Rejected: `*.corp.internal` (directly under zone), `*.a.b.corp.internal` (too deep for `/2`) ### Rule: Exact match **Syntax:** `=name` Allows or denies an exact DNS name. ```yaml deny: - "=legacy.corp.internal" ``` ## DNS Name Normalization Before matching, all DNS names are: - Lowercased - Trailing dot removed - Rejected if they contain empty labels (`..`) or whitespace Pa## CSR Extension Whitelist (Extended Key Usage) Certeasy validates the contents of the CSR's `extensionRequest` strictly: only DNS-typed SANs and the Extended Key Usage extension (EKU, OID `2.5.29.37`) are accepted. By default the **only EKU value tolerated is `serverAuth`** (OID `1.3.6.1.5.5.7.3.1`) — the appropriate purpose for a public-server TLS certificate. To accept additional EKU values, opt in per policy: ```yaml issuance-policies: - name: lab-server csr: allowed-extra-eku: - clientAuth # - codeSigning # - emailProtection # - timeStamping # - ocspSigning # - anyPurpose # - "1.3.6.1.4.1.311.10.3.4" # raw OID also accepted ``` `serverAuth` is implicit and does not need to be listed. :::warning Security Adding entries to `allowed-extra-eku` lets ACME clients request certificates with non-server-TLS purposes through that policy. Whether the issued certificate actually carries those EKUs depends on the back-end CA template: - **ADCS templates configured as "Build from this Active Directory information"** ignore the CSR's EKU and apply the template's own. Adding entries here has no effect on the issued cert. - **ADCS templates configured as "Supply in the request"** honor the CSR's EKU. The issued cert will carry whatever the CSR asked for, as long as the template permits it. Only loosen this for policies whose authority you trust to enforce purpose constraints — e.g. a dedicated code-signing authority + template + audit trail. For the typical "Web Server" use case, leave it empty. ::: ### Note on `clientAuth` and the CA/B Forum baseline For most of TLS history, server certificates routinely declared both `serverAuth` and `clientAuth` in their Extended Key Usage. Some popular ACME clients still do this by default — notably **acme.sh**, whose built-in CSR template emits `extendedKeyUsage = serverAuth, clientAuth`. Without `clientAuth` in `allowed-extra-eku`, those CSRs are refused. The CA/B Forum's TLS Baseline Requirements **forbid this combination from June 2026 onwards**: a publicly-trusted server certificate must declare `serverAuth` only. Certeasy is most often deployed against an internal ADCS — outside the public WebPKI — so the rule is advisory rather than binding for your deployment, but mirroring the public-trust posture is good hygiene. Two practical positions: 1. **Strict (recommended for new deployments)**: leave `allowed-extra-eku` empty. Use lego or certbot, which emit `serverAuth` only by default. acme.sh works after a one-line override of its OpenSSL template. 2. **Pragmatic (existing acme.sh fleet)**: add `clientAuth` to `allowed-extra-eku` so existing scripts keep working, and plan a migration once the fleet has moved off acme.sh's default template. ## Signature Defaults If `signature` is omitted: - `min-rsa-bits`: `3072` - `allowed-algorithms`: when empty, all supported algorithms are accepted — `RSA-SHA256`, `RSA-SHA384`, `RSA-SHA512`, `ECDSA-SHA256`, `ECDSA-SHA384`, `ECDSA-SHA512`, `ED25519` - `allowed-ec-curves`: internal secure defaults (P-256, P-384) ## Multiple Policies You can define multiple issuance policies for different environments or certificate types: ```yaml issuance-policies: - name: corp-servers dns-validation-profile: internal dns: allow: - ".corp.internal/3" - name: dmz-servers dns-validation-profile: dmz dns: allow: - ".dmz.example.com/2" ``` When multiple policies exist, you must define explicit [policy bindings](/configuration/policy-bindings). --- # configuration/license.md --- sidebar_position: 10 title: License --- # License Configuration The `license` section controls optional online checks and auto-renew behavior. License activation is done with a CLI subcommand — either `certeasy license register ` (e.g. `CRT-…`) for online registration, or `certeasy license install ` to import a `.lic` file. On first start the server prints its **installation key** (`INST-…`) in the logs; you'll need it to download a `.lic` from the portal. See [Getting Started / License](../getting-started/license) for the full flow. ## Configuration ```yaml license: offline: false proxy-url: "http://proxy.corp.local:3128" timeout: 8s ``` ## Fields | Field | Default | Description | |---|---|---| | `offline` | `false` | If `true`, disables online check/renew and runs in offline-only mode | | `proxy-url` | empty | Optional explicit HTTP/HTTPS proxy URL for online license calls | | `timeout` | `30s` | HTTP timeout per online request | ## Online Mode (Default) Online mode is active when `offline` is not set (or set to `false`). Behavior: - startup always validates license offline first (signature + expiry) - background online checks run with adaptive cadence: - `> 30` days before expiry: every 30 days - `<= 30` days before expiry: every 24h - after failed online attempt: retry in 6h (or 1h near expiry) - if the backend returns a renewed `.lic`, Certeasy stores it in DB automatically - if backend is unreachable, Certeasy keeps running from offline validation - only explicit revocation response from backend is a hard failure The backend base URL is fixed to Certeasy's official endpoint in customer-facing deployments. ## Offline Mode (Air-Gapped) Set `offline: true`: ```yaml license: offline: true ``` In offline mode: - no outbound license HTTP calls are made - startup/runtime rely only on the locally stored license in DB - renewal is manual: import a new file with `certeasy license install` :::note `certeasy license register` requires online access and does not work when `offline: true`. Use `certeasy license install` for air-gapped environments. ::: --- # configuration/overview.md --- sidebar_position: 1 title: Overview --- # Configuration Overview Certeasy is configured with a single YAML file. The parser is strict: unknown fields, malformed YAML, and missing required relationships all cause startup to fail with an explicit error. ## Top-Level Sections | Section | Required | Description | |---|---|---| | [`server`](./server) | Yes | ACME HTTP server settings | | [`tls-certificate-manager`](./tls) | Yes | TLS certificate for the ACME endpoint | | [`dns-validation-profiles`](./dns-profiles) | Yes | DNS challenge validation settings | | [`authorities`](./authorities) | Yes | ADCS or fake PKI backends | | [`issuance-policies`](./issuance-policies) | Yes | Which names are allowed, key requirements | | [`policy-bindings`](./policy-bindings) | Conditional | Links policies to authorities | | [`database`](./database) | No | Database driver and connection settings | | [`license`](./license) | No | Optional online license checks and auto-renew | | [`logs`](../administration/logging) | No | Log level, format, output, per-service levels | | [`workers`](./workers) | No | Async job engine tuning | | [`rate-limiting`](./rate-limiting) | No | Per-IP, per-account, and duplicate-certificate rate limits | | [`renewal-info`](./renewal-info) | No | ACME Renewal Information (RFC 9773) — suggested renewal window | | [`audit`](../administration/audit) | No | Tamper-evident audit log (HMAC-chained JSONL) | | `workdir` | No | Base directory for runtime files | ## Runtime Model The configuration expresses a **policy pipeline**: ``` Incoming CSR │ ▼ issuance-policy ← selects allowed DNS scope and key requirements │ ├── dns-validation-profile ← controls how challenge DNS is resolved │ └── policy-binding ← selects which authority handles issuance │ ▼ authority ← ADCS or fake PKI ``` At runtime: 1. An issuance policy is selected based on the requested identifiers and CSR 2. The policy's DNS validation profile is used to validate challenges 3. On finalize, the policy binding selects an authority (with failover or round-robin) 4. The authority submits the CSR to ADCS ## Implicit Defaults Certeasy avoids requiring explicit configuration for common cases: - If `database` is omitted → SQLite at `%WORKDIR%/db.sqlite` - If `license` is omitted → online license mode with defaults (`certeasy.tech`, `30s`) - If `license.offline: true` → offline license mode - If `workers` is omitted → 4 workers with sensible backoff settings - If only one DNS profile exists → policies don't need to reference it explicitly - If exactly one policy and one authority exist → `policy-bindings` can be omitted entirely - If `rate-limiting` is omitted → safe defaults: 200 req/min/IP, 5 accounts/h/IP, 20 orders/h/account, 5 same-FQDN issuances per 7 days, 5 failed validations per (account, hostname) per hour, 30 in-flight pending authzs per account - If `renewal-info` is omitted → ARI is still active with default window (last third of cert lifetime, 48h wide, 6h `Retry-After`) - If `audit` is omitted → the tamper-evident audit log is enabled and writes to `/audit.log` with no in-process rotation (rotation delegated to the OS) ## `workdir` ```yaml workdir: "C:\\ProgramData\\certeasy" ``` Base directory for all runtime files: SQLite database, TLS certificate cache, log files (when `output: file`). | OS | Default | |---|---| | Windows | `%ProgramData%\certeasy` | | Linux | `/var/lib/certeasy` | All relative paths in other configuration sections (e.g. `database.path`, `local-pki-cache-dir`) are resolved relative to `workdir`. --- # configuration/policy-bindings.md --- sidebar_position: 5 title: Policy Bindings --- # Policy Bindings Policy bindings connect **issuance policies** to **authorities**. They define which ADCS backend(s) handle certificate requests for a given policy, and the selection strategy when multiple authorities are available. ## Configuration ```yaml policy-bindings: - policy: corp-server authorities: - ca1 - ca2 strategy: first_available ``` ## Fields | Field | Default | Description | |---|---|---| | `policy` | — | Name of the issuance policy | | `authorities` | — | List of authority names to use for this policy | | `strategy` | `first_available` | Selection strategy when multiple authorities are listed | ## Strategies ### `first_available` Certeasy tries the first authority. If it fails (unreachable, error), it moves to the next. This provides **failover**. ```yaml strategy: first_available ``` Use this when you have a primary CA and a backup. ### `round_robin` Certeasy distributes requests evenly across all listed authorities. This provides **load balancing**. ```yaml strategy: round_robin ``` Use this when you have multiple equivalent CAs and want to spread load. ## Implicit Binding If `policy-bindings` is omitted entirely and the configuration has **exactly one issuance policy and one authority**, Certeasy creates an implicit binding: - policy → the only issuance policy - authorities → the only authority - strategy → `first_available` This simplifies minimal configurations. As soon as you add a second policy or a second authority, you must declare bindings explicitly. ## Multiple Policies Example ```yaml policy-bindings: - policy: corp-servers authorities: - adcs-primary - adcs-backup strategy: first_available - policy: dmz-servers authorities: - adcs-dmz strategy: first_available ``` ## Validation Rules At startup, Certeasy verifies: - Every issuance policy has exactly one binding - Every authority referenced in a binding exists - No dangling or duplicate bindings --- # configuration/rate-limiting.md --- sidebar_position: 10 title: Rate Limiting --- # Rate Limiting Certeasy enforces several rate limits to protect the ACME endpoint from abuse and to prevent runaway clients from issuing thousands of certificates for the same names. All limits are configurable and individually disablable. The block lives at the top level of the configuration file. The defaults below apply when `rate-limiting` is omitted entirely — note the empty `whitelist`: **no IP bypasses rate limits unless you opt in explicitly** (secure by default). ```yaml rate-limiting: whitelist: # Empty — no bypass by default global: enabled: true requests-per-minute: 200 burst: 20 account-creation: enabled: true per-ip-per-hour: 5 burst: 2 order-creation: enabled: true orders-per-account-per-hour: 20 order-burst: 5 san-budget-per-account-per-hour: 100 duplicate-certificate: enabled: true max-per-window: 5 window: 168h failed-validation: enabled: true max-per-window: 5 window: 1h pending-authorizations: enabled: true max: 30 ``` ## How It Works Rate limits are enforced at five layers, in order: 1. **Global per-IP** — token bucket on every entry endpoint (`new-account`, `new-order`, `revoke-cert`, `renewal-info`). 2. **Operation-specific** — tighter caps on account creation (per IP) and order creation (per account). 3. **Duplicate Certificate** — DB-backed defense against repeat issuance for the same FQDN set. 4. **Failed Validation** — in-memory defense against clients with broken DNS / unreachable challenge targets. 5. **Pending Authorizations** — DB-backed cap on in-flight authzs per account. When a limit is hit, the server replies with HTTP 429 (`urn:ietf:params:acme:error:rateLimited`) and a `Retry-After` header. ## Whitelist **Empty by default.** Adding entries explicitly opts an IP or range out of IP-based limits — Certeasy never auto-trusts private RFC 1918 ranges or any other network. `whitelist` accepts both single IPs and CIDR ranges: ```yaml rate-limiting: whitelist: - "127.0.0.1" - "10.0.0.0/8" - "2001:db8::/32" ``` Any client whose IP matches a whitelist entry bypasses **all** IP-based limits (`global`, `account-creation`). Account-scoped limits (`order-creation`, `duplicate-certificate`, `failed-validation`, `pending-authorizations`) still apply — whitelisting an IP does not grant unlimited issuance to the account behind it. Use this sparingly: typical setups don't need a whitelist at all. The most common use case is whitelisting a known reverse-proxy CIDR when many clients share a frontend IP, so a single proxy doesn't get throttled by aggregate request volume. ## Global Per-IP token bucket applied to every ACME endpoint that accepts a connection. | Field | Default | Meaning | |---|---|---| | `enabled` | `true` | Set to `false` to disable the global limiter entirely | | `requests-per-minute` | `200` | Sustained rate per source IP | | `burst` | `20` | Maximum tokens accumulated when idle | A burst of `20` and a sustained rate of `200/min` lets a client issue 20 quick requests, then refill at ~3.3/sec. ## Account Creation Per-IP token bucket applied at `new-account`. Prevents an IP from registering an unbounded number of accounts. | Field | Default | Meaning | |---|---|---| | `enabled` | `true` | | | `per-ip-per-hour` | `5` | Sustained rate of new accounts per IP | | `burst` | `2` | Initial burst | ## Order Creation Per-account, with two **independent** quotas: | Field | Default | Meaning | |---|---|---| | `enabled` | `true` | | | `orders-per-account-per-hour` | `20` | Order count quota — 1 token per order, regardless of size | | `order-burst` | `5` | Initial burst on the order count quota | | `san-budget-per-account-per-hour` | `100` | SAN budget — N tokens per N-SAN order | The two quotas are independent: a multi-SAN order consumes more SAN budget but does not consume more burst on the order count. This lets a client issue a small number of large orders OR a larger number of small orders, but not both unboundedly. ## Duplicate Certificate Anti-runaway defense, **DB-backed**. Counts non-revoked certificates issued to an account for the same canonical FQDN set within a rolling time window. Targeted at the most damaging failure mode: a misconfigured client looping on the same domain. | Field | Default | Meaning | |---|---|---| | `enabled` | `true` | | | `max-per-window` | `5` | Maximum issuances per (account, FQDN set) per window | | `window` | `168h` (7 days) | Rolling window for the count | ### How the FQDN set is canonicalised Identifiers in the `newOrder` request are: 1. Lowercased 2. Trimmed of trailing dots 3. Validated as DNS names (LDH form) 4. Deduplicated 5. Sorted Wildcards are preserved (`*.example.com` ≠ `example.com`). The canonical list is hashed with SHA-256 and stored on the order; subsequent orders comparing the same hash count toward the limit. ### Revoked certificates are excluded Revoking a certificate frees a quota slot. This lets an operator legitimately re-issue after a key compromise without being locked out. ### Retry-After is precise When the limit is hit, `Retry-After` is computed from the oldest in-window certificate: once it falls out of the rolling window, one slot frees up. Clients that respect `Retry-After` will wake up exactly when issuance becomes possible again, not earlier. ### When to disable The duplicate-certificate limit is the primary protection against the "2000 certs for one site" scenario. Disabling it is reasonable only if: - You operate a fully internal PKI with trusted, well-behaved clients - You have alternative monitoring (e.g. cert volume alerts) in place Disable it by setting `enabled: false`. ## Failed Validation In-memory token bucket per `(account, hostname)`. Counts challenge failures (challenge transitioning to `invalid`) and refuses new authorizations for that pair once the cap is hit. Targets misconfigured clients with broken DNS, unreachable port 80, or wrong TLS-ALPN setup — without this, such clients endlessly retry and burn worker capacity. | Field | Default | Meaning | |---|---|---| | `enabled` | `true` | | | `max-per-window` | `5` | Maximum failed validations per (account, hostname) per window | | `window` | `1h` | Rolling window over which failures decay | ### How it works - A challenge transition to `invalid` records one failure. - The next `newOrder` request for the same hostname checks the counter: - If under the cap → order created normally. - If at the cap → HTTP 429 with `Retry-After` set to the time until at least one slot frees up. - Counters live in memory only — they are lost on restart, which is fine: a misconfigured client that survives a restart will rediscover its broken setup within a few seconds and the counter will refill. - Wildcards are separate from the base name (`*.example.com` and `example.com` have independent counters). ### Why in-memory and not DB-backed The window is short (1h) and the goal is to short-circuit live abuse, not to enforce a long-term quota. Tracking in memory avoids DB writes on the hot failure path; an in-memory miss after a restart costs at most one extra burst of failures before the counter rebuilds. ### Implementation note The counter increment runs **outside the database transaction** that marks the challenge invalid (via a `PostCommit` hook on the job effect). This avoids extending the SQLite write-lock duration with non-DB work. ## Pending Authorizations Caps the number of **in-flight** pending authorizations per account. An "in-flight" authz is one whose row in `acme_authorizations` has `status='pending'` AND has not yet expired. Targets clients that create orders without ever finalizing them — abandoned orders accumulate authz rows and waste storage. | Field | Default | Meaning | |---|---|---| | `enabled` | `true` | | | `max` | `30` | Maximum in-flight pending authzs per account | ### Why 30 by default The CertEasy deployment model is typically **one machine = one ACME account**. A single host issuing certificates for its own domains rarely has more than 5–10 pending authzs at once. 30 is generous for legitimate workflows and tight enough to catch runaway loops. For multi-tenant deployments where one account fronts many machines, raise the cap explicitly. ### Why expired authzs are excluded CertEasy does not auto-purge expired authzs from the database (they remain visible for audit). Counting them would mean an account that abandons a few orders gets locked out **permanently**. The check uses `expires_at IS NULL OR expires_at > now()` to count only rows that are actually still in flight. ### Retry-After If the cap is hit, `Retry-After` is the time until the soonest-expiring pending authz drops out of the count. The client wakes up exactly when one slot frees up. ## Tuning Recommendations | Scenario | Suggested change | |---|---| | Internal PKI, few clients | Increase `requests-per-minute` and `orders-per-account-per-hour`; keep `duplicate-certificate` enabled | | Many short-lived test environments | Lower `duplicate-certificate.max-per-window` to `2` to catch loops faster | | Public-facing service | Keep all defaults; do not whitelist anything unless you have a specific reason | | Behind a reverse proxy with shared egress IP | Configure `trusted-proxies` in `server` so client IPs are extracted correctly — the limiter will then key on real client IPs, not the proxy. Avoid whitelisting the proxy CIDR (it would let any client behind the proxy bypass IP limits) | --- # configuration/renewal-info.md --- sidebar_position: 11 title: Renewal Information (ARI) --- # ACME Renewal Information (RFC 9773) Certeasy implements [ACME Renewal Information (ARI)](https://datatracker.ietf.org/doc/html/rfc9773), the standard mechanism that lets the server tell ACME clients **when** to renew. Compliant clients (recent certbot, acme.sh, lego, Caddy, Traefik) honour the suggested window instead of relying on their own renewal heuristics. ARI is **always enabled** and exposes one new endpoint: ``` GET /acme/renewal-info/ ``` It is also advertised in the `/directory` response under the `renewalInfo` key. Clients discover the endpoint automatically. ## Why ARI Matters Without ARI, every client picks its own renewal threshold (typically "30 days before expiry"). When ten thousand certificates were all issued on the same day, ten thousand clients renew on the same day, hammering the CA. ARI lets the CA suggest a **per-certificate window** so renewals are spread out across days and the server can also signal an early renewal — for example, after a key compromise. ## Configuration ```yaml renewal-info: lifetime-fraction: 0.66 window-width: 48h retry-after: 6h ``` Omitting this section applies the defaults. | Field | Default | Meaning | |---|---|---| | `lifetime-fraction` | `0.66` | Fraction of the cert lifetime past which the suggested window opens. `0.66` means renewal is suggested in the last third of the certificate's validity. | | `window-width` | `48h` | Width of the suggested window. Spreads renewals over this interval to avoid thundering-herd reissue. | | `retry-after` | `6h` | Sent as the HTTP `Retry-After` header — tells clients how long to wait before polling `renewal-info` again. | There is no `enabled` flag. ARI is a recovery tool: a client cannot ignore renewal-info if you also need it to react to revocation. Disabling it would only encourage clients to fall back to their own heuristics, defeating the purpose. ## How the Window Is Computed For a non-revoked certificate: ``` lifetime = notAfter − notBefore start = notBefore + lifetime × lifetime-fraction end = min(start + window-width, notAfter) ``` For a **revoked** certificate, the window collapses to `[now, now]`. Compliant clients renew immediately. This makes revocation an effective rollover mechanism — operators no longer need to coordinate manual reissuance after a key compromise or template change. For a degenerate (zero-lifetime) certificate, the window is also `[now, now]`. ## Endpoint Details | Property | Value | |---|---| | Method | `GET` (no JWS) | | Path | `/acme/renewal-info/` | | Auth | None (public) | | Rate limit | Global per-IP only — see [Rate Limiting](./rate-limiting) | | Body format | RFC 9773 §4.2: `suggestedWindow.start` and `suggestedWindow.end` as RFC 3339 timestamps | | Response headers | `Retry-After: ` | The `certID` path parameter is the [RFC 9773 §4.1](https://datatracker.ietf.org/doc/html/rfc9773#section-4.1) format: ``` base64url(AKI) "." base64url(serialNumber) ``` Both components use unpadded base64url. AKI is the Authority Key Identifier extension of the leaf certificate; serial number is the cert's serial as unsigned big-endian bytes. ### Example response ```json { "suggestedWindow": { "start": "2026-08-15T00:00:00Z", "end": "2026-08-17T00:00:00Z" } } ``` ## Lookup Storage Each issued certificate is stored with its `aki` and `serial` (lowercase hex) in `acme_certificates`, with a composite index `(aki, serial)`. Lookup is O(log n). ## Compatibility Notes - Certificates issued by an authority that does not include an Authority Key Identifier extension are not addressable via ARI. The `renewal-info` endpoint will return 404 for them. Configure your ADCS template to include AKI (this is the default in modern ADCS). - Older ACME clients that do not implement ARI continue to work normally — the `directory.renewalInfo` field is simply ignored by them. --- # configuration/server.md --- sidebar_position: 7 title: Server --- # Server The `server` section configures the ACME HTTP endpoint — the address Certeasy listens on and the public URL exposed to ACME clients. ## Configuration ```yaml server: listen: ":8443" url: - "https://acme.corp.internal" read-header-timeout: 5s read-timeout: 10s write-timeout: 30s idle-timeout: 60s max-body-bytes: 1048576 shutdown-timeout: 30s remote-ip-header: "X-Forwarded-For" trusted-proxies: - "10.0.0.0/8" ``` ## Fields | Field | Default | Required | Description | |---|---|---|---| | `url` | — | Yes | Public URL(s) ACME clients use to reach Certeasy. Used to build all ACME directory links. | | `listen` | `0.0.0.0:8443` | Recommended | Address and port to listen on. | | `read-header-timeout` | `5s` | No | Timeout for reading request headers. | | `read-timeout` | `10s` | No | Timeout for reading the full request body. | | `write-timeout` | `30s` | No | Timeout for writing the response. | | `idle-timeout` | `60s` | No | Keep-alive idle connection timeout. | | `max-body-bytes` | `1048576` (1 MB) | No | Maximum request body size. | | `shutdown-timeout` | `30s` | No | Graceful shutdown wait time. Must be ≤ `workers.drain-timeout`. See [Graceful shutdown](../administration/shutdown.md). | | `remote-ip-header` | — | No | Header to trust for the client IP (e.g. `X-Forwarded-For`). Only used if `trusted-proxies` is set. | | `trusted-proxies` | — | No | CIDR ranges of trusted reverse proxies. | ## Notes ### `server.url` `url` is mandatory. It must match the hostname that ACME clients will use to reach Certeasy. Certeasy embeds this URL in the ACME directory response and in all object links (orders, authorizations, challenges). If you are behind a reverse proxy, set `url` to the public hostname, not the internal listen address. ```yaml server: listen: ":8443" # internal bind address url: - "https://acme.corp.internal" # public URL clients use ``` ### Behind a Reverse Proxy If Certeasy sits behind a reverse proxy (nginx, Caddy, IIS ARR…), set `remote-ip-header` and `trusted-proxies` to preserve the original client IP in logs and audit records: ```yaml server: remote-ip-header: "X-Forwarded-For" trusted-proxies: - "10.0.0.0/8" ``` Only proxies in `trusted-proxies` are allowed to set the `remote-ip-header`. Requests from untrusted IPs ignore the header. --- # configuration/tls.md --- sidebar_position: 8 title: TLS Certificate Manager --- # TLS Certificate Manager The `tls-certificate-manager` section configures the TLS certificate that Certeasy uses for its **own HTTPS endpoint** — not the certificates it issues to clients. Every hostname listed in `server.url` must be covered by exactly one bundle, or the server will not start. ## Configuration ```yaml tls-certificate-manager: bundles: - name: public hosts: - "acme.corp.internal" mode: files local-cert-file: "C:\\certeasy\\tls\\fullchain.pem" local-key-file: "C:\\certeasy\\tls\\privkey.pem" file-watch-interval: 60s ``` ## Bundles A bundle associates a set of hostnames with a TLS certificate source. At least one bundle is required. For an external name you can use a Let's Encrypt certificate; for an internal name you can use your ADCS certificate. ### Common fields | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Bundle identifier | | `hosts` | list of strings | Conditional | Hostnames this bundle serves. Can be omitted if there is only one bundle. | | `mode` | string | Yes | Certificate source: `files` or `pki` | ### `files` mode fields | Field | Type | Required | Description | |---|---|---|---| | `local-cert-file` | string | Yes | Path to the PEM certificate chain | | `local-key-file` | string | Yes | Path to the PEM private key | ### `pki` mode fields | Field | Type | Required | Description | |---|---|---|---| | `authority` | string | Yes | Name of the authority to use for auto-issuance and renewal | ## Modes ### `files` — Static Files Certeasy reads the certificate and key from disk. Use this when you manage the server certificate externally (e.g. via another ACME client or manual renewal). ```yaml bundles: - name: public mode: files local-cert-file: "C:\\certeasy\\tls\\fullchain.pem" local-key-file: "C:\\certeasy\\tls\\privkey.pem" ``` Certeasy watches the files for changes and reloads automatically (controlled by `file-watch-interval`). | Field | Default | Description | |---|---|---| | `file-watch-interval` | `5s` | How often to check for certificate file changes | ### `pki` — Auto-renewal via Internal PKI Certeasy issues and renews its own server certificate through one of its configured authorities. The certificate is cached locally. ```yaml bundles: - name: public mode: pki authority: ca1 ``` This is the recommended mode for fully automated certificate management. | Field | Default | Description | |---|---|---| | `acquire-timeout` | `2m` | Timeout to acquire a certificate at startup | | `renew-before` | `720h` (30 days) | How early to start renewal before expiry | | `pki-poll-interval` | `2s` | Polling interval when waiting for PKI issuance | | `local-pki-cache-dir` | `%WORKDIR%/server-certificate-cache` | Directory to cache PKI-issued server certificates | ## Multiple Bundles If you serve Certeasy on multiple hostnames, define one bundle per hostname group: ```yaml tls-certificate-manager: bundles: - name: internal hosts: - "acme.corp.internal" mode: files local-cert-file: "/etc/certeasy/tls/internal.pem" local-key-file: "/etc/certeasy/tls/internal.key" - name: dmz hosts: - "acme.dmz.example.com" mode: files local-cert-file: "/etc/certeasy/tls/dmz.pem" local-key-file: "/etc/certeasy/tls/dmz.key" ``` --- # configuration/workers.md --- sidebar_position: 9 title: Workers --- # Workers The `workers` section configures the **async job engine** that runs challenge validation and certificate issuance in the background. ## Configuration ```yaml workers: worker-id: "worker-1" workers: 4 lease: 30s idle-min: 50ms idle-max: 200ms base-backoff: 1s max-backoff: 2m queue-size: 4 drain-timeout: 30s ``` ## Fields | Field | Default | Description | |---|---|---| | `worker-id` | `worker` | Unique identifier for this worker instance. Useful in multi-node deployments. | | `workers` | `4` | Number of concurrent worker goroutines. | | `lease` | `30s` | How long a worker holds a job lock. If processing takes longer, the lease is renewed automatically. | | `idle-min` | `50ms` | Minimum polling interval when the queue is empty. | | `idle-max` | `200ms` | Maximum polling interval when the queue is empty. Caps the empty-queue exponential backoff so the first job that arrives after a long quiet period is picked up within this delay. | | `base-backoff` | `1s` | Initial backoff on job failure. | | `max-backoff` | `2m` | Maximum backoff after repeated failures. | | `queue-size` | value of `workers` | In-memory job queue buffer size. | | `drain-timeout` | `30s` | Maximum graceful-stop wait time for in-flight jobs before forced worker cancellation. Must be ≥ `server.shutdown-timeout`. See [Graceful shutdown](../administration/shutdown.md). | ## How the Job Engine Works All background work in Certeasy (DNS challenge validation, ADCS polling) is handled by the job engine: 1. An ACME handler enqueues a job in the database 2. A worker picks up the job and acquires a lease 3. The worker executes the job handler (validate DNS, poll ADCS…) 4. On success, the job is marked complete 5. On transient failure, the job is rescheduled with exponential backoff 6. On fatal failure, the job is failed and the associated order is invalidated Jobs are persistent — if Certeasy restarts mid-processing, workers resume from the database. ## Shutdown and Recovery - On **graceful stop** (`SIGTERM`), the dispatcher stops claiming new jobs, then workers drain in-flight jobs for up to `drain-timeout`. - If `drain-timeout` is exceeded, in-flight handlers are cancelled and process shutdown continues. - On **force kill** (`SIGKILL` / `kill -9`), no graceful cleanup runs. In-flight jobs remain locked until their lease expires, then are picked again by workers after restart. - In practice, worst-case recovery delay after force kill is approximately `lease`. ## Tuning The default settings (4 workers, 1s–2m backoff) work well for most deployments. Consider adjusting if: - **High certificate volume**: increase `workers` and `queue-size` - **Slow ADCS**: increase `max-backoff` and `lease` to tolerate longer processing times - **Multi-node**: set a unique `worker-id` per instance to distinguish workers in logs - **Many idle instances against a shared database (HA)**: raise `idle-max` to `1s`–`2s` to reduce the steady-state read load on the shared database. The defaults are tuned for a single-instance deployment, where the per-poll cost is negligible and tight polling keeps certificate-issuance latency low. ## Tuning Relationships - Set `drain-timeout` to cover normal in-flight processing time during maintenance restarts. - Keep `lease` long enough to avoid premature reclaim during transient slowdowns, while still allowing acceptable post-crash recovery time. - In orchestrators, configure termination grace period to be greater than both `server.shutdown-timeout` and `workers.drain-timeout` (plus margin). ## Multi-node Deployments Running multiple Certeasy instances against the same database is supported (PostgreSQL, SQL Server). Each instance competes for job leases — only one instance processes each job. Set `worker-id` to a unique value per instance: ```yaml # Node 1 workers: worker-id: "worker-node1" ``` ```yaml # Node 2 workers: worker-id: "worker-node2" ``` --- # getting-started/first-certificate.md --- sidebar_position: 3 title: First Certificate --- # First Certificate Goal: prove your Certeasy instance works end-to-end by issuing one certificate. We use **certbot** with **HTTP-01** because it's the safest "out of the box" combination — certbot's defaults (RSA 4096 key, `serverAuth` only EKU) line up with Certeasy's default policy without any tuning. If you already know which client you want to use, skip straight to its fiche: | Client | Best fit | Fiche | |---|---|---| | **certbot** | Linux servers, distro packages, widest docs | [ACME Clients → certbot](../clients/certbot.md) | | **lego** | Containers / CI, static binary, ECDSA default | [ACME Clients → lego](../clients/lego.md) | | **acme.sh** | Minimal systems, pure shell+curl, no Python/Go | [ACME Clients → acme.sh](../clients/acme-sh.md) | Each fiche covers HTTP-01 (standalone + webroot), DNS-01, renewal and revocation for that specific client, plus the gotchas (key types, trust store, EKU…). ## Prerequisites - Certeasy running and accessible at `https://acme.corp.internal` - certbot installed on the target machine - Port **80** reachable on the target machine from Certeasy's configured DNS resolver - The target machine's DNS name is allowed by your issuance policy - Your internal CA deployed to the OS trust store (see [Trusting your internal CA](../clients/certbot.md#trusting-your-internal-ca) on the certbot fiche) ## Issue your first certificate ```bash export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # adapt for RHEL certbot certonly \ --standalone \ --preferred-challenges http \ --server https://acme.corp.internal/acme/directory \ -d app.corp.internal ``` What happens: 1. certbot opens port 80 inside its process. 2. certbot POSTs an order to Certeasy. 3. Certeasy fetches `http://app.corp.internal/.well-known/acme-challenge/` and verifies the response. 4. Certeasy forwards the CSR to your back-end CA (ADCS or fakepki). 5. certbot writes the issued certificate under `/etc/letsencrypt/live/app.corp.internal/`. ## Check the result ```bash openssl s_client -connect app.corp.internal:443 -showcerts ``` You should see a certificate issued by your internal CA. ## Next steps - **Automate renewal**: `systemctl enable --now certbot.timer`. Full details on the [certbot fiche → Renewal](../clients/certbot.md#renewal). - **Wildcards**: HTTP-01 cannot validate `*.corp.internal`. Switch to DNS-01 — see [DNS-01 on the certbot fiche](../clients/certbot.md#dns-01-for-wildcards). - **Switch clients**: [lego](../clients/lego.md) for containers/CI, [acme.sh](../clients/acme-sh.md) for minimal systems. ## Troubleshooting If this minimal scenario fails, the [certbot fiche](../clients/certbot.md) has the comprehensive coverage. Three most common causes on a fresh setup: - **TLS handshake fails before any cert request**: the internal CA isn't in the OS trust store, and `REQUESTS_CA_BUNDLE` isn't set. Re-read [Trusting your internal CA](../clients/certbot.md#trusting-your-internal-ca). - **Challenge validation fails**: port 80 isn't reachable from Certeasy's host, or certbot can't bind it (run as root or grant `cap_net_bind_service`). - **DNS name rejected**: the target FQDN is outside the allow list of your [issuance policy](../configuration/issuance-policies.md). --- # getting-started/installation.md --- sidebar_position: 1 title: Installation --- # Installation Certeasy runs as a single binary. It targets **Windows Server** (to run close to your ADCS), but can also run on Linux for test environments. ## Requirements | Requirement | Detail | |---|---| | **OS** | Windows Server 2016+ (production), Linux (dev/test) | | **ADCS** | Active Directory Certificate Services, accessible from the Certeasy host | | **certreq.exe** | Available on Windows, used to submit CSRs to ADCS | | **Network** | Certeasy must be reachable by ACME clients (HTTPS, port 443 or custom) | | **Database** | SQLite (default, no setup), PostgreSQL, or SQL Server | :::warning Deployment topology Certeasy is supported as a **single-instance** deployment, or as **cold Active / Passive** with manual switchover (PostgreSQL or SQL Server required, no SQLite). Running two Certeasy instances concurrently against the same database is **not supported** and produces silent failure modes (`badNonce` errors, drifting rate limits, etc.). See [Deployment topology](../administration/deployment-topology.md) before deploying. ::: ## Download Download the latest release from the [releases page](https://github.com/certeasy-tech/certeasy/releases). Each release ships three binaries — `certeasy--linux-amd64`, `certeasy--darwin-arm64`, and `certeasy--windows-amd64.exe`. The Windows binary is a single executable, no installer or runtime dependencies. :::tip Verify your download Each release ships a `SHA256SUMS` file. Verify the integrity of the binary before running it — see [Verifying release binaries](../security/verifying-binaries.md). ::: ## Directory Layout Certeasy uses a **work directory** for runtime files (SQLite database, TLS cache, logs). The default locations are: - **Windows**: `%ProgramData%\certeasy` - **Linux**: `/var/lib/certeasy` Create the directory and make sure Certeasy's service account has write access. ```powershell # Windows New-Item -ItemType Directory -Path "C:\ProgramData\certeasy" ``` ```bash # Linux mkdir -p /var/lib/certeasy ``` ## Running as a Windows Service The recommended production setup is to run Certeasy as a Windows service using `sc.exe` or NSSM: ```powershell # Using sc.exe sc.exe create Certeasy binPath= "C:\certeasy\certeasy.exe -f C:\certeasy\config.yml" start= auto sc.exe description Certeasy "ACME server for internal ADCS" sc.exe start Certeasy ``` The service account must have: - Write access to the work directory - Access to `certreq.exe` (usually `C:\Windows\System32\certreq.exe`) - Network access to the ADCS host ## Running on Linux ```bash go run cmd/main.go -f config.yml # or ./certeasy -f config.yml ``` :::info The Linux binary cannot submit to ADCS (no `certreq.exe`). Use the **fake PKI** authority for local testing on Linux. ::: ## Next Step Once the binary is in place, [configure Certeasy](/getting-started/minimal-configuration). --- # getting-started/license.md --- sidebar_position: 2 title: License --- # License Certeasy requires a valid license file (`.lic`) to run. Free licenses are issued from [certeasy.tech/free](https://certeasy.tech/free). Paid licenses are sent by email after trial/purchase. ## License File Format The `.lic` file is a PEM-encoded text file: ``` -----BEGIN CERTEASY LICENSE----- Signature: -----END CERTEASY LICENSE----- ``` The payload contains your plan, the number of authorized ADCS authorities, and the expiry date. The signature is verified offline against a public key embedded in the binary. ## Identifiers Certeasy uses two human-readable keys, both in Crockford-base32 with a built-in check digit (no `I`, `L`, `O`, or `U`): | Key | Prefix | Example | Where it comes from | |---|---|---|---| | **License key** | `CRT-` | `CRT-EAYG2Q-QQBYYQ-VZHZ4M-5GWHNJ-V96MQX` | Issued on your account page; pass to `certeasy license register` | | **Installation key** | `INST-` | `INST-4RD63B-JE8MKM-MA5R51-DENCSA-52HJ6X` | Generated locally on first start; printed in the logs | Both keys are five groups of six characters; the last character is a checksum (Luhn mod-32 over Crockford-base32). The example values above intentionally end with `X` and **will not validate** — replace them with the real key shown on your account page or printed in your server logs. A mistyped license key is rejected at `certeasy license register` time with a clear error message before any network call is made. ## Activation Methods There are two ways to activate Certeasy: online registration or manual file import. ### Option 1 — Online Registration Register directly from the command line using your license key from [certeasy.tech/account](https://certeasy.tech/account). You need: - Your **license key** — available on your account page (shape: `CRT-XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX`) - A **deployment environment** label (`prod`, `dev`, `staging`, etc.) ```powershell # Windows certeasy.exe license register -f C:\certeasy\config.yml --env prod ``` ```bash # Linux ./certeasy license register -f /etc/certeasy/config.yml --env prod ``` The server name defaults to the machine hostname. Override it with `--env-name`: ```bash ./certeasy license register -f /etc/certeasy/config.yml --env prod --env-name my-server ``` Behavior of `certeasy license register`: - connects to certeasy.tech and registers the installation - downloads and stores the `.lic` in DB automatically - exits (does not start the ACME server) - `--env` is required; `--env-name` defaults to the machine hostname If this installation is already registered under a different license, the command fails with an error asking you to migrate via the portal. :::note `certeasy license register` requires online access to certeasy.tech. For air-gapped environments, use Option 2. ::: ### Option 2 — Manual File Import Download the `.lic` from [certeasy.tech/account](https://certeasy.tech/account) (you will need the installation key — see [Runtime Validation](#runtime-validation) below) and import it: ```powershell # Windows certeasy.exe license install -f C:\certeasy\config.yml C:\temp\certeasy.lic ``` ```bash # Linux ./certeasy license install -f /etc/certeasy/config.yml /tmp/certeasy.lic ``` Behavior of `certeasy license install`: - validates signature + expiry - writes the license to DB - exits (does not start the ACME server) If the import fails, the process exits with a non-zero code. ## Runtime Validation At startup, Certeasy validates the stored license offline (signature + expiry). No internet access is required for this step. If no license is installed, Certeasy logs your **installation key** and the available activation options. To activate: - run `certeasy license register` with your license key from the portal (online), or - import a `.lic` file with `certeasy license install` (offline-compatible) Startup fails by default without a license. Use `--grace` for a first-install grace window (7 days). If a license is expired: - startup is still allowed for 14 days (post-expiry grace) - after that, startup fails with `license has expired` ## Online Checks and Auto-Renew Certeasy can optionally run online checks and auto-renew by calling the backend refresh API. Online behavior is configured in `license` (see [Configuration / License](../configuration/license)). Default check cadence: - more than 30 days before expiry: every 30 days - 30 days or less before expiry: every 24h - after a failed online attempt: retry in 6h (or 1h near expiry) If the refresh endpoint is unreachable, Certeasy continues with offline validation. Only an explicit server revocation response is a hard failure. During post-expiry startup grace, online renewal can still recover the installation automatically if online checks are enabled. By default, online checks are enabled and target Certeasy's official backend. To force offline mode, set: ```yaml license: offline: true ``` ## Manual Renewal / Replacement To manually update a license (air-gapped, support-issued license, etc.), run `certeasy license install` again with the new file: ```bash ./certeasy license install -f /etc/certeasy/config.yml /tmp/new-certeasy.lic ``` For immediate effect on a running instance, restart the service after import. ## Checking License Status ```powershell # Windows — tail the Certeasy log Get-Content "C:\ProgramData\certeasy\certeasy.log" -Tail 20 ``` ```bash # Linux tail -20 /var/lib/certeasy/certeasy.log ``` On startup, Certeasy logs license details (`id`, `plan`, `max_cas`, holder, expiry, source). ## Troubleshooting **`WARNING: PRODUCT NOT REGISTERED`** No license is stored in the database. The startup logs print your **installation key** (`INST-…`) and the registration URL — use it to activate via `certeasy license register ` or download a `.lic` from the portal and import it with `certeasy license install`. Use `--grace` for an initial bootstrap grace period. **`invalid license: invalid license signature`** The provided `.lic` file is corrupted or was modified. **`license has expired`** License is beyond the post-expiry startup grace window. Import a renewed license. **`license has been revoked by the server`** The server explicitly revoked the license. Contact `contact@certeasy.tech`. **`installation already registered under a different license`** The installation key is already bound to a different license on the server. Go to [certeasy.tech/account](https://certeasy.tech/account) to migrate the installation before running `certeasy license register` again. --- # getting-started/minimal-configuration.md --- sidebar_position: 2 title: Minimal Configuration --- # Minimal Configuration Certeasy is configured with a single YAML file. This page shows the smallest valid configuration to get started. ## Config File Location Pass the config file explicitly: ```bash certeasy.exe -f C:\certeasy\config.yml ``` Without `-f`, Certeasy searches for `config.yml` / `config.yaml` in: 1. Current directory 2. Executable directory 3. Windows: `%PROGRAMDATA%\certeasy`, then user config directory 4. Linux: `$XDG_CONFIG_HOME/certeasy`, then `/etc/certeasy` ## Minimal Example This configuration relies on safe defaults wherever possible: ```yaml server: url: - "https://acme.corp.internal" listen: ":8443" tls-certificate-manager: bundles: - name: public mode: pki authority: ca1 dns-validation-profiles: - name: internal mode: local zones: - suffixes: - "corp.internal" system: true authorities: - name: ca1 type: adcs configuration: ca-name: "PKI\\LAB-RootCA" certificate-template: "ACME-Template-Server" issuance-policies: - name: corp-server dns: allow: - ".corp.internal/3" ``` ## What this configuration actually does In plain English: > Certeasy listens on port 8443 and exposes itself at `https://acme.corp.internal`. > It contacts `LAB-RootCA` (your ADCS) to obtain a certificate for that hostname using the `ACME-Template-Server` template, and renews it automatically before expiry. > It accepts ACME certificate requests for any name under `corp.internal` (up to 3 labels), validates challenges using the system DNS resolver, and forwards CSR signing to the same `LAB-RootCA`. The authority `ca1` plays **two roles** here: it secures Certeasy's own HTTPS endpoint **and** signs the certificates your ACME clients request. Both use the same ADCS CA and the same template. `ca-name` (`PKI\\LAB-RootCA`) is the name of your ADCS certification authority — the backslash-separated form is `\`. You can retrieve the exact value with `certutil -CA` on the ADCS host. `certificate-template` (`ACME-Template-Server`) is the name of the certificate template configured in ADCS for ACME enrollment. See [ADCS Configuration](../configuration/adcs) for how to set up the template and permissions. ## Workers Certeasy processes certificate orders (validation, CSR submission, renewals) through an internal job queue. By default, **4 workers** consume that queue in the background. You don't need to configure this for a standard deployment — the default handles the load of most environments. Workers are only worth tuning if you have a very high volume of concurrent requests. ## Implicit policy binding This configuration has exactly one policy (`corp-server`) and one authority (`ca1`). Certeasy connects them automatically — no `policy-bindings` section is needed. :::tip Think of it like a default route With a single destination, you don't need a routing table. As soon as you add a second authority (e.g. a pre-production CA), Certeasy can no longer guess which policy routes where — you'll need to declare `policy-bindings` explicitly at that point. ::: :::info How PKI-mode TLS works On first startup, Certeasy submits a CSR to your ADCS for a certificate covering `acme.corp.internal` (taken from `server.url`). The certificate is cached locally and renewed automatically before expiry. No manual certificate provisioning required. The issuance policy must cover the server hostname — `.corp.internal/3` handles `acme.corp.internal`. ::: ## What Each Section Does | Section | Purpose | |---|---| | `server` | ACME endpoint URL and listen address | | `tls-certificate-manager` | TLS certificate for the ACME HTTPS endpoint itself | | `dns-validation-profiles` | How Certeasy resolves and validates DNS challenges | | `authorities` | Your ADCS backend | | `issuance-policies` | Which DNS names are allowed, key requirements | ## Startup Checklist Before starting: - [ ] `server.url` is set to the hostname ACME clients will use - [ ] `ca-name` matches your ADCS CA exactly (check with `certutil -CA`) - [ ] `certificate-template` exists in ADCS and is configured for ACME enrollment - [ ] The service account has enroll permission on the template - [ ] Work directory is writable ## Next Step Once Certeasy starts successfully, follow the [First Certificate](/getting-started/first-certificate) guide to issue your first certificate. --- # intro/how-it-works.md --- sidebar_position: 2 title: How It Works --- # How It Works ## Architecture Overview ``` ACME Client (certbot, acme.sh, Caddy…) │ │ HTTPS RFC 8555 ▼ ┌─────────────────────────┐ │ Certeasy │ │ │ │ ┌─────────────────┐ │ │ │ ACME Server │ │ │ │ (HTTP handlers)│ │ │ └────────┬────────┘ │ │ │ │ │ ┌────────▼────────┐ │ │ │ Challenge │ │ │ │ Validator │◄───┼─── DNS / HTTP / TLS-ALPN │ └────────┬────────┘ │ │ │ │ │ ┌────────▼────────┐ │ │ │ Issuance │ │ │ │ Policy Engine │ │ │ └────────┬────────┘ │ └───────────┼─────────────┘ │ certreq.exe ▼ ┌───────────────┐ │ Your ADCS │ │ (unchanged) │ └───────────────┘ ``` ## Step-by-Step Flow ### 1. Account Registration The ACME client creates an account on Certeasy by submitting a JWK public key. Certeasy stores the account and issues a unique account URL. ### 2. Order Creation The client requests a certificate by submitting a list of DNS identifiers (e.g. `app.corp.internal`, `*.corp.internal`). Certeasy creates an order with one authorization per identifier. ### 3. Challenge Validation For each identifier, the client responds to a DNS-01, HTTP-01, or TLS-ALPN-01 challenge. Certeasy validates the challenge asynchronously using its configured DNS validation profile. The validation profile controls: - which DNS resolver to use - which DNS zones are in scope - which resolved IP ranges are allowed ### 4. Issuance Policy Selection Once all challenges pass, the client submits a CSR to finalize the order. Certeasy selects the appropriate **issuance policy** based on the requested identifiers. The issuance policy defines: - which DNS names are allowed - what key types and sizes are accepted - which ADCS authority handles the request ### 5. Certificate Issuance Certeasy submits the validated CSR to the configured ADCS authority using `certreq.exe`. The authority issues the certificate according to the configured template. This step is asynchronous — Certeasy polls ADCS until the certificate is ready. ### 6. Certificate Delivery The signed certificate (PEM chain, without private key) is stored and made available at the certificate URL. The client downloads it with a standard `GET` request. ## Async Job Engine Challenge validation and certificate issuance both run as **async jobs**. This decouples the ACME HTTP layer from the potentially slow operations of DNS validation and ADCS polling. Jobs are persisted in the database. If Certeasy restarts mid-operation, jobs resume where they left off. ## Security Model Certeasy enforces a strict security model at issuance time: - **Certificate identity limited to DNS**: SAN contains only the validated DNS names — no IP, UPN, or email - **No identity fields**: `O`, `OU`, `DC`, `L`, `ST`, `C` are forbidden in Subject - **Restricted EKU**: only Server Authentication (`1.3.6.1.5.5.7.3.1`) is allowed - **No UPN/email SAN**: prevents ADCS ESC attacks See [Security Model](/security/certificate-model) for full details. --- # intro/plans.md --- sidebar_position: 3 title: Plans & Pricing --- # Plans & Pricing ## Plans ### Free Ideal for small environments and proof-of-concept deployments. - **1 production installation** - **~25 managed servers** (distinct ACME accounts with at least one active certificate) - **1 ADCS production authority** - HTTP-01, DNS-01, TLS-ALPN-01 challenge validation - SQLite database ### Starter — €299 / year *(excl. VAT)* For small production environments. - **1 production installation** - **~250 managed servers** (distinct ACME accounts with at least one active certificate) - **2 ADCS production authorities** - HTTP-01, DNS-01, TLS-ALPN-01 challenge validation - SQLite database ### Pro — €499 / year *(excl. VAT)* For production environments and larger organizations. - **1 production installation** (Active/Passive included) - **Unlimited managed servers** - **3 ADCS production authorities** - PostgreSQL database - SQL Server support *(coming in V2)* - Dashboard *(coming in V4)* - Monitoring & alerting *(coming in V4)* ### Enterprise — €999 / year *(excl. VAT)* For organizations with advanced requirements. - Everything in Pro - **Up to 5 ADCS production authorities** - Split deployment: ADCS connector on Tier 0 + ACME responder on separate host *(coming in V2)* - Active/Active high availability (multi-node, requires PostgreSQL) *(coming in V2)* - Distributed validation agents (segmented networks) *(coming in V3)* :::tip Active/Passive high availability Active/Passive HA is a supported deployment pattern available to all Pro and above users — run two Certeasy instances against the same PostgreSQL database with a load balancer or keepalived in front. No additional license required. ::: - TLS service discovery *(coming in V4)* - Optional SLA Beyond 5 CAs — [contact us](https://certeasy.tech/contact). :::note License required A license file (`certeasy.lic`) is required to run Certeasy, including on the Free plan. Registration takes 30 seconds and delivers the file by email. **Managed server quota** is counted as the number of distinct ACME accounts with at least one active (non-expired, non-revoked) certificate. Retries and re-issuances from the same ACME account do not count. An account with no active certificate (failed setup, tests) does not consume quota. Plan quotas (managed server count, number of authorities, allowed database driver) are enforced by the binary at startup and on every new order. Renewals continue to work even when the configuration exceeds the plan, so existing clients are never interrupted by a downgrade. See the [License enforcement page](../administration/license-enforcement.md) for the full behaviour. ::: ## Evaluation period All paid plans include a **6-month free trial** — sign up, no card required, no automatic charge. At the end, you choose to subscribe for a year or simply stop. If you subscribe, a new license file is sent to your email. Replace the existing `certeasy.lic` on your server: no reinstallation, no configuration change. Your license is extended by one year from the trial expiry date, not from the payment date. On connected installations, auto-renewal can be configured so the binary fetches and replaces the file itself. On air-gapped servers, the manual file replacement is the only step required. [Start your free trial](https://certeasy.tech/trial) on the official site. :::note All prices exclude VAT. One activation slot = one ADCS CA fingerprint. Prices are locked — no unexpected increases. ::: --- # intro/roadmap.md --- sidebar_position: 4 title: Public roadmap --- # Public roadmap This page lists the major features Certeasy ships progressively across versions, what drives each one, and which plan unlocks it. Subscribers on annual plans **lock the price** when they sign up — new features unlock on the same subscription as they ship. The roadmap is a planning indication, not a contractual commitment. Versions and feature ordering may change based on customer feedback. Legend: ✅ shipped · 🎯 next release in flight. ## Features by version | Feature | Version | Plan(s) | Driver | |---|---|---|---| | ACME core (RFC 8555: account / order / authz / challenge / finalize / revoke) | 0.9 ✅ | All | Standard interop with any ACME client | | ARI read-only (RFC 9773 `renewalInfo` endpoint) | 0.9 ✅ | All | Lets clients pick their own renewal window | | HTTP-01 / DNS-01 / TLS-ALPN-01 challenges | 0.9 ✅ | All | Validation flexibility on every network topology | | ADCS bridge via `certreq.exe` + built-in fake PKI for testing | 0.9 ✅ | All | Core promise: bridge ACME to your existing ADCS | | SQLite (default), PostgreSQL and SQL Server backends | 0.9 ✅ | All / PostgreSQL and SQL Server on Pro+ | Operators pick the persistence they already operate | | Tamper-evident audit log (JSONL + HMAC chain + `audit verify`) | 0.9 ✅ | All | Compliance and forensic without DB lock contention | | SQLite backup CLI (`backup create` / `backup verify`) | 0.9 ✅ | All | Disaster recovery without a 3rd-party tool | | License enforcement (strict boot + acknowledgement) | 0.9 ✅ | All | Predictable cost ceiling, no surprise billing | | Graceful HTTP shutdown | 0.9 ✅ | All | Zero in-flight cert lost on `systemctl restart` | | RFC 8555 `Location` headers audit complete | 0.9 ✅ | All | Conformance with strict-RFC ACME clients (NativeClient, Caddy) | | Native ADCS bridge (drop `certreq.exe` / `certutil.exe` spawn, talk MS-WCCE directly) | 1.0 🎯 | All | Removes the LOLBin process chain that strict EDRs flag (Defender for Endpoint, CrowdStrike, SentinelOne) — eligible for stricter deployment perimeters | | Real ADCS revocation (CRL / OCSP propagation) | 1.0 🎯 | All | A revoked certificate is actually revoked end-to-end | | Cleanup / retention of expired ACME records | 1.0 🎯 | All | Long-term operations: the database stops growing forever | | Health / metrics endpoints (`/healthz`, `/readyz`, Prometheus `/metrics`) | 1.0 🎯 | All | Drop-in integration with existing supervision (Zabbix, Centreon, Prometheus, Grafana) | | PKI health checks + load-balanced CAs (Ping at boot + runtime) | 1.0 🎯 | All | Mis-configured CAs fail loudly at boot; `round_robin` policy actually skips unhealthy CAs | | ADCS lab documentation (template setup, EKU, SAN, permissions) | 1.0 🎯 | All | Customers can deploy without contacting support | | ARI `replaces` semantics (RFC 9773 §5: link, persist, collapse window) | 1.1 | All | Full benefit of ARI in multi-instance fleets | | Split deployment (Tier 0 connector + ACME responder on separate host) | 2.0 | Enterprise | Keep the ADCS-touching component on Tier 0, expose ACME elsewhere | | Active/Active high availability (multi-node) | 2.0 | Enterprise | Uptime without a manual failover step | | External Account Binding (EAB, RFC 8555 §7.3.4) | 2.0 | All | Multi-tenant DevOps deployments (per-team credentials) | | Distributed validators | 3.0 | Enterprise | Reach internal services that the central node cannot validate (split-DNS, restricted egress) | | Web dashboard | 4.0 | Pro / Enterprise | Quick operator view without parsing the audit log | | Monitoring & alerting templates (Grafana, Centreon) | 4.0 | Pro / Enterprise | Alert quick-start without writing your own queries | | TLS service discovery (probe + deployment status) | 4.0 | Enterprise | End-to-end loop: from "issued" to "actually deployed and serving" | ## Compliance and RFC gaps The RFC gaps documented in [Standards & RFC support](../reference/standards-compliance.md) (ADCS revocation propagation, EAB) are tracked in the table above. The "1.0 🎯" entries close the gaps that are visible to a standard ACME client today. ## Pricing and feature gating Each feature above is tagged with the plan that includes it and the version it ships in. See the [pricing page](https://certeasy.tech/#pricing) for the current line-up and the [plans documentation](./plans.md) for what each tier includes. Subscribe today on an annual plan to **lock the price** and follow the feature ramp without any annual increase. --- # intro/what-is-certeasy.md --- sidebar_position: 1 title: What is Certeasy? --- # What is Certeasy? Certeasy is an **on-premise ACME server** that bridges standard ACME clients (certbot, acme.sh, Caddy, Traefik…) with your internal **Active Directory Certificate Services (ADCS)** PKI. It lets you automate TLS certificate issuance inside your organization — without relying on any external cloud service, without exposing your PKI, and without changing your existing infrastructure. ## The Problem Active Directory takes care of Windows machines: certificates are deployed automatically through Group Policy, no one has to think about it. Linux servers, reverse proxies, load balancers, and containers are a different story. ADCS was never designed for them, so teams fill the gap however they can: - Certificates managed manually, renewed by hand → forgotten renewals, outages - Custom scripts around `certreq.exe` → fragile, hard to audit, breaks on updates - External CAs for internal services → certificates issued outside your network, outside your policies ## The Solution Certeasy sits between your ACME clients and your ADCS. It: 1. Exposes a standard ACME endpoint that any ACME client can talk to 2. Validates the DNS challenge to confirm ownership of the requested domain 3. Submits the CSR to your ADCS authority using `certreq.exe` 4. Returns the signed certificate to the ACME client Your ADCS never changes. Your ACME clients don't know they're talking to an internal CA. Everything stays inside your network. ## Key Properties | Property | Detail | |---|---| | **100% on-premise** | No data leaves your network | | **Standard protocol** | RFC 8555 ACME + RFC 9773 ARI (read-only) — works with any ACME client. See [Standards & RFC support](../reference/standards-compliance.md) for the detailed conformance matrix. | | **ADCS-native** | Uses `certreq.exe`, no ADCS changes required | | **Secure by default** | Conservative defaults: RSA 3072-bit minimum, strict algorithm allow-list | | **Hardened against ADCS attacks** | Certificate identity limited to validated DNS names — prevents ESC1–ESC13 by design | | **Isolated networks** | Supports segmented environments (v2) | | **Auditable** | Full audit log of all certificate operations | ## What Certeasy Is Not - Not a CA — it delegates issuance to your existing ADCS - Not a cloud service — it runs entirely inside your infrastructure - Not a replacement for your PKI — it automates access to it --- # reference/full-example.md --- sidebar_position: 1 title: Full Configuration Example --- # Full Configuration Example A complete configuration file with every available option. All optional fields are included with their default values or representative examples. Comments indicate which fields are required, optional, or mode-specific. ```yaml # Base directory for runtime files (SQLite, TLS cache, logs). # Default: %ProgramData%\certeasy (Windows) | /var/lib/certeasy (Linux) workdir: "C:\\ProgramData\\certeasy" # ── Database ────────────────────────────────────────────────────────────────── # Omit this section entirely to use SQLite with all defaults. database: driver: sqlite # sqlite | postgres | sqlserver path: "" # SQLite only — defaults to %WORKDIR%/db.sqlite dsn: "" # PostgreSQL and SQL Server connection string ping-timeout-sec: 10 max-idle-conn: 2 # Default: 2 (SQLite) | 5 (postgres/sqlserver) max-conn: 10 # ── Server ──────────────────────────────────────────────────────────────────── server: url: - "https://acme.corp.internal" # Public URL(s) for ACME clients — required listen: "0.0.0.0:8443" read-header-timeout: 5s read-timeout: 10s write-timeout: 30s idle-timeout: 60s max-body-bytes: 1048576 # 1 MB shutdown-timeout: 10s remote-ip-header: "X-Forwarded-For" # Only used when trusted-proxies is set trusted-proxies: - "10.0.0.0/8" # ── Logs ────────────────────────────────────────────────────────────────────── logs: level: info # debug | info | warn | error format: json # json | text output: file # stderr | stdout | file file: "C:\\ProgramData\\certeasy\\certeasy.log" rotate: max-size-mb: 100 max-backups: 10 services: # Per-service log level overrides DB-Driver: warn adcs: info Certeasy-acme-server: info Async-Acme-Pki-Handler: info Async-Acme-Challenges: info JWKS: warn worker: info http-server: info tags: # Free-form labels added to every log entry (Grafana/Loki) instance: cert-srv-01 region: eu-west # ── TLS Certificate Manager ─────────────────────────────────────────────────── tls-certificate-manager: acquire-timeout: 2m # pki mode only renew-before: 720h # pki mode only — 30 days pki-poll-interval: 2s # pki mode only file-watch-interval: 5s # files mode only local-pki-cache-dir: "%WORKDIR%\\server-certificate-cache" # pki mode only bundles: - name: public hosts: - "acme.corp.internal" mode: pki # files | pki authority: ca1 # pki mode — authority name # files mode fields (use instead of authority): # local-cert-file: "C:\\certeasy\\tls\\fullchain.pem" # local-key-file: "C:\\certeasy\\tls\\privkey.pem" # ── DNS Validation Profiles ─────────────────────────────────────────────────── dns-validation-profiles: - name: internal-default mode: local # local only (remote: not yet implemented) timeout: "" # overall validation timeout zones: - suffixes: - "corp.internal" system: true # use system DNS resolver dns-server: "" # explicit resolver (overrides system) authoritative: false dnssec: false protocol: udp # udp | tcp resolved-ip-policy: allow-cidrs: - "10.0.0.0/8" deny-cidrs: - "127.0.0.0/8" - "169.254.0.0/16" - "::1/128" - "fe80::/10" # ── Authorities ─────────────────────────────────────────────────────────────── authorities: - name: ca1 type: adcs # adcs | fake configuration: ca-name: "PKI\\LAB-RootCA" # as shown by certutil -CA certificate-template: "ACME-Template-Server" certreq-path: "certreq.exe" # full path if not in PATH default-timeout: 10m cert-util-timeout: 30s # Fake PKI for local testing — do not use in production # - name: test-ca # type: fake # configuration: # common-name: "Certeasy Test CA" # password: "testpassword" # key-size: 2048 # validity: 8760h # ── Issuance Policies ───────────────────────────────────────────────────────── issuance-policies: - name: corp-server dns-validation-profile: internal-default # required if multiple profiles exist dns: allow: - ".corp.internal/3" # non-wildcard names, max 3 labels - "*.corp.internal" # wildcard at zone root only deny: - "=forbidden.corp.internal" # exact match deny signature: allowed-algorithms: - "RSA-SHA256" - "RSA-SHA384" - "RSA-SHA512" - "ECDSA-SHA256" - "ECDSA-SHA384" - "ECDSA-SHA512" - "ED25519" min-rsa-bits: 3072 allowed-ec-curves: - "P-256" - "P-384" # CSR Extended Key Usage whitelist. Default: serverAuth only. # See SECURITY WARNING in configuration/issuance-policies.md before # adding non-server purposes — back-end ADCS templates configured as # "Supply in the request" will honor the CSR's EKU. # # clientAuth note: acme.sh's default OpenSSL template emits # EKU=serverAuth,clientAuth. The CA/B Forum baseline forbids this # combination on publicly-trusted certs from June 2026 onwards. Keep # the entry below ONLY if you must support unmodified acme.sh; lego # and certbot emit serverAuth only and don't need it. See # configuration/issuance-policies.md for the full discussion. # csr: # allowed-extra-eku: # - clientAuth # # - codeSigning # # - emailProtection # # raw OID also accepted, e.g. Microsoft EFS: # # - "1.3.6.1.4.1.311.10.3.4" # ── Policy Bindings ─────────────────────────────────────────────────────────── # Can be omitted when there is exactly one policy and one authority. policy-bindings: - policy: corp-server authorities: - ca1 strategy: first_available # first_available | round_robin # ── Workers ─────────────────────────────────────────────────────────────────── workers: worker-id: "worker" workers: 4 lease: 30s idle-min: 50ms idle-max: 200ms base-backoff: 1s max-backoff: 2m queue-size: 4 # defaults to value of workers # ── Rate Limiting ───────────────────────────────────────────────────────────── # Omit this section entirely to apply the defaults shown below. # Whitelist is intentionally empty — secure by default, no IP is auto-trusted. rate-limiting: whitelist: # OPTIONAL — entries bypass IP-based limits # Add only if you have a specific reason # (e.g. monitoring probes you control). # Example values (commented out by default): # - "127.0.0.1" # - "10.42.0.0/16" global: # Per-IP token bucket on every endpoint enabled: true requests-per-minute: 200 burst: 20 account-creation: # Per-IP cap on new-account enabled: true per-ip-per-hour: 5 burst: 2 order-creation: # Per-account caps on new-order enabled: true orders-per-account-per-hour: 20 order-burst: 5 san-budget-per-account-per-hour: 100 duplicate-certificate: # Anti-runaway: same FQDN set per account enabled: true # Set to false to disable entirely max-per-window: 5 window: 168h # 7 days failed-validation: # Anti-misconfig: bucket per (account, hostname) enabled: true # Counter lives in memory only max-per-window: 5 window: 1h pending-authorizations: # Anti-DoS: in-flight authzs per account enabled: true max: 30 # Calibrated for 1 machine = 1 account # ── Renewal Information (ARI, RFC 9773) ─────────────────────────────────────── # Always active — endpoint advertised in /directory as renewalInfo. # Omit this section to apply the defaults. renewal-info: lifetime-fraction: 0.66 # Window opens at notBefore + lifetime*0.66 window-width: 48h # Spread renewals across this duration retry-after: 6h # Sent as Retry-After header on responses # ── Audit log (HMAC-chained JSONL) ──────────────────────────────────────────── # Enabled by default. Omit this section to apply the defaults shown below. # Verify the chain with: certeasy audit verify -f config.yml audit: enabled: true path: "" # Empty → /audit.log rotate: max-size-mb: 0 # 0 → no in-process rotation (let logrotate / Task Scheduler handle it) max-backups: 0 # Ignored when max-size-mb is 0 ``` --- # reference/standards-compliance.md --- sidebar_position: 2 title: Standards & RFC support --- # Standards & RFC support Certeasy implements the IETF ACME family of standards. This page documents which parts of each RFC are supported today, which are partial, and which are planned. Use it when auditing Certeasy against a compliance requirement or before integrating an ACME client that depends on a specific feature. ## RFC 8555 — ACME core protocol **Status**: Supported with known gaps documented below. | Feature | RFC § | Status | |---|---|---| | Account create / contact update / key change / deactivate | §7.3 | ✅ | | `newOrder` and order state machine | §7.4 | ✅ | | Authorization + challenge dispatch (HTTP-01, DNS-01, TLS-ALPN-01) | §7.5, §8 | ✅ | | `finalize` + CSR validation | §7.4 | ✅ | | `revoke-cert` signed with account key | §7.6 | ✅ (server-side — see limitations) | | Wildcard issuance (`*.zone`, mixed `zone + *.zone` in one order) | §7.1.4, §8.4 | ✅ | | `Location` headers and response URLs canonicalization | §7.4 and following | ✅ | | External Account Binding (EAB) | §7.3.4 | 🔴 not supported in V0.9 / V1.0 — planned for V2.0 | ### Known limitations #### Server-side revocation only (full propagation planned for V1.0) `POST /acme/revoke-cert` marks the certificate revoked in Certeasy's database and emits an audit event. **The underlying ADCS CRL / OCSP responder is not yet updated** — a client validating chain status against ADCS will still see the certificate as valid until the CRL is published. Cabling the revocation all the way to ADCS (`certreq -revoke`) lands in V1.0. #### External Account Binding (EAB) — planned for V2.0 EAB lets you bind a new ACME account to an out-of-band identity (HMAC key shared via your provisioning system). Useful for multi-tenant DevOps deployments where each team is given its own credentials. Not implemented in V0.9 / V1.0 — single-tenant enterprise deployments do not need it. Tracked on the [roadmap](../intro/roadmap.md) for V2.0. ## RFC 9773 — ACME Renewal Information (ARI) **Status**: Partial — read-only ARI is supported, the `replaces` hint is silently accepted. | Feature | RFC § | Status | |---|---|---| | `renewalInfo` directory entry | §3 | ✅ | | `GET /acme/renewal-info/` with suggested window + `Retry-After` | §4 | ✅ | | `newOrder.replaces` validation + persistence + `renewalInfo` window collapse on the replaced cert | §5 | 🟡 field accepted silently — full semantics planned for V1.1 | ### Known limitations #### `replaces` semantics (planned for V1.1) ARI-aware clients (recent lego, certbot) can send `newOrder.replaces` without seeing a `400 malformed` — Certeasy accepts the field. However the server does not yet: - Reject duplicate `replaces` with `409 alreadyReplaced`. - Persist the `old_cert → new_cert` link. - Collapse the replaced certificate's `renewalInfo` window to force renewal of other clients holding the old cert. In a single-instance deployment this is invisible. In multi-instance or HA deployments the full ARI benefit is reduced until the semantics ship. ## RFC 5280 — X.509 certificates **Status**: Supported. - Issued certificates carry the **Subject Alternative Name** extension only, with DNS names from the validated authorizations. - The **Subject Common Name** field is intentionally left empty. This follows the CA/Browser Forum baseline since 2019: modern TLS clients (Chrome, Firefox, Go ≥ 1.15, Java ≥ 11) validate against SAN, not CN. Legacy clients that still require a CN-based match may need adjustment. - **Extended Key Usage**: `serverAuth` only. `clientAuth`, `codeSigning`, `anyPurpose` and other EKUs are never set, even if requested by the client's CSR. Certificates issued by Certeasy are TLS server certificates — never reusable for AD authentication, SMB signing, or other server-side roles. The strict EKU policy can be relaxed per-policy via `csr.allowed-extra-eku` if a specific client (e.g. acme.sh) declares additional EKUs in its CSR. - **Signature algorithms**: configurable allow-list per policy, default minimum RSA 3072-bit, ECDSA P-256 and above. ## Roadmap The remaining gaps on this page are tracked in the [public roadmap](../intro/roadmap.md) and target the next minor releases. Subscribe now (Pro, Enterprise) to lock the price while features ship progressively. --- # reference/test-coverage.md --- sidebar_position: 3 title: Test coverage --- # Test coverage Every Certeasy release ships only after the full automated test suite passes against the targeted database backends. This page lists the suites that compose the gate, what each one verifies, and the headline counts at the time of writing. ## Headline numbers | Category | Tests | What it verifies | |---|---|---| | Unit (TU) | **474** | Pure logic: configuration parsing, policy resolution, JWS signing primitives, DNS scope matching, CSR validation, crypto helpers, rate-limit decision tables, audit-line encoding. No I/O, no database. | | Integration (IT) | **63** | Real database (SQLite, PostgreSQL, SQL Server), real audit file on disk, real PKI request store, full ACME handler stack wired against the storage layer. Each test runs against every supported database backend. | | End-to-end (E2E) | **68** | The full Certeasy binary running as a subprocess. Two flavours: (1) CLI black-box — every subcommand (`serve`, `license`, `backup`, `audit verify`), exit codes, error messages. (2) ACME protocol — real third-party clients (lego, certbot, acme.sh) plus a RFC-strict native client driving certificate issuance, renewal, revocation, account lifecycle, key rollover, and the full error/security path. | | **Total** | **605** | | Numbers are refreshed at every release. The most recent count above reflects the **v0.9** line. ## What is covered, by area ### ACME protocol (RFC 8555 + RFC 9773) - Account: create, lookup-by-key, contact update, deactivate, key rollover (including the `409 Conflict` path when rolling to an already-used key). - Order: create, get, finalize, state machine transitions, expiry. - Authorization + challenges: HTTP-01, DNS-01, TLS-ALPN-01 — happy path and every documented failure mode. - Wildcards: pure `*.zone` and mixed `zone + *.zone` in a single order. - Renewal information (ARI): suggested window, `Retry-After`, revoked-cert collapse to immediate renewal. - Revocation: client-signed and account-key-signed paths, double-revoke rejection. - URL and header conformance: every endpoint where RFC 8555 requires a `Location` header is asserted on the wire. ### ACME client interoperability E2E suite runs the full happy-path issuance against: - **lego** — HTTP-01, DNS-01, TLS-ALPN-01. - **certbot** — HTTP-01, DNS-01. - **acme.sh** — HTTP-01, DNS-01, TLS-ALPN-01. - A built-in RFC-strict native client (`golang.org/x/crypto/acme`) for paths the third-party CLIs do not exercise: error-path, security probes, account lifecycle ops, RFC URL/header conformance. ### Database support The integration suite runs the same test set against every supported backend: - **SQLite** — always. - **PostgreSQL** — when configured in the CI environment. - **SQL Server** — when configured in the CI environment. Schema migrations, concurrent-writer behaviour (serialisable retry), and dialect-specific edge cases (UUID handling, NULL semantics in unique indexes, cascade chain restrictions on SQL Server) are all covered. ### Rate limiting Dedicated suite (7 tests) under a tight rate-limit profile, covering global denial, account-creation throttling, order-creation throttling, duplicate-certificate refusal, failed-validation back-off, and the pending-authorization cap. ### Audit log - Round-trip write + verify on every supported database backend. - HMAC chain anchoring on `installation_id`. - Recovery across process restart, including rotated files. - Tampering detection (line removed, line modified, MAC altered, wrong installation_id). - Every protocol event (account create / key change / deactivate, order create / finalize / invalid, authorization & challenge validate, certificate issue / revoke, rate-limit deny, license deny) is asserted to fire exactly once at the right point in the request lifecycle. ### License enforcement - Boot-strict refusal (no license, expired license, wrong environment). - Runtime per-order enforcement (`max_managed_servers`, `max_cas`, `allowed_dbs`, `max_managed_servers=0`). - Renewal escape hatch (an order for a domain already covered by an active certificate is not counted against the cap). - Acknowledgement path for boot-degraded states. ### CLI Every subcommand and flag is exercised in the E2E suite: argument validation, exit codes, error messages on missing file / bad format / incompatible flag combinations, help output for every command level. ### Backup & restore - SQLite snapshot round-trip with schema verification. - Integrity check (`quick` and `full`). - Refusal to overwrite an existing target. - Verify against a corrupted file / missing tables / wrong driver. ### Cross-platform The suite is run on Linux for every release. The protocol suite is additionally run on Windows when an ADCS lab is available (the ADCS backend variant is gated on a Windows host with `certreq.exe`). ## How the categories are defined Classification is purely based on the file path of the test, so the numbers are reproducible without judgement calls: - **End-to-end (E2E)** — tests that run the Certeasy binary as a subprocess and assert on the wire or the CLI output. - **Integration (IT)** — tests that hit a real database, write a real audit file to disk, or wire the full handler stack against a real storage backend. - **Unit (TU)** — every other test: pure-function logic with no I/O. A test that touches both a database and a real subprocess counts as E2E (the more demanding category wins). ## Where the numbers come from The full suite is launched from the repository root with a single script that records pass/fail/skip per module and writes per-module logs for diagnostics. The headline numbers above are produced by enumerating `go test -list` per module and classifying each test by its package path. --- # security/TODO.md --- sidebar_position: 99 title: Security TODO --- # Security TODO This page tracks security mitigations that are **claimed in the documentation but not currently enforced by Certeasy code**. These are either operator responsibilities or planned enforcement features. --- ## ESC12 — Short Validity Periods **Claim in docs**: "Short validity periods" is listed as a Certeasy mitigation. **Reality**: For ADCS authorities, certificate validity is controlled entirely by the ADCS template configuration. Certeasy does not inspect or enforce validity periods on ADCS-issued certificates. The 90-day limit only exists in the `fake` (test) PKI backend. **Gap**: An ADCS template could issue 10-year certificates and Certeasy would accept and serve them without complaint. **Proposed fix**: Add an optional `max-validity` field to authority or issuance-policy config. If the issued certificate's `NotAfter` exceeds the configured maximum, Certeasy refuses to store and return it. --- ## ESC3 — No Enrollment Agent Templates **Claim in docs**: "No Enrollment Agent templates used." **Reality**: This is purely an operator configuration responsibility. Certeasy does not inspect or validate the ADCS template it is configured to use. Nothing prevents an operator from pointing `certificate-template` at an Enrollment Agent template. **Gap**: No validation that the configured template is safe. **Proposed fix**: Document clearly as operator responsibility (not a Certeasy enforcement). Optionally: warn or refuse if the issued certificate contains Enrollment Agent EKU (`1.3.6.1.4.1.311.20.2.1`). --- ## ESC4/ESC5 — Enrollment Permissions Restricted to Service Account **Claim in docs**: "Enrollment permissions restricted to service account." **Reality**: Architectural — Certeasy calls `certreq.exe` under its own service account, so clients never authenticate to ADCS directly. However, Certeasy cannot verify or enforce what permissions the service account has on the ADCS template. A misconfigured service account with Write/Manage permissions on the template would be a risk Certeasy cannot detect. **Gap**: No validation of service account permissions. **Proposed fix**: Document as operator responsibility. Consider adding a startup check that verifies the service account only has Enroll (not Manage/Write) on the ADCS template, using `certutil -v -template`. --- ## ESC12 — Full Audit Logging **Claim in docs**: "Full audit logging." **Reality**: Certeasy logs certificate operations (submission, issuance, revocation) via its structured logger. However, there is no dedicated, tamper-evident audit log file separate from the application log. Log integrity depends entirely on the operator's log aggregation and storage setup. **Gap**: No dedicated audit log with guaranteed durability/integrity. **Proposed fix**: Already tracked separately — the `acme_audit_logs` database table stores all actions. Consider exposing a dedicated audit log export endpoint or file. --- ## Notes Mitigations **fully enforced in Certeasy code** (not listed here): - Subject validation: only empty or `CN=` allowed (`common/x509v/certificate_context.go`) - SAN validation: DNS names only, no otherName/UPN/email/IP/URI (`common/x509v/certificate_context.go`) - EKU: forced to Server Authentication, not configurable by clients - Template selection: not exposed to ACME clients - CSR structural validation: no ASN.1 smuggling, duplicate extensions, etc. --- # security/certificate-model.md --- sidebar_position: 1 title: Certificate Security Model --- # Certificate Security Model Certeasy enforces a strict certificate identity model at issuance time. This behavior is **mandatory, non-configurable, and secure by default**. ## Core Principle > ACME proves **control over a DNS identifier** — nothing else. ACME does not prove organizational identity, user identity, Active Directory account ownership, or authorization to authenticate to AD. Any certificate content beyond validated DNS names cannot be justified by the ACME protocol. ## Subject Rules ### What is allowed - An **empty Subject**, or - `CN = one of the validated DNS names` ### What is forbidden All other Subject fields are rejected: | Field | Reason | |---|---| | `O` (Organization) | Identity claim — not proven by ACME | | `OU` (Organizational Unit) | Identity claim — not proven by ACME | | `DC` (Domain Component) | AD-specific — can influence authentication | | `L`, `ST`, `C` | Identity/location claims | | Any custom RDN | Not justified by DNS validation | In Windows and ADCS environments, Subject fields influence certificate-to-account mapping and authentication flows. Allowing arbitrary Subject attributes reintroduces identity confusion and privilege escalation risk. ## Subject Alternative Name Rules - SAN **must** be present - SAN entries **must** be `dNSName` only - DNS names **must** match ACME-validated identifiers ### Forbidden SAN types | Type | Reason | |---|---| | `otherName` (UPN / msUPN) | Enables AD account impersonation | | `rfc822Name` (email) | Identity claim | | `uniformResourceIdentifier` | Not proven by ACME | | `iPAddress` | Not validated via DNS challenge | ## Extension Rules ### Allowed extensions | Extension | OID | Constraint | |---|---|---| | Subject Alternative Name | `2.5.29.17` | DNS names only, no duplicates | | Extended Key Usage | `2.5.29.37` | Server Authentication (`1.3.6.1.5.5.7.3.1`) only | EKU values are **forced by policy**. CSR-provided EKU values are ignored or rejected. ### Forbidden extensions All extensions not in the allow-list are rejected, including: - `Any Purpose` EKU - `Client Authentication` EKU - `Smartcard Logon` - `IP Security` EKUs - `Certificate Policies` - `Name Constraints` - `Authority Information Access` - `CRL Distribution Points` - Microsoft-specific extensions ## CSR Structural Validation To prevent ASN.1 smuggling and parsing ambiguity: - Exactly one `extensionRequest` attribute (`1.2.840.113549.1.9.14`) - No other CSR attributes - Exactly one SAN extension - No duplicate extensions - No trailing or unused ASN.1 bytes - Full DER consumed - Valid CSR signature Any deviation results in rejection. ## Why This Is Not Configurable Security boundaries must be enforced in code. Allowing configuration to relax identity or extension rules would: - Shift responsibility to operators - Increase misconfiguration risk - Complicate audits - Reintroduce known ADCS vulnerabilities Certeasy enforces a single safe issuance model. --- ## ADCS ESC Attack Mitigations The enforced rules prevent entire classes of ADCS certificate-based attacks (ESC1–ESC13). ### ESC1 — User-Supplied Subject or SAN with Client Authentication **Attack**: Requester controls Subject or SAN (e.g. UPN) and obtains a certificate usable for AD authentication. **Mitigations**: No user-supplied Subject identity. No `otherName`/UPN in SAN. EKU restricted to Server Authentication only. --- ### ESC2 — Any Purpose EKU Abuse **Attack**: A certificate with `Any Purpose` EKU is used for unintended authentication. **Mitigations**: `Any Purpose` EKU explicitly forbidden. EKU forced to Server Authentication only. --- ### ESC3 — Enrollment Agent Abuse **Attack**: Enrollment Agent certificates allow requesting certificates on behalf of other users. **Mitigations**: - No delegation of enrollment authority — ACME clients never authenticate to ADCS directly *(architectural)* - ⚠️ **Operator responsibility**: do not configure `certificate-template` to point at an Enrollment Agent template. Certeasy does not validate the template type. --- ### ESC4 / ESC5 — Dangerous CA or Template Permissions **Attack**: An attacker modifies CA or template permissions to issue malicious certificates. **Mitigations**: - Template selection not exposed to ACME clients — enforced in code, clients cannot influence which template is used - Enrollment runs under the Certeasy service account *(architectural)* - ⚠️ **Operator responsibility**: create a dedicated ADCS template for ACME issuance and grant only Enroll permission to the Certeasy service account --- ### ESC6 — UPN Injection via SAN **Attack**: A certificate contains a UPN in SAN, enabling authentication abuse. **Mitigations**: `otherName` SAN types explicitly forbidden. DNS-only SAN enforcement. --- ### ESC8 — NTLM Relay to ADCS **Attack**: NTLM authentication to ADCS is relayed to obtain certificates as another identity. **Mitigations**: ACME service does not expose ADCS enrollment endpoints. ACME clients never authenticate directly to ADCS. --- ### ESC9 / ESC10 — Weak or Legacy Certificate Mapping **Attack**: Certificates map to AD accounts via weak identifiers (CN, email, legacy rules). **Mitigations**: No email, UPN, or URI SANs. Minimal Subject. No identity-bearing attributes. --- ### ESC11 — Web Enrollment Abuse **Attack**: ADCS Web Enrollment interfaces abused for unauthorized issuance. **Mitigations**: Web Enrollment not used. Enrollment performed by controlled service account only. --- ### ESC12 — Long-Lived Misissued Certificates **Attack**: Misissued certificates remain valid for long periods. **Mitigations**: - All certificate operations are recorded in the audit log (`acme_audit_logs`) *(enforced)* - ACME protocol supports automated renewal — clients can request new certificates before expiry *(architectural)* - ⚠️ **Operator responsibility**: configure the ADCS template with a short validity period (30–90 days recommended). Certeasy does not currently enforce a maximum validity on ADCS-issued certificates. See [Security TODO](/security/TODO). --- ### ESC13 — Cross-Forest Certificate Abuse **Attack**: Certificates trusted across forests allow lateral movement. **Mitigations**: EKU restricted to Server Authentication. No user or machine authentication EKUs. No identity-bearing Subject or SAN fields. --- ## Lifecycle Protections The rules above apply at issuance time. Two additional protections operate around the certificate's lifetime: ### Anti-DoS: Pending Authorizations Cap Clients that create orders but never finalize them leave behind pending `acme_authorizations` rows. Without a cap, this is a silent storage-growth DoS. Certeasy refuses new orders when the account already has too many in-flight pending authzs. | Property | Default | Configurable | |---|---|---| | Max in-flight | 30 | `rate-limiting.pending-authorizations.max` | | Disable | — | `rate-limiting.pending-authorizations.enabled: false` | The default of 30 is calibrated for the typical "one machine = one ACME account" model where a real client rarely has more than 5–10 pending authzs at once. Expired authzs are excluded from the count so abandoned orders don't lock the account out forever. See [Rate Limiting](../configuration/rate-limiting#pending-authorizations). ### Anti-Misconfig: Failed Validation Limit A misconfigured ACME client (broken DNS, port 80 closed, wrong TLS-ALPN) will keep retrying validations indefinitely, burning CA worker capacity. Certeasy keeps an in-memory counter per `(account, hostname)` and refuses new authorizations once that counter is at cap. | Property | Default | Configurable | |---|---|---| | Cap | 5 failed validations | `rate-limiting.failed-validation.max-per-window` | | Window | 1h | `rate-limiting.failed-validation.window` | | Disable | — | `rate-limiting.failed-validation.enabled: false` | The counter decays continuously, so a transient outage that produces a few failures clears within minutes. The check at order-creation time is non-consuming — only actual challenge failures count. See [Rate Limiting](../configuration/rate-limiting#failed-validation). ### Anti-Runaway: Duplicate Certificate Limit A misconfigured or compromised ACME client can loop on the same domain and burn through CA resources — the "2000 certs for one site" failure mode. Certeasy caps repeat issuance of the same FQDN set per account within a rolling time window. | Property | Default | Configurable | |---|---|---| | Cap | 5 issuances | `rate-limiting.duplicate-certificate.max-per-window` | | Window | 168h (7 days) | `rate-limiting.duplicate-certificate.window` | | Disable | — | `rate-limiting.duplicate-certificate.enabled: false` | The set is canonicalised (lowercased, sorted, deduplicated, wildcards preserved) and hashed; the count uses an indexed DB lookup. **Revoked certificates are excluded** so legitimate post-revocation reissuance is not blocked. When the limit is hit, the response is HTTP 429 with a precise `Retry-After` (the moment the oldest in-window certificate falls out of the window). See [Rate Limiting](../configuration/rate-limiting#duplicate-certificate). ### Forced Renewal via ARI Certeasy implements ACME Renewal Information (RFC 9773). For a **revoked** certificate, the suggested renewal window collapses to `[now, now]`, instructing compliant clients (recent certbot, acme.sh, lego, Caddy, Traefik) to renew immediately. This makes revocation a usable rollover tool for key compromise, template misconfiguration, or rotation. For non-revoked certificates, ARI spreads renewals across a configurable window in the last third of the certificate's lifetime, avoiding thundering-herd reissue across thousands of clients. See [Renewal Information](../configuration/renewal-info). --- # security/dependencies.md --- sidebar_position: 2 title: Dependencies & SBOM --- # Dependencies & SBOM Certeasy is a Go binary with a small, auditable dependency tree. This page lists the direct runtime dependencies, explains how to generate a Software Bill of Materials (SBOM), and covers compliance requirements under the EU **Cyber Resilience Act (CRA)** and **NIS2 Directive**. ## Direct Dependencies | Package | Purpose | License | |---|---|---| | `github.com/miekg/dns` | DNS resolver for challenge validation | BSD-3-Clause | | `golang.org/x/crypto` | TLS, PKCS8, cryptographic primitives | BSD-3-Clause | | `golang.org/x/net` | HTTP/2, IDNA, DNS utilities | BSD-3-Clause | | `golang.org/x/sync` | Concurrency primitives | BSD-3-Clause | | `modernc.org/sqlite` | SQLite driver (pure Go, CGO-free) | MIT | | `github.com/lib/pq` | PostgreSQL driver | MIT | | `github.com/microsoft/go-mssqldb` | SQL Server driver | BSD-3-Clause | | `gopkg.in/yaml.v3` | YAML configuration parser | MIT / Apache-2.0 | | `github.com/google/uuid` | UUID generation | BSD-3-Clause | | `github.com/shopspring/decimal` | Decimal arithmetic (SQL Server) | MIT | | `github.com/dustin/go-humanize` | Human-readable sizes in logs | MIT | | `github.com/mattn/go-isatty` | Terminal detection for log formatting | MIT | All dependencies are **open source** with permissive licenses (MIT, BSD, Apache 2.0). No GPL or LGPL dependencies are included. ## Transitive Dependencies The full transitive dependency graph is recorded in each module's `go.sum` file. To list all dependencies including transitive ones: ```bash go list -m all ``` To check for known vulnerabilities: ```bash # Install govulncheck go install golang.org/x/vuln/cmd/govulncheck@latest # Run against the binary or source govulncheck ./... ``` ## Generating an SBOM ### CycloneDX (recommended) [CycloneDX](https://cyclonedx.org) is the format required by most regulatory frameworks including CRA. ```bash # Install cyclonedx-gomod go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest # Generate SBOM for the cmd module cd cmd cyclonedx-gomod app -output certeasy-sbom.cdx.json -json ``` This produces a machine-readable SBOM listing all dependencies with version, hash, and license information. ### SPDX ```bash # Install syft curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin # Generate SPDX SBOM from the binary syft certeasy.exe -o spdx-json > certeasy-sbom.spdx.json ``` ### Go native ```bash # Export dependency graph as JSON go list -m -json all > sbom-deps.json ``` ## CRA & NIS2 Compliance ### EU Cyber Resilience Act (CRA) The CRA (applicable from 2027) requires software vendors to: - Maintain and publish an SBOM for each release - Track and remediate known vulnerabilities (CVEs) within defined timelines - Provide a vulnerability disclosure policy - Document security properties of the software **Certeasy approach:** - SBOM generated per release using `cyclonedx-gomod` - Dependencies monitored via `govulncheck` in CI - Vulnerability reports accepted at [security contact on certeasy.tech](https://certeasy.tech) ### NIS2 Directive NIS2 applies to operators of essential and important entities. If your organization falls under NIS2, deploying Certeasy for internal certificate automation contributes to: - **Supply chain security**: all dependencies are open source and auditable - **Incident response**: structured audit log (`acme_audit_logs`) records all certificate operations - **Patch management**: single binary deployment simplifies updates ### Go Supply Chain Security Go's module system provides strong supply chain security guarantees: - **Reproducible builds**: `go.sum` records cryptographic hashes of every dependency - **Module transparency log**: the Go checksum database (`sum.golang.org`) independently verifies module hashes - **No runtime package loading**: all dependencies are compiled into the binary — no dynamic loading, no plugin injection surface To verify the binary was built from unmodified sources: ```bash go mod verify ``` ## Minimal Footprint Certeasy is designed for a minimal attack surface: - **Single binary** — no installer, no runtime dependencies, no package manager - **No external network calls** at runtime (except to your own ADCS and DNS servers) - **No telemetry** — Certeasy does not call home - **Standard library first** — cryptographic operations use Go's standard `crypto/x509` and `crypto/tls`; no custom crypto implementations