Skip to content

Commit 21e2b70

Browse files
committed
release: v0.13.0 — shimkit tls --method dns-cloudflare
DNS-01 ACME challenge via Cloudflare. The path to wildcard certs (`*.example.com`) — Let's Encrypt only issues those via DNS-01. Surface (additive — webroot path unchanged): shimkit tls request --method dns-cloudflare \ --credentials FILE -d D -d '*.D' ... Auto-selects the `certbot/dns-cloudflare:v3.0.1` image. Webroot method still uses `certbot/certbot:v3.0.1`. Setup: echo 'dns_cloudflare_api_token = TOKEN' > ~/.secrets/cloudflare.ini chmod 600 ~/.secrets/cloudflare.ini # mandatory — both shimkit # and certbot refuse # group/world-readable The parent directory of the credentials file is mounted at `/credentials` inside the container, read-only. shimkit catches the mode check up-front with a clearer message than certbot. Cloudflare-only today. Other DNS providers (Route53, DigitalOcean, Hurricane Electric, etc.) each need their own credential surface and a different `certbot/dns-<provider>` image. Opt-in extras in a future release. Implementation: - `certbot.request_argv` widened to `method: Literal["webroot", "dns-cloudflare"]`. The DNS path adds the four `--dns-cloudflare*` flags including the propagation-seconds knob. - `certbot.container_volumes` accepts an optional `credentials` Path and mounts its parent at `/credentials:ro`. - `manager.request` chooses image based on method; runs the per- method preflight (webroot dir vs credentials-file mode 0600). - `_is_valid_domain` accepts a leading `*.` (wildcard required by DNS-01). - New `tools.tls.{certbot_dns_cloudflare_image, cloudflare_propagation_seconds}` config fields. Propagation has `Field(ge=0, le=600)`. Tests: 17 new in test_tools_tls_dns_cloudflare.py (1027 → 1044 total). Pure argv-builder shape (DNS flags + propagation + staging/dry-run; webroot path unaffected), container_volumes with + without credentials mount, manager validation (missing- credentials refusal / missing-file refusal / loose-mode refusal / happy path picks dns-cloudflare image and mounts credentials parent / JSON output includes method), config plumbing (propagation_seconds range validation incl. negative-value rejection). Gates: pytest 1044 passed, ruff clean, mypy strict clean. No new optional dependency extras.
1 parent 11f575b commit 21e2b70

12 files changed

Lines changed: 819 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,61 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
66

77
## [Unreleased]
88

