certmate-cli 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {certmate_cli-0.1.2 → certmate_cli-0.1.3}/.gitignore +14 -9
- {certmate_cli-0.1.2 → certmate_cli-0.1.3}/PKG-INFO +26 -2
- certmate_cli-0.1.3/README.md +45 -0
- {certmate_cli-0.1.2 → certmate_cli-0.1.3}/certmate_cli/main.py +89 -0
- {certmate_cli-0.1.2 → certmate_cli-0.1.3}/pyproject.toml +2 -2
- certmate_cli-0.1.2/README.md +0 -21
- {certmate_cli-0.1.2 → certmate_cli-0.1.3}/certmate_cli/__init__.py +0 -0
|
@@ -64,9 +64,11 @@ Thumbs.db
|
|
|
64
64
|
# Logs
|
|
65
65
|
*.log
|
|
66
66
|
|
|
67
|
-
# Backup files
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
# Backup files. Anchored to the repository root: unanchored, `*_backup.*`
|
|
68
|
+
# also matched tests/test_backup.py, which therefore existed on disk for
|
|
69
|
+
# months without ever being committed or run in CI.
|
|
70
|
+
/*_old.*
|
|
71
|
+
/*_backup.*
|
|
70
72
|
|
|
71
73
|
# Planning and development documents
|
|
72
74
|
NEXT_FEATURES.md
|
|
@@ -88,12 +90,15 @@ app.py.backup
|
|
|
88
90
|
.claude/
|
|
89
91
|
test_screenshots/
|
|
90
92
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
# Ad-hoc test scripts with real credentials, written at the repository root.
|
|
94
|
+
# Anchored (leading /) on purpose: unanchored, `test_api_*.py` swallowed
|
|
95
|
+
# tests/test_api_key_expiry_and_masking.py — a committed-looking test file
|
|
96
|
+
# that git silently refused to track, so it protected nothing in CI.
|
|
97
|
+
/test_real_cert_*.py
|
|
98
|
+
/test_api_*.py
|
|
99
|
+
/test_certificate_creation_*.py
|
|
100
|
+
/real_world_test_*.py
|
|
101
|
+
/integration_test_*.py
|
|
97
102
|
github_response.md
|
|
98
103
|
|
|
99
104
|
# local
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: certmate-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: CertMate command-line interface — the SSL certificate lifecycle from your terminal
|
|
5
5
|
Project-URL: Homepage, https://github.com/fabriziosalmi/certmate
|
|
6
6
|
Project-URL: Source, https://github.com/fabriziosalmi/certmate/tree/main/clients/certmate-cli
|
|
@@ -23,7 +23,7 @@ Classifier: Topic :: Security
|
|
|
23
23
|
Classifier: Topic :: System :: Systems Administration
|
|
24
24
|
Classifier: Topic :: Utilities
|
|
25
25
|
Requires-Python: >=3.9
|
|
26
|
-
Requires-Dist: certmate-sdk>=0.1.
|
|
26
|
+
Requires-Dist: certmate-sdk>=0.1.3
|
|
27
27
|
Requires-Dist: rich>=13
|
|
28
28
|
Requires-Dist: typer>=0.9
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
@@ -46,6 +46,30 @@ certmate cert create app.example.com --dns cloudflare --dry-run
|
|
|
46
46
|
certmate audit verify
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Pulling certificates onto a host
|
|
50
|
+
|
|
51
|
+
`cert download` fetches one file at a time, so a target server can pull
|
|
52
|
+
exactly what it deploys instead of the certificate manager pushing to it:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
certmate cert download app.example.com --file fullchain -o /etc/ssl/certs/app.pem
|
|
56
|
+
certmate cert download app.example.com --file privkey -o /etc/ssl/private/app.key
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Files are created **0600**, with the mode set at creation rather than after
|
|
60
|
+
the write, so the key is never briefly world-readable.
|
|
61
|
+
|
|
62
|
+
This is worth preferring over pushing when the manager would otherwise need
|
|
63
|
+
credentials on every target host. Give each host an API key scoped to its own
|
|
64
|
+
domain and run the pull on a timer: the host needs no inbound access, and the
|
|
65
|
+
manager holds no credentials for it. `cert`, `chain` and `fullchain` are
|
|
66
|
+
readable by a viewer-role key; `privkey`, `combined` and `pfx` need operator.
|
|
67
|
+
|
|
68
|
+
`--file privkey --key-format pkcs1` serves the legacy
|
|
69
|
+
`BEGIN RSA PRIVATE KEY` form for stacks that reject certbot's PKCS#8.
|
|
70
|
+
`--bundle zip` or `--bundle json` fetch the whole certificate instead, and
|
|
71
|
+
`-o -` writes to stdout.
|
|
72
|
+
|
|
49
73
|
Connection comes from `--url`/`--token` or `CERTMATE_URL`/`CERTMATE_TOKEN`.
|
|
50
74
|
Prefer the `CERTMATE_TOKEN` environment variable over `--token`: command-line
|
|
51
75
|
arguments are visible to other local processes (`ps`) and shell history.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# certmate-cli
|
|
2
|
+
|
|
3
|
+
The [CertMate](https://github.com/fabriziosalmi/certmate) SSL certificate
|
|
4
|
+
lifecycle from your terminal — built on `certmate-sdk`.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install certmate-cli
|
|
8
|
+
export CERTMATE_URL=http://localhost:8000
|
|
9
|
+
export CERTMATE_TOKEN=...
|
|
10
|
+
|
|
11
|
+
certmate cert create app.example.com --dns cloudflare --wait
|
|
12
|
+
certmate cert ls
|
|
13
|
+
certmate cert info app.example.com
|
|
14
|
+
certmate cert renew app.example.com --force
|
|
15
|
+
certmate cert create app.example.com --dns cloudflare --dry-run
|
|
16
|
+
certmate audit verify
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Pulling certificates onto a host
|
|
20
|
+
|
|
21
|
+
`cert download` fetches one file at a time, so a target server can pull
|
|
22
|
+
exactly what it deploys instead of the certificate manager pushing to it:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
certmate cert download app.example.com --file fullchain -o /etc/ssl/certs/app.pem
|
|
26
|
+
certmate cert download app.example.com --file privkey -o /etc/ssl/private/app.key
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Files are created **0600**, with the mode set at creation rather than after
|
|
30
|
+
the write, so the key is never briefly world-readable.
|
|
31
|
+
|
|
32
|
+
This is worth preferring over pushing when the manager would otherwise need
|
|
33
|
+
credentials on every target host. Give each host an API key scoped to its own
|
|
34
|
+
domain and run the pull on a timer: the host needs no inbound access, and the
|
|
35
|
+
manager holds no credentials for it. `cert`, `chain` and `fullchain` are
|
|
36
|
+
readable by a viewer-role key; `privkey`, `combined` and `pfx` need operator.
|
|
37
|
+
|
|
38
|
+
`--file privkey --key-format pkcs1` serves the legacy
|
|
39
|
+
`BEGIN RSA PRIVATE KEY` form for stacks that reject certbot's PKCS#8.
|
|
40
|
+
`--bundle zip` or `--bundle json` fetch the whole certificate instead, and
|
|
41
|
+
`-o -` writes to stdout.
|
|
42
|
+
|
|
43
|
+
Connection comes from `--url`/`--token` or `CERTMATE_URL`/`CERTMATE_TOKEN`.
|
|
44
|
+
Prefer the `CERTMATE_TOKEN` environment variable over `--token`: command-line
|
|
45
|
+
arguments are visible to other local processes (`ps`) and shell history.
|
|
@@ -9,8 +9,12 @@ Connection comes from --url/--token or CERTMATE_URL/CERTMATE_TOKEN.
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
12
14
|
import re
|
|
15
|
+
import stat
|
|
13
16
|
import sys
|
|
17
|
+
from pathlib import Path
|
|
14
18
|
from typing import List, Optional
|
|
15
19
|
|
|
16
20
|
import typer
|
|
@@ -264,6 +268,91 @@ def cert_rm(ctx: typer.Context, domain: str,
|
|
|
264
268
|
out.print(f"[green]deleted[/] {domain}.")
|
|
265
269
|
|
|
266
270
|
|
|
271
|
+
_FILE_DEFAULT_NAME = {
|
|
272
|
+
"cert": "cert.pem",
|
|
273
|
+
"chain": "chain.pem",
|
|
274
|
+
"fullchain": "fullchain.pem",
|
|
275
|
+
"privkey": "privkey.pem",
|
|
276
|
+
"combined": "combined.pem",
|
|
277
|
+
"pfx": "cert.pfx",
|
|
278
|
+
}
|
|
279
|
+
# Anything that can carry key material is written owner-only. The public
|
|
280
|
+
# files get the same treatment: a deploy script that later relaxes them is
|
|
281
|
+
# an explicit act, whereas a private key written 0644 is a silent one.
|
|
282
|
+
_DOWNLOAD_MODE = 0o600
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@cert_app.command("download")
|
|
286
|
+
def cert_download(
|
|
287
|
+
ctx: typer.Context,
|
|
288
|
+
domain: str,
|
|
289
|
+
file: str = typer.Option(
|
|
290
|
+
"fullchain", "--file", "-f",
|
|
291
|
+
help="Which file: cert, chain, fullchain, privkey, combined, pfx. "
|
|
292
|
+
"Use --bundle for a whole-certificate archive instead."),
|
|
293
|
+
output: Optional[str] = typer.Option(
|
|
294
|
+
None, "--output", "-o",
|
|
295
|
+
help="Where to write it. Defaults to the file's usual name in the "
|
|
296
|
+
"current directory; '-' writes to stdout."),
|
|
297
|
+
key_format: Optional[str] = typer.Option(
|
|
298
|
+
None, "--key-format",
|
|
299
|
+
help="pkcs1 or pkcs8, for --file privkey. certbot writes pkcs8; "
|
|
300
|
+
"pkcs1 is the legacy 'BEGIN RSA PRIVATE KEY' form."),
|
|
301
|
+
bundle: Optional[str] = typer.Option(
|
|
302
|
+
None, "--bundle",
|
|
303
|
+
help="Download a bundle instead of a single file: zip or json."),
|
|
304
|
+
):
|
|
305
|
+
"""Download a certificate file, so a host can pull what it needs.
|
|
306
|
+
|
|
307
|
+
Pulling beats pushing when the certificate manager would otherwise need
|
|
308
|
+
credentials on every target host: give each host an API key scoped to its
|
|
309
|
+
own domain and let it fetch on a timer.
|
|
310
|
+
|
|
311
|
+
certmate cert download example.com --file fullchain -o /etc/ssl/certs/x.pem
|
|
312
|
+
certmate cert download example.com --file privkey -o /etc/ssl/private/x.key
|
|
313
|
+
"""
|
|
314
|
+
# Every one of these is checked before a request goes out: being told
|
|
315
|
+
# "invalid key_format" by the server after a round trip is a worse error
|
|
316
|
+
# than being told here, and it costs an authenticated call to learn it.
|
|
317
|
+
if bundle is not None and bundle not in ("zip", "json"):
|
|
318
|
+
_die("--bundle must be zip or json")
|
|
319
|
+
if bundle and key_format:
|
|
320
|
+
_die("--key-format applies to --file privkey, not to a bundle")
|
|
321
|
+
if key_format is not None:
|
|
322
|
+
if key_format not in ("pkcs1", "pkcs8"):
|
|
323
|
+
_die(f"unknown --key-format {key_format!r}; use pkcs1 or pkcs8")
|
|
324
|
+
if not bundle and file != "privkey":
|
|
325
|
+
_die(f"--key-format applies to --file privkey, not to {file!r}")
|
|
326
|
+
|
|
327
|
+
if bundle:
|
|
328
|
+
data = _run(lambda: _client(ctx).download_certificate(domain, fmt=bundle))
|
|
329
|
+
default_name = f"{domain}.zip" if bundle == "zip" else f"{domain}.json"
|
|
330
|
+
payload = json.dumps(data, indent=2).encode() if bundle == "json" else data
|
|
331
|
+
else:
|
|
332
|
+
if file not in _FILE_DEFAULT_NAME:
|
|
333
|
+
_die(f"unknown --file {file!r}; use one of "
|
|
334
|
+
f"{', '.join(sorted(_FILE_DEFAULT_NAME))}")
|
|
335
|
+
payload = _run(lambda: _client(ctx).download_certificate_file(
|
|
336
|
+
domain, file, key_format=key_format))
|
|
337
|
+
default_name = _FILE_DEFAULT_NAME[file]
|
|
338
|
+
|
|
339
|
+
if output == "-":
|
|
340
|
+
# Binary-safe: .pfx and .zip are not text.
|
|
341
|
+
sys.stdout.buffer.write(payload)
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
target = Path(output or default_name)
|
|
345
|
+
# Create with the restrictive mode rather than chmod-ing after: between
|
|
346
|
+
# an open() and a chmod() the key is readable by anyone on the box.
|
|
347
|
+
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, _DOWNLOAD_MODE)
|
|
348
|
+
with os.fdopen(fd, "wb") as fh:
|
|
349
|
+
fh.write(payload)
|
|
350
|
+
# An existing file keeps its old mode through O_CREAT, so state what was
|
|
351
|
+
# actually written rather than claiming 0600 unconditionally.
|
|
352
|
+
mode = stat.S_IMODE(os.stat(target).st_mode)
|
|
353
|
+
out.print(f"[green]wrote[/] {target} ({len(payload)} bytes, mode {mode:04o})")
|
|
354
|
+
|
|
355
|
+
|
|
267
356
|
@cert_app.command("reissue")
|
|
268
357
|
def cert_reissue(ctx: typer.Context, domain: str,
|
|
269
358
|
san: Optional[str] = typer.Option(None, "--san", help="Comma-separated SAN domains.")):
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "certmate-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "CertMate command-line interface — the SSL certificate lifecycle from your terminal"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -33,7 +33,7 @@ classifiers = [
|
|
|
33
33
|
# The SDK floor tracks the CLI release: 0.1.2 needs TransportError and the
|
|
34
34
|
# renew/audit semantics introduced in certmate-sdk 0.1.2.
|
|
35
35
|
dependencies = [
|
|
36
|
-
"certmate-sdk>=0.1.
|
|
36
|
+
"certmate-sdk>=0.1.3",
|
|
37
37
|
"typer>=0.9",
|
|
38
38
|
"rich>=13",
|
|
39
39
|
]
|
certmate_cli-0.1.2/README.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# certmate-cli
|
|
2
|
-
|
|
3
|
-
The [CertMate](https://github.com/fabriziosalmi/certmate) SSL certificate
|
|
4
|
-
lifecycle from your terminal — built on `certmate-sdk`.
|
|
5
|
-
|
|
6
|
-
```bash
|
|
7
|
-
pip install certmate-cli
|
|
8
|
-
export CERTMATE_URL=http://localhost:8000
|
|
9
|
-
export CERTMATE_TOKEN=...
|
|
10
|
-
|
|
11
|
-
certmate cert create app.example.com --dns cloudflare --wait
|
|
12
|
-
certmate cert ls
|
|
13
|
-
certmate cert info app.example.com
|
|
14
|
-
certmate cert renew app.example.com --force
|
|
15
|
-
certmate cert create app.example.com --dns cloudflare --dry-run
|
|
16
|
-
certmate audit verify
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Connection comes from `--url`/`--token` or `CERTMATE_URL`/`CERTMATE_TOKEN`.
|
|
20
|
-
Prefer the `CERTMATE_TOKEN` environment variable over `--token`: command-line
|
|
21
|
-
arguments are visible to other local processes (`ps`) and shell history.
|
|
File without changes
|