---
sidebar_position: 1
title: Changelog
---

# Changelog

## v0.9.5 - unreleased

**The first Hortval release.** One idea runs through it: the server no longer
guesses where anything is. Every path it uses is one you wrote, ambiguity is
refused instead of resolved silently, and each refusal names what it wanted and
prints the exact line to paste.

If you are coming from Certeasy v0.9.4 and you pass `-f` with absolute paths,
your configuration needs nothing — jump to
[Upgrading](#upgrading-from-v094--what-changes-for-an-existing-configuration)
for the case-by-case.

### What this release gives you

- **Hortval, under its own name.** The command, the binary and the release
  artefacts are `hortval` / `hortval-<version>-<os>-<arch>`. Every invocation
  changes with it: the `ExecStart` of a systemd unit, the `binPath=` of a Windows
  service, your runbooks and your monitoring.

  Nothing inside the ACME protocol carries the name, so **ACME clients are
  unaffected** — no account, order or certificate needs anything done to it, and
  no client configuration changes. The configuration file keeps its name and every
  one of its keys.

- **Nothing is guessed any more.** Seven lookups and settings that used to resolve
  silently now refuse, and say what to write instead. This is the release, not a
  side effect of it — see [Breaking changes](#breaking-changes) for the list and
  [Upgrading](#upgrading-from-v094--what-changes-for-an-existing-configuration)
  for what to do.

- **The Windows binary is signed with Authenticode**, timestamped so the
  signature outlives the signing certificate. Verify with
  `Get-AuthenticodeSignature`; the signer is SAFE PIC TECHNOLOGIES. Note that
  **SmartScreen still appears** — what changes is that it now names the publisher
  instead of saying "Unknown publisher". Linux and macOS binaries remain covered
  by `SHA256SUMS` for integrity only. See
  [Verifying release binaries](../security/verifying-binaries.md).

- **SQL Server integrated authentication.** Add `authenticator=winsspi` to your
  `database.dsn` and drop the credentials: Hortval connects as the Windows account
  it runs under, and no SQL password sits in `config.yml`. Windows only, and it
  needs a domain account. **Hortval reports which method it obtained at startup**
  and warns when the connection falls back to NTLM — which happens silently, and
  succeeds, when no Service Principal Name is registered on the SQL Server
  service account. See [Database](../configuration/database.md).

- **`hortval validate` now checks the filesystem**, and `--no-disk` skips it. It
  looks at what it can — does a working directory's parent exist, is an
  installation already sitting where a path resolves — because the machine you
  validate on is usually the machine that will serve. Two things it deliberately
  does not do: it never writes anything, and it never checks writability. A
  filesystem failure it cannot attribute is reported as a **warning**, not a
  verdict, because `validate` runs as the account that typed it while the server
  runs as its service account — guessing another identity's permissions produces
  confident wrong answers.

- **New `%CONFIGDIR%` token, valid in `workdir`**, expanding to the directory
  holding the configuration file. It exists because `workdir` is the one path that
  could not use `%WORKDIR%` — it *is* the working directory. `hortval init` now
  proposes `%CONFIGDIR%/workdir` instead of `./workdir`, so a generated
  configuration means the same directory wherever it is later started from.

- **A `letsencrypt` bundle now always serves an ECDSA P-256 certificate.**
  Previously the key type was decided by whichever client connected first, and
  then served to everyone: `autocert` indexes its cache by key type and holds
  both, but the certificate manager keeps one certificate per bundle, so the first
  handshake settled it for the life of the process. If a client of yours needs
  RSA, use `mode: pki` or `mode: files`, where `key.type` is yours to set.

### Not yet: running as a Windows service — this ships in v0.9.6

**v0.9.5 cannot be started by the Windows Service Control Manager.** The SCM
handshake is not implemented: `sc.exe start` fails with error 1053, and the
process is killed after about thirty seconds with its shutdown drain unfinished,
leaving `db.sqlite-wal` / `db.sqlite-shm` behind.

Run it in a console, or under a wrapper that performs the handshake on its behalf
(a scheduled task, or NSSM — neither is validated against Hortval yet). Native
service support, the Windows event log, and a `hortval diag` subcommand to prove
where the messages went are the headline of **v0.9.6**. See
[Installation → Windows service](../getting-started/installation.md#windows-service).

This is stated here rather than left to be discovered: on the platform Hortval
targets, "runs as a service" is what deployment means.

### Breaking changes

All of them are the same rule — **a value that was guessed is now demanded** —
and each refusal prints what to write. Only the last row is a rename rather than
a refusal.

| What | v0.9.4 did | v0.9.5 does |
|---|---|---|
| `workdir` absent | picked a location for you | **refuses.** Why, and the conventional value per platform: [`workdir`](../configuration/overview.md#workdir) |
| a path setting holding a relative path | resolved it against the current directory | **refuses** unless absolute or anchored with `%WORKDIR%` / `%CONFIGDIR%` |
| `config.yml` in the current directory | loaded it, ahead of everything else | **not searched at all** — the current directory of a service created by `sc.exe` is `C:\Windows\System32`, and any directory a lesser account can write to selected the database, the working directory, the audit destination and the outbound proxy |
| two candidate configuration files | loaded one, ignored the other | **refuses**, naming both |
| a configuration file that exists but cannot be read | selected it, then failed to open it | **skipped and reported**; a readable one is used with a warning |
| `%WORKDIR%` in `audit.path`, `logs.file`, `letsencrypt.cache-dir`, `local-cert-file`, `local-key-file` | taken literally — it created a directory actually *named* `%WORKDIR%` | **resolved** |
| an unrecognised name in `logs.services` | silently ignored, so the level you asked for never applied | **refuses**. `Certeasy-acme-server` → `acme-server`, `cert-easy-main` → `main` |

Two lookups also changed order without changing any outcome, since ambiguity is
now refused either way: configuration is looked for under `hortval` before
`certeasy` (the `certeasy` directories are still read, with a warning naming
both; they go away in v2), and machine-wide directories are searched before
per-user ones on every platform.

**The licensing backend moved to `api.hortval.com`.** If your egress is filtered
by hostname, allow it before upgrading — otherwise online licence checks fail and
the deployment slides into its degraded states. Offline installations are
unaffected.

### Upgrading from v0.9.4 — what changes for an existing configuration

Nothing to do if you pass `-f` and every path in your configuration is absolute.
Otherwise, find your case:

| Your v0.9.4 setup | v0.9.4 did | v0.9.5 does | What to do |
|---|---|---|---|
| `-f <path>` on the command line | loads it | same | nothing |
| No `-f`, `config.yml` in the **current directory** | loaded it | **refuses**, and lists where it looked | pass `-f`, or move the file |
| No `-f`, `config.yml` next to the binary | loaded it | same | nothing |
| No `-f`, `config.yml` in `/etc/certeasy` or `%PROGRAMDATA%\certeasy` | loaded it | loads it **and warns** | rename the directory to `hortval` before v2 |
| Two candidate files in two directories | loaded the first, ignored the other | **refuses**, names both | pass `-f`, or delete one |
| `config.yml` **and** `config.yaml` side by side | `.yml` won | **refuses**, names both | pass `-f`, or delete one |
| A config file that exists but cannot be read | was selected, then failed to open | **skipped**, and reported; another readable file is used with a warning | fix the permissions |
| `workdir: ./workdir` (or any relative path) | resolved against the working directory | **refuses**; if a live installation sits where it resolves, prints the exact line to write | use that line, or an absolute path, or `%CONFIGDIR%/…` |
| `workdir` not set, old directory holds data | `%ProgramData%\certeasy` / `/var/lib/certeasy` | **refuses**, and names the three ways out | move it, or name it with `workdir:`, or delete it if it is a leftover |
| `workdir` not set, nothing in the old directory | `%ProgramData%\certeasy` / `/var/lib/certeasy` | `%ProgramData%\hortval` / `/var/lib/hortval` | nothing |
| `workdir` set to an absolute path | resolved | same, and the rename check is skipped | nothing |
| `database.path: %WORKDIR%/db.sqlite` | resolved | same | nothing |
| `audit.path`, `logs.file`, `letsencrypt.cache-dir`, `local-cert-file`, `local-key-file` with `%WORKDIR%` | **taken literally** — created a directory actually named `%WORKDIR%` under the working directory | resolved | delete any stray `%WORKDIR%` directory once the real files are in place |
| `certreq-path` / `certutil-path` | resolved from the Windows system directory, relative refused | **unchanged** | nothing |
| `logs.services` naming a service | applied it | **refuses** an unrecognised name (they were silently ignored); `Certeasy-acme-server` → `acme-server`, `cert-easy-main` → `main` | rename the keys |

The wizard is affected too: `certeasy init` used to write `workdir: ./workdir`,
which v0.9.5 refuses. A configuration generated by v0.9.4's wizard therefore
needs its `workdir` line changed — the startup message tells you what to write.

See [Minimal configuration](../getting-started/minimal-configuration.md) for the full
search order, and [Configuration overview](../configuration/overview.md) for the
anchor tokens. If you are coming from a release older than v0.9.4, work through
[Upgrading](../upgrading.md) first — it collects everything that changed since v0.9.1.

### Rolling back to v0.9.4

**You can.** Nothing in this release prevents going back:

- A configuration fixed for v0.9.5 stays readable by v0.9.4, provided you used
  the absolute paths the startup message hands you. The `%CONFIGDIR%` token is
  new and v0.9.4 would take it literally — it only appears in configurations
  generated by `hortval init`, that is on fresh installs, which have nothing to
  roll back to.
- No schema migration ships in this release, so the database is unchanged.

Keep this in mind if you edit by hand: anchoring a path with a token that
v0.9.4 does not substitute is what would close the door. An absolute path never
does.

## v0.9.4 - 2026-08-04

We re-review the Certeasy codebase internally whenever materially more capable
code-analysis tooling becomes available. This release is the outcome of such a
review, run in July 2026 with the then-current generation of Claude and triaged
by hand. Every finding was assessed, and the ones that could affect a running
deployment are fixed here. Two breaking changes had to be introduced in the
product's configuration along the way; both are described below.

The review also recorded what was attacked and held. That part never appears in
a changelog, which is a shame, because it is what a security review is actually
for:

- **No finding permits private key compromise, customer data exfiltration, or
  remote code execution.**
- **CSR validation verifies the signature** rather than assuming proof of
  possession, and parses the request **twice** — once with the standard library,
  once with a strict hand-written ASN.1 pass — then compares the resulting SAN
  sets byte for byte, as a defence against parser differentials. Subject
  Alternative Names are restricted to `dNSName`: `otherName`/UPN, `rfc822Name`,
  URI and IP addresses are refused outright. This makes the ESC1/ESC6
  escalation vector **structurally impossible to request rather than filtered**
  — a distinction that matters on an ADCS deployment. It bounds what a client
  can ask for, not what your CA can issue; the template remains yours to
  harden. See [Certificate Security Model](../security/certificate-model.md).
- **JWS algorithm confusion has no landing point.** The verifier is selected on
  key type, each verifier re-reads the protected header and requires an exact
  algorithm with a matching curve, and there is **no HMAC verifier and no `none`
  verifier anywhere in the code** — so the classic "sign with HS256 using the
  public key as the secret" attack has nothing to reach.
- **No IDOR.** Every endpoint that takes an object identifier — account, order,
  finalize, authorization, challenge, certificate — compares the object's owning
  account against the authenticated one before returning anything. No handler
  fetches by identifier without that check. The single unscoped lookup in the
  codebase is reachable only on the certificate-key revocation path — which
  RFC 8555 §7.6 requires — and only after the embedded key has been matched byte
  for byte against the certificate's public key.
- **No SQL injection.** Every query is a compile-time constant with
  placeholders; there is no `LIKE` query anywhere in the repository and every
  `ORDER BY` is a literal.

A dedicated security page covering the threat model and host hardening is in
preparation.

### Security

This release resolves findings from an internal security review. Details of the
underlying mechanisms are deliberately omitted while deployments upgrade.

- **A specially crafted request could cause a denial of service** (remote,
  unauthenticated). Fixed. No data exposure and no authentication bypass.
- **Order identifiers are now canonicalised when the order is created**, and
  non-DNS identifier types are rejected explicitly. Previously an identifier was
  stored as submitted, so what was validated could differ from what was stored.
  This is a conformance fix (RFC 8555 §8.4) rather than an exploitable one:
  reaching the divergent case required write access to the DNS zone, which
  already allows obtaining the same certificate through the normal `dns-01`
  path. It is corrected because a stored value that diverges from the validated
  one is a hazard for future changes, not because it granted anything.
- **Valid requests could be rejected under normal operation.** A defect in the
  replay-protection bookkeeping refused nonces that were still valid. **Any
  deployment with two or more concurrent ACME clients was affected
  continuously**, without anyone attacking it — clients recovered by retrying as
  the protocol requires, so the symptom was extra round-trips and latency rather
  than visible failures. Single-client deployments were never affected.
- **Log and audit files are now owner-only** (`0600`, directories `0700`),
  matching the rest of the product. This matters as soon as `audit.path` points
  outside the working directory: the audit trail records account identifiers,
  source IPs, user agents and decisions. **On Windows this has no effect**:
  access there is governed by the NTFS permissions inherited from the containing
  folder — set that folder's ACL at install time. The mode bits apply on Linux
  and macOS.
- **Audit log rotation could destroy history.** A failed rotation — an antivirus
  holding the file, a full disk, a permissions problem — could discard a
  generation of history without archiving anything, and without reporting it.
  Rotation was rewritten: it now fails without touching existing files, reports
  the reason on stderr, and retries later. See *Breaking changes* for the
  consequence on file naming.
- Updated `golang.org/x/text` and `golang.org/x/crypto`.

### Rate limiting

Two changes here, and the second is new behaviour rather than a new setting.

- **The per-IP request ceiling now applies to every endpoint**, and is checked
  before any cryptographic work. Previously only four endpoints consulted it;
  the rest — including the polling a client does while waiting for validation —
  were unmetered. Because the ceiling now sees a client's full traffic, its
  defaults were raised accordingly: `requests-per-minute` from `200` to `1200`,
  `burst` from `20` to `100`. If you had tuned these values down, review them:
  issuing a single 3-name certificate costs at least 18 requests.
- **New `abuse` limiter, enabled by default.** It does not cap requests; it
  *marks* an IP that behaves in a way no conformant client does — a signature
  that does not verify, or an attempt on a resource belonging to another
  account. A marked IP is then refused on everything until it recovers, which
  takes about thirty seconds of good behaviour. Ten such events are tolerated
  first, so a client that slips once is unaffected.

  Marks are **weighted**. A request for an identifier that simply does not exist
  counts a quarter, because it has an innocent reading — a client returning to a
  URL whose resource has since been cleaned up gets the same answer as someone
  probing at random. Four such misses make one abuse, so occasional ones cost
  nothing while systematic probing still blocks. Routine outcomes count nothing
  at all, `badNonce` in particular: it happens to every client whenever the
  server restarts. See the configuration reference for the full table.

- **Account creation now allows a deployment wave.** `account-creation` moves
  from `5`/hour with a burst of `2` to `10`/hour with a burst of `10`. Creating
  an account is a once-per-machine-for-life event, so legitimate traffic arrives
  in bursts when you provision, not at a steady rate — and a burst smaller than
  the hourly allowance refused the third machine of a batch deployed behind a
  single NAT address. The sustained ceiling, which is what actually bounds
  abuse, only doubles.

If your clients reach Certeasy through a reverse proxy, set `trusted-proxies` in
the `server` section so the limiters see real client addresses. Whitelisting the
proxy would exempt every client behind it.

### Schema and migrations

A restart is rarely something a human decided — a crash, a reboot, a service
manager or a failed health check all restart the process, and none of them is a
moment when someone is standing by with a backup. Certeasy now applies on its own
only what doing nothing could have survived.

- **A restart applies additive migrations only.** Anything that cannot be undone
  by rolling the binary back stops startup and names what is pending. Nothing
  changes today for an existing deployment: every migration currently shipped is
  additive.
- **New command `certeasy migrate`**, with `--confirm` to acknowledge a backup and
  `--sql` to write the statements instead of running them.
- **New setting `database.noddl`** for accounts that hold no schema rights.
- **A database newer than the binary, or left mid-upgrade, is refused** rather
  than started on.

See [Migrations](../administration/migrations.md) for the full contract, the exit
codes and the `noddl` workflow.

### Breaking changes

- **The `adcs-cli` connector takes `certreq.exe` and `certutil.exe` from the
  Windows system directory** (typically `C:\Windows\System32`) instead of
  looking them up through `%PATH%`. An unset key, or a bare file name with or
  without the `.exe` extension, resolves there — existing configurations keep
  working. Set a full path to run a copy kept elsewhere, for instance on a path
  carved out of an EDR policy. A path relative to the working directory
  (`tools\certutil.exe`) is now refused at startup and by `certeasy validate`.
  The native connector is unaffected: it runs in-process over COM and starts no
  external binary.
- **ADCS `ca-name` and `certificate-template` are refused when they contain a
  control character or a colon, or start with a dash.** Those characters
  delimit fields in the request sent to the CA, so a value carrying one changes
  the request rather than naming it. Ordinary names — `PKI01\Lab-Issuing-CA`,
  `Web Server v2` — are unaffected.
- **A `dns-validation-profiles` entry with neither `allow-cidrs` nor
  `deny-cidrs` is refused at startup**, and by `certeasy validate`. An empty
  `resolved-ip-policy` accepted every address DNS returned — link-local and cloud
  metadata (`169.254.169.254`) included. Unset is not the same as deliberately
  open, so it is refused instead of defaulted. Add the networks your targets live
  on, or `allow-cidrs: ["0.0.0.0/0", "::/0"]` if you accept any address on
  purpose. This applies to every profile whatever challenge you use: challenge
  types cannot be restricted per profile, so a profile that has only ever seen
  `dns-01` still offers the paths this policy guards. Configurations produced by
  `certeasy init`, and the shipped examples, already carry the block.
- **A `fake` authority with no `common-name`, no `password`, a `key-size` below
  2048 or a non-positive `validity` is refused at startup**, and by
  `certeasy validate`. That driver parsed its configuration without checking any
  field, so these were accepted and then failed later — after the database had
  been migrated — or produced a CA that expired the second it was created.

- **`audit.rotate.max-backups` no longer exists and a configuration containing
  it is refused at startup.** The audit log is a compliance artifact: deleting it
  by file count is not a setting. Remove the key from your configuration before
  upgrading. Retention will return as a *duration*, which is the unit a
  compliance requirement is actually written in.
- **External rotation of the audit file (logrotate, Scheduled Task) is no longer
  supported.** A third party renaming or truncating the file breaks the
  tamper-evident chain, and Certeasy cannot distinguish that from tampering.
  Certeasy segments the file itself; remove any logrotate rule targeting
  `audit.path`. Earlier documentation recommended `copytruncate` here — that
  recommendation was wrong and is withdrawn.
- **With rotation enabled, `logs.file` and `audit.path` are naming bases, not
  files.** Segments are written beside them as
  `<name>.<UTC timestamp>.<discriminant>.<ext>` — for example
  `certeasy.20260726T091702Z.ms123.log` — and are **never renamed**. Configure log
  collectors with the containing folder and a `*.log` pattern rather than the
  path in `file`; following the live file interactively now means picking the
  newest segment.
  This also means existing `logrotate` rules on `logs.file` become inert.
  Rationale: a rotation now creates exactly one file and mutates nothing else,
  and a closed segment is immutable — which is what makes archival, and the
  audit chain, safe. It also removes a long-standing annoyance for log
  collectors, which previously saw duplicate or missing lines at every rotation.

### Fixes

- **On a shared database, Certeasy could build its schema in the wrong place.**
  Where a table of the same name already existed in another schema — a `jobs` or
  a `servers` belonging to a different application — Certeasy could take it for
  its own and then read and write that application's data. SQL Server
  deployments using a schema other than `dbo` were affected. Certeasy no longer
  asks the question that made this possible.
- **The schema in use is reported at every start**, and any of Certeasy's tables
  found outside it are named. Two instances sharing a database *and* a schema
  share their data — a valid multi-node deployment, and an accident that looks
  identical from the database's side. See
  [Migrations](../administration/migrations.md).

### Documentation

- **The migrations page was rewritten** — it described a schema silently brought
  up to date at every start, with nothing ever to run by hand. Neither is true
  any more.
- The database configuration page gained the `noddl` setting and a section on
  schema selection.
- The rate-limiting page described the whitelist incorrectly: it stated that
  `order-creation` still applied to a whitelisted IP. It does not — a whitelisted
  IP also bypasses that limiter. The behaviour is unchanged; only the
  documentation was wrong. The limits that remain in force for a whitelisted IP
  are the account-scoped ones: `duplicate-certificate`, `failed-validation` and
  `pending-authorizations`.

### Improvements

- **Security controls that are switched off are now announced at startup.**
  Disabling the audit log or a rate limiter is a legitimate choice; doing it
  without a trace is not. Certeasy logs one warning per disabled control, naming
  the setting and its consequence. A default configuration stays silent — a
  warning that fires on a healthy install is noise.

---

## v0.9.3 - 2026-07-09

### New features

- **ADCS certificate revocation**: ACME revocations now propagate to the backing Microsoft ADCS CA (CRL / OCSP), not just Certeasy's own database. RFC 8555 §7.6 authorization is supported with both the account key (`kid`) and the certificate key (`jwk`). Propagation can be turned off per authority with `disable-ca-revocation: true` (for service accounts without the required CA role, or air-gapped deployments). Note: revoking on the CA needs the **Certificate Manager** role — a higher privilege than enrollment.
- **`certeasy adcs check`** — a read-only preflight for ADCS authorities: checks that the CA is reachable, that the certificate template is published, and reports the template's key requirement. Use it to diagnose ADCS setup before starting the server (or to hand support a clear status).
- **Configurable server-certificate key**: a `pki`-mode bundle in the TLS certificate manager now accepts an explicit `key:` — `type: rsa` with `size:` (bits), or `type: ecdsa` with `curve:` (`P-256`/`P-384`/`P-521`). Set it when the CA template mandates a specific key. In particular, an ADCS template that requires **RSA 4096** previously rejected Certeasy's default ECDSA key and prevented startup; setting `key.type: rsa` with `key.size: 4096` resolves it. The default is unchanged (ECDSA P-256).

### Improvements

- **`certeasy init` — ADCS onboarding**: the wizard now defaults to ADCS, lists the CA's published certificate templates so you select the exact one (no typos, only published templates), and reads the template's key requirement to set the server-certificate key automatically — falling back to asking you when the template cannot be read.
- **Clearer ADCS denial messages**: when the CA denies a request, Certeasy now surfaces the actual reason (for example, "the public key does not meet the template's key size requirement", `CERTSRV_E_KEY_LENGTH`) with an actionable hint, instead of an opaque `CR_DISP_DENIED`. The diagnosis is keyed on the CA's error code, so it stays accurate regardless of the CA's display language.

### Security

- Updated the Go toolchain to **1.26.5** to address **GO-2026-5856** (an Encrypted Client Hello privacy leak in the standard library's `crypto/tls`).

---

## v0.9.2 - 2026-06-17

### New features

- **Native ADCS connector** (now the default for `type: adcs`): Certeasy enrolls against ADCS **in-process**, without launching `certreq.exe`. This removes the child-process (LOLBin) signature that strict EDRs flag, making Certeasy eligible for more hardened deployment perimeters. Existing `type: adcs` configurations switch to it automatically on upgrade — no change required.
- **`certeasy validate`** — a configuration check in the spirit of `nginx -t`. `certeasy validate -f <config>` parses and validates a configuration statically, with no side effects (no database, no network, no file writes). `serve` now runs the same validation as a fail-fast boot gate, so an invalid configuration is rejected before startup instead of failing halfway through.

### Improvements

- `certeasy init` now lets you choose the ADCS connector (native in-process, or `certreq.exe`) when generating a configuration.

### Changes

- The `certreq.exe`-based integration remains available as an opt-in fallback under `type: adcs-cli`.
- The unused `cert-util-timeout` ADCS option is no longer documented; it is still accepted in existing configurations but has no effect.
- **ADCS request timeout**: `default-timeout` now defaults to **4 minutes** and is honored as configured. Previously a legacy behavior capped the effective ADCS wait at **30 seconds** regardless of `default-timeout`; that cap is removed. Keep `default-timeout` below `workers.max-job-duration` (default 5m) — Certeasy warns at startup if it is greater than or equal.

### Fixes

- `certeasy init` generated an ADCS authority block with incorrect field names; it now emits the correct `ca-name` / `certificate-template` schema.
- **SQL Server backends**: upgraded the SQL Server driver to go-mssqldb v1.10.0, which improves connection handling when a query is cancelled or times out. Recommended for all SQL Server deployments.
- **DNS validation profiles**: the zone `protocol` field (`udp` / `tcp`) is now honored. It was previously parsed but ignored (DNS lookups were always UDP-first with a TCP fallback); set `protocol: tcp` to force DNS validation over TCP on networks where UDP/53 is unavailable.

---

## v0.9.1 - 2026-06-10

### New features

- `certeasy init` command — interactive (and scriptable) wizard to generate a `config.yml`.
- Database connection: new `conn-max-lifetime` and `conn-max-idle-time` options.

### Improvements

- Readable text output for one-shot subcommands (`license`, `backup`, `audit`, `cold-start`). The `serve` daemon keeps its structured JSON output.
- All displayed and logged dates are in UTC.
- Richer startup banner: installation ID and license ID.

### Changes

- The `--grace` and `--cold-start-plan` flags are removed. The boot mode (valid license, post-expiration, cold-start, refusal) is now determined automatically from persistent state.
- License registration is validated by the portal; a portal refusal is surfaced verbatim to the operator.
- A license retired by the portal enters a grace window before boot is refused.

### Fixes

- Shutdown stability: removed a possible SQLite panic and a goroutine leak on the ACME rate limiter.

---

## 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 v0.9.x is a **stable release**, used for day-to-day issuance, renewal and revocation. The full **production-ready** label is reserved for the upcoming **v1.0**, which closes the known, non-blocking limitations below:

- **Certeasy cannot be started as a Windows service with `sc.exe`.** The Service Control Manager handshake is not implemented, so `sc.exe start` fails with error 1053 and the process is killed after about thirty seconds, leaving the shutdown drain unfinished. Run it in a console, or under a wrapper that performs the handshake. Planned for v1.0. See [Installation](../getting-started/installation.md).
- **Some startup log lines only reach stderr, never `logs.file`.** The file receives everything the audit, PKI, HTTP, license and worker services emit, but a few lines from the main service are written before the configured destination is installed. A *refused* startup writes nothing to the file at all. Under systemd those lines land in journald; under the Windows SCM they are lost. Planned for v1.0. See [Logging](../administration/logging.md).
- **No health or metrics HTTP endpoints yet.** Operational monitoring is limited to log scraping and database introspection for now; dedicated `/health` and metrics endpoints are planned for v1.0.
- **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 in v1.0.
- **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; full `replaces` semantics are planned for v1.1.
- **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.
:::