9+
## [0.13.0] — 2026-05-16
10+
11+
### Added
12+
13+
- `shimkit tls request --method dns-cloudflare` — DNS-01 ACME
14+
challenge via Cloudflare. Required for wildcard certs (`*.example.com`).
15+
Uses the upstream `certbot/dns-cloudflare:v3.0.1` image (auto-
16+
selected when `--method dns-cloudflare` is passed; the webroot
17+
method still uses `certbot/certbot:v3.0.1`).
18+
- `--credentials PATH` flag on `tls request`. Points at a file
19+
containing `dns_cloudflare_api_token = <token>` (one line).
20+
Manager refuses the file when mode isn't 0600 — certbot also
21+
refuses, but shimkit catches it earlier with a clearer message.
22+
Parent directory of the file is mounted at `/credentials`
23+
inside the container read-only.
24+
- `tools.tls.certbot_dns_cloudflare_image` config field — pin the
25+
DNS plugin image independently of the webroot image.
26+
- `tools.tls.cloudflare_propagation_seconds` config field —
27+
default `60`, range `[0, 600]`. Lower it on accounts with fast
28+
Cloudflare propagation; raise for slow zones.
29+
30+
### Changed
31+
32+
- `_is_valid_domain` accepts a leading `*.` for wildcard domains
33+
(required by DNS-01). The rest of the domain validates as before.
34+
- `tls request` `--webroot` is now optional — required for the
35+
webroot method but not for dns-cloudflare. The error message
36+
guides the user to the right flag combination for each method.
37+
- `TlsConfig.default_method` widened from `Literal["webroot"]` to
38+
`Literal["webroot", "dns-cloudflare"]`.
39+
40+
### Tests
41+
42+
- 17 new tests in `tests/test_tools_tls_dns_cloudflare.py` (1027 →
43+
1044 total). Pure argv-builder shape (DNS flags + propagation +
44+
staging/dry-run combos; webroot path unaffected), container_volumes
45+
with + without credentials mount, manager validation
46+
(missing-credentials refusal / missing-file refusal / loose-mode
47+
refusal / happy path picks dns-cloudflare image and mounts
48+
credentials parent dir / JSON output includes method), config
49+
plumbing (cloudflare_propagation_seconds range validation).
50+
51+
### Notes
52+
53+
DNS-01 is the only ACME path that supports wildcard certs. The
54+
plugin image is ~70MB so the first `tls request --method
55+
dns-cloudflare` pulls it; subsequent runs are local.
56+
57+
Cloudflare-only today. Other DNS providers (Route53,
58+
DigitalOcean, etc.) each need their own credential surface — opt-
59+
in extras in a future release.
60+
61+
Gates: pytest 1044 passed, ruff clean, mypy strict clean. No new
62+
optional dependency extras (reuses `[docker-clean]`'s `docker`).
63+
964
## [0.12.0] — 2026-05-15
1065

1166
### Changed

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Top-level utilities (not tools):
9191

9292
Per-version, user-facing summaries (newest first):
9393

94+
- **[`v0.13.0`](release-notes/v0.13.0.md)** — `shimkit tls --method
95+
dns-cloudflare` for DNS-01 + wildcard certs.
9496
- **[`v0.12.0`](release-notes/v0.12.0.md)** — stale-doc cleanup +
9597
codecov upload + `gh attestation verify` smoke. No code changes.
9698
- **[`v0.11.0`](release-notes/v0.11.0.md)** — docs consolidation +

docs/release-notes/v0.13.0.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# shimkit 0.13.0
2+
3+
`shimkit tls --method dns-cloudflare` — DNS-01 ACME challenge via
4+
Cloudflare. The path to wildcard certs (`*.example.com`). For the
5+
full machine-readable changelog, see
6+
[`CHANGELOG.md`](../../CHANGELOG.md).
7+
8+
---
9+
10+
## TL;DR
11+
12+
```
13+
shimkit tls request --method dns-cloudflare --credentials FILE -d ...
14+
```
15+
16+
Uses `certbot/dns-cloudflare:v3.0.1` (auto-selected). Webroot
17+
method still uses `certbot/certbot:v3.0.1` — the two paths are
18+
independent and you can mix-and-match by domain.
19+
20+
---
21+
22+
## Why DNS-01
23+
24+
The webroot (HTTP-01) method that shipped in v0.8.0 works for
25+
single hostnames you control via HTTP. But **wildcard certs
26+
(`*.example.com`) only work via DNS-01** — that's a Let's Encrypt
27+
constraint, not a shimkit one.
28+
29+
DNS-01 also works for domains where you don't (or can't) terminate
30+
HTTP locally — internal-only services, mail hosts, anything behind
31+
a load balancer that doesn't forward `/.well-known/acme-challenge/`.
32+
33+
---
34+
35+
## Setup
36+
37+
1. **Create a Cloudflare API token** with `Zone:DNS:Edit` scope on
38+
the zone you're issuing for. <https://dash.cloudflare.com/profile/api-tokens>.
39+
(Don't use the legacy "Global API Key" — that's account-wide.)
40+
41+
2. **Write the credentials file**:
42+
43+
```bash
44+
echo 'dns_cloudflare_api_token = YOUR-CLOUDFLARE-API-TOKEN' \
45+
> ~/.secrets/cloudflare.ini
46+
chmod 600 ~/.secrets/cloudflare.ini
47+
```
48+
49+
Mode 0600 is mandatory — both shimkit and certbot refuse the
50+
file if it's group- or world-readable.
51+
52+
3. **Issue the cert** (staging first; production rate limits are
53+
punishing):
54+
55+
```bash
56+
shimkit tls request --yes --staging \
57+
--email ops@example.com \
58+
--method dns-cloudflare \
59+
--credentials ~/.secrets/cloudflare.ini \
60+
-d example.com -d '*.example.com'
61+
```
62+
63+
4. Once staging works, drop `--staging` for the real cert.
64+
65+
---
66+
67+
## How it works
68+
69+
```
70+
docker run --rm \
71+
-v ~/.shimkit/data/tls/etc-letsencrypt:/etc/letsencrypt \
72+
-v ~/.shimkit/data/tls/var-lib-letsencrypt:/var/lib/letsencrypt \
73+
-v ~/.secrets:/credentials:ro \
74+
certbot/dns-cloudflare:v3.0.1 \
75+
certonly --non-interactive --agree-tos \
76+
--email ops@example.com \
77+
--dns-cloudflare \
78+
--dns-cloudflare-credentials /credentials/cloudflare.ini \
79+
--dns-cloudflare-propagation-seconds 60 \
80+
-d example.com -d '*.example.com'
81+
```
82+
83+
The parent directory of the credentials file gets mounted at
84+
`/credentials` inside the container, so
85+
`~/.secrets/cloudflare.ini` on the host becomes
86+
`/credentials/cloudflare.ini` inside.
87+
88+
Cloudflare's plugin uses the API token to write the
89+
`_acme-challenge.<domain>` TXT record, waits the configured
90+
propagation seconds (default 60), then asks the ACME CA to
91+
validate.
92+
93+
---
94+
95+
## What's new
96+
97+
| | |
98+
|---|---|
99+
| `--method webroot` | (default) HTTP-01 via local webserver |
100+
| `--method dns-cloudflare` | DNS-01 via Cloudflare API |
101+
| `--credentials PATH` | Cloudflare creds file (mode 0600 required) |
102+
| `tools.tls.cloudflare_propagation_seconds` | Default `60`, range `[0, 600]` |
103+
| `tools.tls.certbot_dns_cloudflare_image` | Pinned `certbot/dns-cloudflare:v3.0.1` |
104+
105+
Wildcards (`*.example.com`) now validate in shimkit's domain check
106+
— previously only single-label-then-dot patterns were accepted.
107+
108+
---
109+
110+
## Renewal
111+
112+
DNS-01 certs renew the same way as webroot certs:
113+
114+
```bash
115+
shimkit tls renew --yes
116+
```
117+
118+
The `renew` command doesn't need `--method`, `--credentials`, or
119+
`--webroot` — certbot reads each cert's renewal config from
120+
`/etc/letsencrypt/renewal/<domain>.conf` (written at issuance
121+
time) and reuses the same method.
122+
123+
The daily renewal cron from v0.8.0 covers both methods.
124+
125+
---
126+
127+
## Limits
128+
129+
- **Cloudflare-only today.** Other DNS providers (Route53,
130+
DigitalOcean, Hurricane Electric, etc.) each need their own
131+
credential surface and a different `certbot/dns-<provider>`
132+
image. Opt-in extras in a future release.
133+
- **Token, not key.** Use a scoped API token (`Zone:DNS:Edit` on
134+
the zone), not the legacy Global API Key. The Global API Key is
135+
account-wide and gives the credentials file far more power than
136+
cert issuance needs.
137+
138+
---
139+
140+
## Stats
141+
142+
- Tests: 1027 → 1044 (+17)
143+
- Gates: pytest, ruff, mypy strict — all green
144+
- New optional extras: 0
145+
146+
---
147+
148+
## Upgrading
149+
150+
```bash
151+
uv tool upgrade shimkit
152+
pipx upgrade shimkit
153+
```
154+
155+
Existing webroot users see no behavioural change. To start using
156+
DNS-01, add the credentials file + pass `--method dns-cloudflare
157+
--credentials`.

