certmagic 0.2.0

Automatic TLS certificate acquisition, renewal, and maintenance for Rust servers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
<div align="center">

# certmagic

**Automatic TLS certificate acquisition, renewal, and maintenance for Rust servers.**

[![CI](https://github.com/houseme/certmagic/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/houseme/certmagic/actions/workflows/ci.yml)
[![Audit](https://github.com/houseme/certmagic/actions/workflows/audit.yml/badge.svg?branch=main)](https://github.com/houseme/certmagic/actions/workflows/audit.yml)
[![Crates](https://img.shields.io/crates/v/certmagic.svg)](https://crates.io/crates/certmagic)
[![Documentation](https://docs.rs/certmagic/badge.svg)](https://docs.rs/certmagic)
[![Dependency status](https://deps.rs/repo/github/houseme/certmagic/status.svg)](https://deps.rs/repo/github/houseme/certmagic)
[![Crates.io Total Downloads](https://img.shields.io/crates/d/certmagic)](https://crates.io/crates/certmagic)
[![Crates.io License](https://img.shields.io/crates/l/certmagic)](https://crates.io/crates/certmagic)

[English]README.md | [简体中文]README.zh-CN.md

</div>

---

## Overview

CertMagic manages TLS certificates for you: it obtains certificates from ACME CAs (Let's Encrypt, ZeroSSL, …), renews
them before expiry, staples OCSP responses,
coordinates with other instances through shared storage, and can even obtain
certificates **on-demand during the TLS handshake**.

The crate automates the full certificate lifecycle and ships an idiomatic
Rust API (async traits, typed events, `thiserror`, `Send + Sync` everywhere).

## Feature highlights

- **ACME v2 client, self-developed**: directory
  discovery, replay-nonce pool, account management with External Account
  Binding, order state machine, revocation, ARI (renewal information) with
  suggested-window jitter.
- **Four challenge solvers**: HTTP-01 (reference-counted shared listener +
  framework-agnostic handler), TLS-ALPN-01 (RFC 8737 challenge certificates),
  DNS-01 (pluggable `DnsProvider` + propagation checks), and **distributed
  solving** through shared storage — cluster-wide issuance without sticky sessions.
  HTTP-01 helpers validate unpadded base64url tokens, tolerate query strings and
  one router-added trailing slash, and expose an opt-in blind-solving fallback
  for deployments that cannot retain challenge state.
- **Handshake-time issuance and on-demand TLS**: serve a certificate the first
  time a name is seen, gated by your `DecisionFunc` or explicit host allowlist;
  if neither gate is configured, issuance is denied.
- **Certificate cache and maintenance loop**: SAN-indexed in-memory cache,
  renewal windows (fixed ratio or ARI-driven with jitter), OCSP stapling with a
  hand-rolled RFC 6960 codec (request building, delegated-responder
  authorization, ECDSA/RSA/Ed25519 signature verification).
- **Storage abstraction with distributed locking**: default file storage with
  atomic writes and heartbeat lockfiles; bring-your-own backend (Redis, etcd,
  S3, …) for multi-instance clusters.
- **Separate certificate store**: certificates and private keys can use a
  dedicated `CertStore` backend while ACME accounts, locks and OCSP data stay
  on the ground-truth `Storage`.
- **Production hygiene**: retry with hand-tuned 30-day backoff budget, per-name
  job deduplication, sliding-window rate limiter, typed event hooks, panic-proof
  maintenance loop.
- **Operational hooks**: cache lifecycle callbacks, configurable HTTPS redirect
  host policy, custom DNS resolvers for propagation checks, and optional
  suppression of automatic replacement after an OCSP revocation report.

## Installation

```toml
[dependencies]
certmagic = "0.2"
tokio = { version = "1", features = ["full"] }
```

The default build uses the AWS-LC-RS crypto provider and enables the ZeroSSL
issuers. RSA key generation requires the optional `rsa` feature. For a smaller, portable build using the
Ring provider, disable default features and select the required capabilities
explicitly:

```toml
[dependencies]
certmagic = { version = "0.2", default-features = false, features = [
  "ring", "file-storage", "http-01", "dns-01", "ocsp",
] }
```

The `ring` and `aws-lc-rs` provider features also select the matching
`x509-parser` signature-verification backend (`verify` and `verify-aws`,
respectively), so certificate verification uses the same backend as rustls,
rcgen, and reqwest. Select exactly one provider in a `--no-default-features`
build; optional runtime modules each have an independent feature flag.

The `rsa` feature is opt-in because it pulls in the RustCrypto `rsa` crate.
Deployments that only issue ECDSA or Ed25519 certificates should leave it off
to keep the dependency tree smaller and avoid RSA-specific timing-sidechannel
risk. RustSec `RUSTSEC-2023-0071` remains explicitly tracked because the
upstream project has not published a fixed release yet.

## Upgrading from 0.1 to 0.2

Version 0.2.0 adds optional Redis/etcd coordination and S3, Vault KV v2 and
AWS Secrets Manager certificate stores. See the [0.2.0 changelog](CHANGELOG.md#020---2026-10-11)
and the backend sections below for configuration and validation limits.

- **FileStorage coordination:** stop all cooperating instances before upgrading,
  then restart them together. The new lock protocol requires OS file locks on
  the shared filesystem; preserve `locks/*.guard` sidecars while nodes run.
  Existing certificate, key, metadata and account paths remain compatible.
- **Custom backends:** implement `move_key` for no-clobber private-key archival.
  Implement `canonical_key` when aliases exist. A backend accepting write fences
  must atomically implement guarded writes and moves; defaults reject unsupported
  fences before CA work. Pair manual `track_lock` calls with `untrack_lock`.
- **Remote certificate stores:** pass the same coordinator instance to Config
  and RemoteCertStore. Etcd validates ownership during reference publication;
  FileStorage/Redis retain their documented coordination limits. Back up both
  references and blobs, and define retention for immutable history and archives.
- **Crypto and builds:** RSA key generation is opt-in (`rsa`); default features
  use AWS-LC. Choose `ring` explicitly when disabling defaults. Etcd builds need
  `protoc`, including `--all-features` and etcd-backed remote-store examples.
- **HTTP and DNS:** the HTTP/1.1 convenience wrapper accepts Content-Length
  bodies up to 1 MiB with a 30-second read deadline and rejects transfer encoding.
  Explicit DNS resolvers accept IPs (port 53) or socket addresses with custom
  ports; hostname resolver entries are rejected before lookup.

## Quick start

```rust,no_run
use std::sync::Arc;
use tokio_util::sync::CancellationToken;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let domain = "example.com"; // must point at this server

    let cache = certmagic::Cache::new(Default::default())?;
    let options = certmagic::ConfigOptions {
        issuers: vec![Arc::new(certmagic::AcmeIssuer::lets_encrypt())],
        ..Default::default()
    };
    let config = certmagic::Config::new(cache, options)?;
    let ct = CancellationToken::new();

    // Obtain now, renew automatically in the background.
    config.manage_sync(&ct, &[domain.to_owned()]).await?;

    // Serve HTTPS with rustls.
    let acceptor = Arc::new(config.certmagic_acceptor()?);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:443").await?;
    loop {
        let (tcp, _) = listener.accept().await?;
        let acceptor = Arc::clone(&acceptor);
        tokio::spawn(async move {
            if let Ok(tls) = acceptor.accept(tcp).await
                && let Ok(mut tls) = tls.await
            {
                // `tls` is a ready TLS stream; hand it to your HTTP stack.
            }
        });
    }
}
```

Prefer a high-level manager? `CertManager` wraps the same machinery behind a
cancellation-free facade:

```rust,no_run
let manager = certmagic::CertManager::builder()
    .issuers(vec![std::sync::Arc::new(certmagic::AcmeIssuer::lets_encrypt())])
    .build()?;                    // try_build() is the explicit fallible alias
manager.manage(&["example.com".to_owned()]).await?;   // foreground obtain + renew
let acceptor = std::sync::Arc::new(manager.config().certmagic_acceptor()?);
```

`Cache::new` starts renewal/OCSP maintenance automatically. Applications that
need an explicit lifecycle can use `Cache::new_without_maintenance`, then call
`start_maintenance`; `stop_and_wait` cancels and joins the task without
consuming the cache. A stopped cache cannot be restarted.

For a one-shot setup, `certmagic::manage(&domains).await` returns a ready
`rustls::ServerConfig`. For reusable policy and backend wiring, use
`ConfigBuilder::new().policy(Policy::default()).storage(storage).build()`.

For a framework-neutral HTTP/1.1 wrapper with HTTP-01 handling and HTTPS
redirects, use `certmagic::https` or `certmagic::https_on`. These helpers advertise
HTTP/1.1 only, accept Content-Length bodies up to 1 MiB, reject transfer
encodings such as chunked, and apply a 30-second request-read deadline.

More in [`examples/`](examples/): [`basic_https`](examples/basic_https.rs),
[`on_demand`](examples/on_demand.rs) (handshake-time issuance),
[`custom_dns01`](examples/custom_dns01.rs) (wildcard via a custom DNS provider).

## Integration with rustls

Three paths, pick per deployment:

| Path                   | API                                              | Behavior                                                                |
|------------------------|--------------------------------------------------|-------------------------------------------------------------------------|
| Sync cache resolver    | `Config::tls_config()`                           | Serves from the in-memory cache, no IO                                  |
| Async acceptor         | `Config::certmagic_acceptor()`                   | Full async resolution (storage load / on-demand issuance / maintenance) **before** the handshake completes |
| Background remediation | automatic in `tls_config()` when on-demand is on | Cache miss spawns issuance in the background; this handshake fails, the next one succeeds |

## Feature flags

| Feature             | Default | Enables                                                  |
|---------------------|---------|----------------------------------------------------------|
| `file-storage`      | ✔      | FileStorage backend (atomic writes, heartbeat lockfiles) |
| `http-01`           | ✔      | HTTP-01 challenge listener (Tokio TCP)                   |
| `dns-01`            | ✔      | DNS propagation checks (hickory-resolver)                |
| `ocsp`              | ✔      | OCSP stapling lifecycle (hand-rolled RFC 6960 codec)     |
| `zerossl`           | ✔      | ZeroSSL ACME/EAB and REST API issuers                    |
| `local-cache`       |         | Node-local read-through storage cache                    |
| `redis-storage`     |         | Redis values and owner-checked renewable leases         |
| `etcd-storage`      |         | Etcd leases, snapshot reads and guarded publication     |
| `remote-cert-store` |         | Immutable certificate blobs with coordinated references |
| `s3-cert-store` |         | S3 certificate blobs (caller-configured AWS SDK client) |
| `vault-cert-store` |         | Vault KV v2 certificate blobs with CAS writes |
| `secrets-manager-cert-store` |         | AWS Secrets Manager certificate blobs with pinned versions |
| `rsa`               |        | Opt-in RSA 2048/4096/8192 key generation                |
| `ring`              |         | Ring crypto provider and `x509-parser/verify`             |
| `aws-lc-rs`         | ✔      | AWS-LC crypto provider and `x509-parser/verify-aws` (including P-521 CSR signing) |
| `integration-tests` |         | Pebble end-to-end tests                                  |

## Choosing a storage backend

FileStorage remains the built-in durable backend. LocalCache is a node-local
read-through decorator, not a distributed source of truth. `redis-storage` adds
an optional Redis adapter. `etcd-storage` adds transaction-checked publication
for deployments using etcd for coordination. Optional S3, Vault KV v2 and AWS
Secrets Manager adapters store complete certificate resources through `CertStore`.
SQL storage is not bundled.

Custom backends implement `Storage` and `Locker`, then are supplied through
`ConfigBuilder::storage`. `LockGuard::new` accepts a backend-owned release
callback even without the `file-storage` feature. The callback must retain the
acquisition token and must not release another holder's lease. Network adapters
can override `LockRelease::release_async` for acknowledged cleanup. Use
`guard.release_and_wait().await` when acknowledgement matters; Drop remains a
best-effort fallback. `guard.is_valid()` reports advisory local lease health,
not write-side fencing. Automatic cleanup tracks acquisitions independently,
including identical names on separate backends.

Manual `track_lock(storage, name)` registrations must be paired with
`untrack_lock(name)` when ownership ends. A guard only unregisters its own
automatic registration; dropping an unrelated same-name guard no longer removes
manual ownership. Prefer the automatic acquisition helpers for new adapters.

LocalCache serves hits without awaiting backend operations and serializes
misses/writes per canonical key. Grouped reads, writes and moves acquire the
same gates in canonical sorted order, so independent groups overlap and
reversed/aliased key lists cannot deadlock. Prefix deletion excludes these operations;
concurrent misses for one key share the resulting fill. Cached entries retain
and reuse their key gates; eviction or the last in-flight operation reclaims
unused gates. Custom backends with aliases should override
`Storage::canonical_key`; its default preserves opaque keys, while FileStorage
and Redis normalize slash paths. Nested decorators forward this identity.
Single-key hits see outside changes only after eviction; grouped resource reads
delegate to the backend instead of mixing cached entries. Cancellation
cannot retract a dispatched backend write: after an ambiguous outcome, read
from the authoritative backend rather than assuming the local cache is current.

Exact key movement is a backend operation (`Storage::move_key`); it must not be
emulated using recursive `delete`. Canonical self moves are no-ops, descendants
survive, and an occupied destination returns `StorageError::Conflict`. A custom
Storage using KeyValueCertStore must implement this optional method to support
private-key archival; the default fails before any copy/delete. FileStorage
stages regular files in a private, fsynced sibling copy, publishes the complete
archive through a no-clobber hard link, then unlinks the source. Directory and
symlink sources are rejected. Archive copies remain owner-only on Unix even
when the imported source has broader permissions. The destination filesystem
must support hard links; unsupported filesystems fail with the source intact.
Interrupted filesystem moves may leave both names and are not a cross-file crash
transaction. Redis uses one atomic rename script; etcd uses revision comparisons.
Guarded etcd moves distinguish data conflicts from lost ownership inside one
transaction, keeping a valid lease available after an archive conflict.

Try-lock timeout budgets cover backend attempts as well as retry sleeps. Like
other async deadlines, cancellation is cooperative and cannot retract a command
already dispatched to a backend; expiring leases remain the recovery mechanism.

Certificate/private-key resources can use an independent `CertStore` through
`ConfigBuilder::cert_store`; accounts, challenge publications, locks and OCSP
remain on `Storage`. A transactional database or a versioned object bundle can
therefore provide stronger resource atomicity than the generic three-key adapter.
The generic adapter overlaps its three independent reads/existence checks; this
does not turn them into an atomic backend snapshot.

Redis is an optional choice for deployments already operating Redis, not a
prerequisite for faster TLS cache hits. Configure persistence and eviction policy
for durable account/private-key data; the adapter does not change server settings.
Redis failover assumptions remain explicit. See [Redis locking](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/)
and [persistence](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/).
For transactional storage, [PostgreSQL locks](https://www.postgresql.org/docs/current/explicit-locking.html)
or [etcd transactions and leases](https://etcd.io/docs/v3.6/learning/api/)
are other building blocks. These capabilities still need a backend adapter;
selecting a service alone does not provide fencing for this library's writes.

Reproducible local performance measurements and their limits are documented in
[benches/README.md](benches/README.md).

## Etcd adapter and guarded publication

Etcd support is available in **0.2.0**. Enable
`etcd-storage`; builds enabling this feature (including `--all-features`) need
`protoc` on PATH. The optional client uses the crate's selected Ring/AWS-LC TLS
provider; default builds do not pull in the etcd/gRPC dependency graph.

```rust,ignore
use certmagic::{Config, EtcdStorage, EtcdStorageOptions};

let storage = EtcdStorage::connect(
    &["http://127.0.0.1:2379", "http://127.0.0.1:22379", "http://127.0.0.1:32379"],
    EtcdStorageOptions { namespace: "my-service".into(), ..Default::default() },
).await?;
let config = Config::builder().storage(storage).build()?;
```

All endpoints must belong to one cluster and use the same transport scheme.
HTTPS supports WebPKI roots or `EtcdTlsOptions` with custom CA/mutual-TLS PEMs.
Username/password credentials belong in options, not endpoint URLs. Debug and
backend error output omit credentials, PEMs and remote status bodies.

Config obtains/renews under its existing per-subject lock, then calls
`CertStore::save_with_lock`. The key-value adapter delegates all three certificate
components to `Storage::store_tx_with_lock`. Etcd compares the lock's revision,
lease ID and random token in the **same transaction** as those writes. Private-key
archival uses `move_private_key_with_lock` / `move_with_lock`, comparing source
revision and destination absence as well. An expired or replaced acquisition
cannot overwrite a newer certificate or archive/remove its private key.

`LockRelease::write_fence` provides an opaque backend context. Guards requiring a
fence are rejected by default implementations; a custom CertStore must support
both guarded operations explicitly. Config checks compatibility before CA work.
Etcd contexts work with clones/decorators of the originating storage handle;
a separately connected handle or a different backend is rejected. An etcd lock
combined with an arbitrary S3/secret-store CertStore does **not** provide a
cross-system transaction. The remote adapters below instead publish immutable
blob references through the originating coordinator.

Complete certificate reads use `load_many` and value-presence checks use
`exists_exact_many`; prefix-aware `exists`/`exists_many` retain their existing semantics.
Etcd performs each group in one transaction; LocalCache delegates bundle reads
together to avoid mixing cached generations. Generic backends retain their
parallel-read behavior. Prefix listing uses pagination at a fixed revision;
compaction errors are returned rather than silently switching snapshots.

Persistent values use a versioned binary envelope with a **writer-clock**
modification timestamp; etcd revisions, not timestamps, determine ownership.
Values never inherit the lock lease. Records/batch writes are limited to 1 MiB,
with up to 64 guarded keys. Prefix deletion is an atomic exact-key plus child-range
transaction. Prefix stat derives the newest observed child timestamp.

Leases default to 30 seconds with a 10-second keep-alive interval. Etcd TTL is
fixed at acquisition: explicit renewal refreshes it, but a request exceeding the
configured duration is rejected. Cancellation of an in-flight renewal discards
local ownership rather than reusing a delayed stream reply. Drop queues lease
revocation; `release_and_wait` awaits it, with TTL as the process/runtime-failure
fallback. Connection loss fails local lease health conservatively. Operations
may return timeout/unavailable errors during leader changes; ambiguous write
outcomes are not blindly retried. Linearizable writes require a quorum.

These guarantees cover guarded publication and private-key archival. Plain
`store`, `delete`, legacy `save`/`move_private_key`, account/challenge writes and
storage cleanup are not automatically fenced. FileStorage/Redis guards keep
their previous advisory-health behavior. This is not a guarantee against actors
that bypass guarded APIs or an atomic transaction spanning independent systems.

Run the explicit local integration lane (requires Docker and `protoc`):

```sh
docker pull quay.io/coreos/etcd:v3.6.5
cargo test --locked --no-default-features --features ring,etcd-storage,local-cache \
  --test etcd_storage --test custom_locker -- --include-ignored
```

Tests create uniquely named, owned containers/networks with loopback client ports,
and remove them on exit. They cover a three-member cluster, ownership replacement,
concurrent snapshot reads, Config obtain/renew publication, private-key archival,
keep-alive, cancellation/runtime loss, leader loss, quorum loss/recovery, password
authentication, and a separate mutual-TLS server. No CA or production endpoint is
contacted. The explicit example is `examples/etcd_storage.rs`.

## Remote certificate stores

These adapters are available in **0.2.0**. Enable `s3-cert-store`, `vault-cert-store` or
`secrets-manager-cert-store`; defaults remain unchanged. Each reuses
`RemoteCertStore<B>` and the `ImmutableBlobStore` contract for additional services.
Construct a blob backend, then call `RemoteCertStore::new(backend,
coordinator.clone(), namespace)`. Supply that same coordinator to
`Config::builder().storage(coordinator).cert_store(certificates)`.

A complete certificate, private key and issuer metadata form one bounded,
content-addressed blob. Only small references live in the coordinator; a cold
load reads those references and fetches one blob. Guarded publication uploads
first, then atomically checks the etcd owner and publishes references. A stale
upload can leave an unused blob but cannot replace the current publication.
FileStorage/Redis retain their existing weaker coordination guarantees. This
protocol does not require a transaction spanning the two services.

Use one unique namespace per remote location and identical configuration on all
nodes. Back up both the coordinator and remote objects. Mixed references,
corrupt objects, missing referenced blobs and permission failures return errors.
Legacy administrative `save`/`remove` calls remain unfenced. Private-key archival
moves a reference without overwriting an existing archive;
`load_archived_private_key(destination)` resolves it. Removal and archival are
logical operations: immutable objects, older secrets and failed-upload remnants
are retained. There is no automatic garbage collector or physical key erasure;
retention must account for live references, archives and uncertain write outcomes.

| Adapter | Configuration and operational contract |
| --- | --- |
| `S3CertStore` / `S3BlobStore` | Existing bucket supporting `If-None-Match: *`; default encoded limit 1 MiB. Configure encryption and access policy on the bucket. Uses bounded GET for existence to distinguish a missing object from a missing bucket. |
| `VaultCertStore` / `VaultKv2BlobStore` | KV v2, CAS=0, immutable version 1, HTTPS by default; default decoded blob limit 256 KiB. Token requires create/update/read on data paths and read on metadata paths. Disable scheduled version deletion; token renewal belongs to the caller. |
| `SecretsManagerCertStore` / `SecretsManagerBlobStore` | One secret per blob, pinned version ID, encoded limit 64 KiB including base64 PEM and metadata. Plan secret-count quotas and ongoing storage cost; IAM should deny changes/deletion of retained versions. Optional KMS encryption key; private keys remain exportable, not HSM signing keys. |

AWS adapters accept caller-configured SDK clients. Set credentials, region,
retry policy and HTTP transport explicitly; `cert_store::remote::aws_http_client()`
uses the selected Ring/AWS-LC provider. The adapters do not discover ambient
credentials. S3 and Vault bound downloaded bodies; Secrets Manager checks the
service's 64 KiB binary limit after the SDK has decoded its JSON response, so
custom endpoints must be trusted to enforce service-compatible response limits.
Constructors validate local options, not remote availability or IAM policy.

Runnable configuration examples use etcd for coordination and issue no
certificates: [`s3_cert_store`](examples/s3_cert_store.rs),
[`vault_cert_store`](examples/vault_cert_store.rs), and
[`secrets_manager_cert_store`](examples/secrets_manager_cert_store.rs).
Compile with the corresponding adapter feature plus `etcd-storage` (requires
`protoc`). Protocol tests use loopback mocks; explicit Docker tests use isolated
MinIO, Vault dev mode and LocalStack, never production credentials. MinIO's
[community distribution is source-only](https://github.com/minio/minio#source-only-distribution);
the test Dockerfile builds the official release's fixed commit instead of
pulling the unavailable upstream image:

```sh
docker build -f tests/support/minio.Dockerfile -t certmagic-minio-test:07c3a429 tests/support
docker pull hashicorp/vault:1.21.0
docker pull localstack/localstack:4.14.0
cargo test --locked --no-default-features \
  --features ring,file-storage,s3-cert-store,vault-cert-store,secrets-manager-cert-store \
  --test remote_cert_store --test s3_cert_store --test vault_cert_store \
  --test secrets_manager_cert_store -- --include-ignored
```

## Redis adapter

Redis support is available in **0.2.0**. Enable `redis-storage` explicitly:

```toml
certmagic = { version = "0.2", features = ["redis-storage"] }
```

```rust,no_run
use certmagic::{Config, RedisStorage, RedisStorageOptions};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let storage = RedisStorage::connect(
    &std::env::var("CERTMAGIC_REDIS_URL")?,
    RedisStorageOptions { namespace: "my-service".into(), ..Default::default() },
).await?;
let config = Config::builder().storage(storage).build()?;
# config.cache().stop_and_wait().await;
# Ok(())
# }
```

The adapter supports a single Redis endpoint, password/ACL URL authentication,
`rediss://` with certificate verification and redis-rs Unix socket URLs. It uses
`redis-rs` connection multiplexing/reconnection, not one connection per operation.
Defaults are a 30 s lease, 10 s heartbeat, 5 s command timeout and 100 ms lock poll.
The heartbeat interval plus command budget must be shorter than the lease.

Values and modification timestamps are atomic within one Redis hash and have
no TTL. Lock keys occupy a separate namespace and use `SET NX PX`; Lua scripts
verify the acquisition token before renewal or deletion. Explicit extension is
not shortened by a later heartbeat. Prefix operations use escaped SCAN patterns
and bounded deletion batches; concurrent prefix changes are not atomic snapshots.

This is a single-endpoint lease adapter, not Redlock or write fencing. Redis
Cluster and Sentinel discovery are not implemented. Losing a heartbeat fails
local lease health closed. Issuance/renewal checkpoints stop retries and
publication after detected lease loss; callers needing fenced writes still
require a stronger atomic write protocol. `CertStore` remains separate, and the generic three-key resource
adapter is not a crash-atomic transaction. Persist keys/accounts appropriately
(e.g. AOF with an explicit fsync policy and a non-evicting capacity plan), use
ACL/TLS as appropriate, and do not log connection URLs. No automatic server
configuration changes are performed.

Example: `cargo run --example redis_storage --features redis-storage`.
Tests start owned temporary Redis instances on loopback ports; they never use a
configured production Redis URL:

```sh
cargo test --locked --features redis-storage --test redis_storage -- --include-ignored
cargo test --locked --no-default-features --features ring,redis-storage --test redis_storage -- --include-ignored
```

Set `CERTMAGIC_REDIS_SERVER` to an alternate local server binary. Network tests
are ignored by default; the pure configuration test runs normally. Local evidence
uses Redis 8.10.2, including AOF restart, authentication, lease loss, cancellation
and old-holder protection. TLS handshake, Cluster/Sentinel, Valkey and Dragonfly
are not claimed as validated by those tests.

### Other backend candidates

| Backend | Suitable integration | Required work / current status |
| --- | --- | --- |
| Valkey | Reuse the Redis protocol adapter | Candidate; run the same compatibility tests before claiming support |
| etcd | `Storage` + `Locker`, guarded transactions and snapshot reads | Implemented behind `etcd-storage`; protected writes require the originating backend context |
| Consul KV | `Storage` + session-based `Locker` | Separate adapter; account for session invalidation and lock-delay |
| DynamoDB | Conditional writes for storage and lease records | Separate adapter; TTL deletion is asynchronous, so expiry must be checked in conditions |
| redb / RocksDB | Embedded single-node storage | Separate adapter; move blocking work off Tokio and do not imply distributed locking |
| S3 / Vault KV v2 / AWS Secrets Manager | Complete-resource `CertStore` | Implemented; retain shared coordination for references, accounts and challenges |

See [Valkey compatibility](https://valkey.io/topics/migration/),
[etcd APIs](https://etcd.io/docs/v3.6/learning/api/),
[Consul sessions](https://developer.hashicorp.com/consul/docs/automate/session),
and [DynamoDB expiry semantics](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ttl-expired-items.html).
FileStorage, RedisStorage, EtcdStorage and the three remote CertStore adapters
are implemented here. Valkey, Consul, DynamoDB and embedded KV remain candidates.

## File storage coordination

FileStorage serializes lock creation, heartbeats, release, and stale takeover
using permanent `locks/*.guard` sidecars and unique holder IDs. The shared
filesystem must support OS file locks; never delete the sidecars while an
instance is running. Stop all instances before upgrading from the earlier
heartbeat-only protocol, then restart them on the same version.

This is a lease protocol, not a fencing service: a process paused past lease
expiry can resume application writes after another instance takes over.
Deployments requiring fencing must supply a backend that enforces it.
Certificate resources still use separate certificate/key/metadata writes;
`store_tx` rolls back reported errors but is not a crash-atomic transaction.

## Testing

```sh
cargo test                          # unit tests (offline)
cargo test --lib --all-features     # all features enabled together

# Real ACME end-to-end against Pebble (Let's Encrypt's reference server):
./tests/run-pebble.sh               # DNS-01 by default
PEBBLE_CHALLENGE=http-01 ./tests/run-pebble.sh
PEBBLE_CHALLENGE=tls-alpn-01 ./tests/run-pebble.sh

# Deterministic boundary check (forces Cargo offline mode):
./tests/external-validation.sh --offline
# Explicit local Pebble lane (never a production CA test):
./tests/external-validation.sh --pebble
# Listener/handler checks on ephemeral loopback ports (no CA or public DNS):
./tests/external-validation.sh --loopback-challenges
```

The Pebble test exercises the full lifecycle — account registration, selected
HTTP-01/TLS-ALPN-01/DNS-01 validation, issuance, storage persistence, and
forced renewal — and is the reason several protocol details (lowercase
`application/jose+json`, optional challenge tokens, order `ready` → `finalize`
ordering) are correct. HTTP-01 and TLS-ALPN-01 modes disable challtestsrv's
canned challenge responders so the certmagic solver itself serves Pebble's
validation request on the configured high port.

## Project layout

```
src/
├── acme/           # ACME protocol layer: transport, JWS, directory, orders, issuer
├── solvers/        # http-01 / tls-alpn-01 / dns-01 / distributed challenge solvers
├── storage/        # Storage/Locker traits + FileStorage (atomic writes, locks)
├── ocsp/           # RFC 6960 codec + stapling lifecycle
├── certificate.rs  # parsing, name matching, subject qualification, renewal math
├── cache.rs        # in-memory certificate cache
├── config.rs       # orchestration: obtain / renew / manage / revoke
├── handshake.rs    # handshake-time issuance + on-demand gating
├── tls_integration # rustls glue (three integration paths)
└── runtime.rs      # retry budget, job manager, single-flight
```

## License

Apache-2.0 — see [LICENSE](LICENSE).

## Acknowledgments

- Special thanks to [caddyserver/certmagic]https://github.com/caddyserver/certmagic —
  this project follows its design.
- Thanks to [salvo-rs/certon]https://github.com/salvo-rs/certon — its API
  naming inspired the compatibility aliases and high-level manager shipped
  alongside the native API.