docs/tools/tls.md

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ flags after (`--json`, `--dry-run`, `--yes`, `--force`).
2424

2525
## How it works
2626

27-
`shimkit tls request` runs:
27+
### Webroot (HTTP-01, default)
28+
29+
`shimkit tls request --method webroot` runs:
2830

2931
```
3032
docker run --rm \
@@ -39,6 +41,45 @@ docker run --rm \
3941
[--staging]
4042
```
4143

44+
### DNS-cloudflare (DNS-01, v0.13.0+)
45+
46+
`shimkit tls request --method dns-cloudflare` runs:
47+
48+
```
49+
docker run --rm \
50+
-v ~/.shimkit/data/tls/etc-letsencrypt:/etc/letsencrypt \
51+
-v ~/.shimkit/data/tls/var-lib-letsencrypt:/var/lib/letsencrypt \
52+
-v <credentials-parent-dir>:/credentials:ro \
53+
certbot/dns-cloudflare:v3.0.1 \
54+
certonly --non-interactive --agree-tos \
55+
--email ops@example.com \
56+
--dns-cloudflare \
57+
--dns-cloudflare-credentials /credentials/cloudflare.ini \
58+
--dns-cloudflare-propagation-seconds 60 \
59+
-d example.com [-d '*.example.com'] \
60+
[--staging]
61+
```
62+
63+
**Required for wildcards.** `*.example.com` certs only work via
64+
DNS-01. The Cloudflare plugin needs a Cloudflare API token with
65+
`Zone:DNS:Edit` scope on the zone you're issuing for.
66+
67+
**Credentials file format** (one line):
68+
69+
```ini
70+
dns_cloudflare_api_token = your-cloudflare-api-token-here
71+
```
72+
73+
**Mode 0600 required.** certbot refuses any credentials file that's
74+
group- or world-readable; shimkit refuses up-front with a clear
75+
error before invoking the container. Run `chmod 600 cloudflare.ini`
76+
before passing it.
77+
78+
The parent directory of the credentials file is mounted at
79+
`/credentials` inside the container, read-only. So
80+
`/secrets/cloudflare.ini` on the host becomes
81+
`/credentials/cloudflare.ini` inside.
82+
4283
The webroot must already be served at
4384
`http://<domain>/.well-known/acme-challenge/` for the ACME challenge
4485
to succeed. The recommended layout for `shimkit web nginx vhost`-
@@ -68,8 +109,9 @@ renewal, so nginx never needs to be told a new cert path.
68109
## Examples
69110

70111
```bash
71-
# Request a cert in staging first (recommended — Let's Encrypt
72-
# rate-limits production aggressively, but staging is forgiving).
112+
# Webroot (HTTP-01) — Request a cert in staging first (recommended —
113+
# Let's Encrypt rate-limits production aggressively, but staging is
114+
# forgiving).
73115
shimkit tls request --yes --staging \
74116
--email ops@example.com \
75117
--webroot /var/www/example \
@@ -81,6 +123,15 @@ shimkit tls request --yes \
81123
--webroot /var/www/example \
82124
-d example.com -d www.example.com
83125

126+
# DNS-cloudflare (DNS-01) — required for wildcards.
127+
echo 'dns_cloudflare_api_token = YOUR-TOKEN-HERE' > ~/.secrets/cloudflare.ini
128+
chmod 600 ~/.secrets/cloudflare.ini
129+
shimkit tls request --yes --staging \
130+
--email ops@example.com \
131+
--method dns-cloudflare \
132+
--credentials ~/.secrets/cloudflare.ini \
133+
-d example.com -d '*.example.com'
134+
84135
# Enumerate local certs.
85136
shimkit tls list
86137
shimkit tls list --json
@@ -156,10 +207,11 @@ in `tls list` / `tls status` (which shells out to `openssl x509
156207
punishing (5 failed validations / hour / hostname). Always
157208
pass `--staging` for first runs; the resulting cert isn't
158209
trusted but proves the webroot setup works.
159-
- **Webroot vs DNS-01.** Only webroot is wired today. DNS-01
160-
(required for wildcard certs) lands as opt-in extras in a
161-
future release — each provider needs its own credential
162-
surface (`cloudflare`, `route53`, etc.).
210+
- **Webroot vs DNS-01.** Both are wired as of v0.13.0. Webroot
211+
(HTTP-01) is the default; DNS-01 via Cloudflare is opt-in via
212+
`--method dns-cloudflare` and is the **only** path to wildcard
213+
certs. Other DNS providers (Route53, DigitalOcean, etc.) each
214+
need their own credential surface and are deferred.
163215
- **No PyPI extra.** This tool reuses the `[docker-clean]`
164216
extra's `docker` package — no new install footprint.
165217
- **Renewal cadence.** Let's Encrypt certs are valid for 90 days;

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "shimkit"
7-
version = "0.12.0"
7+
version = "0.13.0"
88
description = "A toolkit of developer utilities — Java version manager, shell upgrader, and more. Python tools, shimmed by bash."
99
readme = "README.md"
1010
license = { file = "LICENSE" }

src/shimkit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
Python tools, shimmed by bash.
44
"""
55

6-
__version__ = "0.12.0"
6+
__version__ = "0.13.0"
77
__all__ = ["__version__"]

src/shimkit/config/defaults.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@
193193
"tls": {
194194
"data_dir": "~/.shimkit/data/tls",
195195
"certbot_image": "certbot/certbot:v3.0.1",
196+
"certbot_dns_cloudflare_image": "certbot/dns-cloudflare:v3.0.1",
196197
"default_method": "webroot",
198+
"cloudflare_propagation_seconds": 60,
197199
"default_email": null,
198200
"renewal_schedule": "17 3 * * *",
199201
"revoke_severe_token": "REVOKE-TLS"

src/shimkit/config/schema.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,20 @@ class TlsConfig(_StrictModel):
382382

383383
# Volume root for /etc/letsencrypt content. Persists across renewals.
384384
data_dir: str = "~/.shimkit/data/tls"
385-
# Certbot image to run one-shot. Pin to a known-good version rather
386-
# than `:latest` so a registry-side image change can't break renewals
387-
# silently.
385+
# Certbot image used for webroot challenges. Pin to a known-good
386+
# version rather than `:latest` so a registry-side image change
387+
# can't break renewals silently.
388388
certbot_image: str = "certbot/certbot:v3.0.1"
389-
# Default ACME challenge method. Only `webroot` is wired today;
390-
# `dns-cloudflare` etc. land as opt-in extras in a later release.
391-
default_method: Literal["webroot"] = "webroot"
389+
# Certbot image with the dns-cloudflare plugin pre-installed.
390+
# Used when `--method dns-cloudflare` is passed.
391+
certbot_dns_cloudflare_image: str = "certbot/dns-cloudflare:v3.0.1"
392+
# Default ACME challenge method. `webroot` (HTTP-01) is the
393+
# original; `dns-cloudflare` (DNS-01) lands in v0.13.0 and is the
394+
# path to wildcard certs.
395+
default_method: Literal["webroot", "dns-cloudflare"] = "webroot"
396+
# Cloudflare DNS propagation seconds. The plugin's recommended
397+
# baseline; safe to lower on accounts with fast propagation.
398+
cloudflare_propagation_seconds: int = Field(default=60, ge=0, le=600)
392399
# ACME account email. Required by Let's Encrypt for issuance unless
393400
# `--register-unsafely-without-email` is passed (we don't expose
394401
# that flag). User config overrides per-install.

0 commit comments

Comments
 (0)