Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Use only post-2020 cipher suites. Minimum TLS 1.2, prefer TLS 1.3.

- For public SSL recommend the user uses [Let's Encrypt](https://letsencrypt.org/) or a commercial CA. This tool is intended for local development and private networks only.

- Allow the user to create the root key with a passphrase but warn them they have to enter this every single time they use the CA. The CA is not intended for production use, and the user should be aware of the security implications of using a local CA. Default should be blank.
- The CA root key is generated unencrypted (no passphrase support). The CA is not intended for production use, and the user should be aware of the security implications of using a local CA.

- At init, allow the user to optionally restrict the CN/SAN name suffix (e.g. `.local`). The input is presented but can be bypassed (blank = unrestricted). The policy is stored in the `meta` table (`name_suffix`, normalised by `config.normalize_name_suffix`) and enforced in `ca.issue_cert` — the single chokepoint all modes funnel through — against the CN and every DNS SAN (IP SANs exempt). It is fixed at init time.

Expand All @@ -75,13 +75,18 @@ Full-screen textual UI. Entry point: `ssltui/tui.py`.
### Headless CLI mode

```bash
ssltui --renew # renew all certs expiring within threshold
ssltui --renew --cert common_name # renew a specific cert
ssltui --issue --cn foo.local [--san ...] # non-interactive issue
ssltui --status # print expiry table, exit 0 if all ok
ssltui renew # renew all certs expiring within threshold
ssltui renew --cert common_name # renew a specific cert
ssltui issue --cn foo.local [--san ...] # non-interactive issue
ssltui status # print expiry table, exit 0 if all ok
ssltui get --cn foo.local --what cert # print/save a cert, key, chain, or full bundle
ssltui getroot # print the root CA certificate (PEM) to stdout
ssltui audit # print the full stored event log
```

Used by cron. Must produce machine-readable output (exit codes + stdout) and never draw curses.
Subcommands, not `--flag` forms — `--dir PATH` (data dir override) is the only
top-level flag; it's parsed before the subcommand. Used by cron. Must produce
machine-readable output (exit codes + stdout) and never draw curses.

### API mode

Expand Down Expand Up @@ -115,7 +120,7 @@ $SSLTUI_DIR/
All cert metadata, revocations, counters (serial / CRL number), and an
append-only **event log** live in a single SQLite database (`ssltui/store.py`),
replacing the earlier `index.json`. WAL mode plus a busy timeout let the TUI,
the cron `--renew` process, and the multi-threaded Flask API read and write
the cron `renew` process, and the multi-threaded Flask API read and write
concurrently without explicit file locking. Cert/key material itself stays as
flat PEM files under `certs/<cn>/`.

Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ uv run ssltui ~/my-ca status
```

Each directory is an independent CA. You can maintain multiple CAs by pointing
at different directories or by using **p Root dir** in the TUI to switch at runtime.
at different directories (`SSLTUI_DIR` or `--dir`/the positional path shorthand);
there is currently no way to switch directories at runtime from within the TUI.

### Directory layout

Expand Down Expand Up @@ -184,9 +185,30 @@ uv run ssltui getroot > local-ca.crt
uv run ssltui getroot --out local-ca.crt
```

### Get a certificate, key, chain, or bundle

Print or save material for an existing certificate. With no flags and a TTY,
it prompts interactively; non-interactively, `--cn` is required and `--what`
defaults to `cert`:

```bash
uv run ssltui get --cn api.local --what cert # to stdout
uv run ssltui get --cn api.local --what key --out api.key # key + chmod 600
uv run ssltui get --cn api.local --what full --out api.pem # chain + key bundle
```

`--what` accepts `cert` (leaf cert), `key` (private key), `chain` (leaf + CA),
or `full` (chain + key concatenated).

### Print the audit log

```bash
uv run ssltui audit # every issue/renew/revoke/key_download event, oldest first
```

### Cron entry

Install via the TUI's **Cron Schedule** option, or add manually:
Add a crontab entry manually:

```
0 3 * * * /path/to/venv/bin/ssltui renew >> ~/.local/share/ssltui/renewal.log 2>&1
Expand Down Expand Up @@ -412,5 +434,5 @@ Run the tests:
uv run pytest
```

CI runs the same `ruff check`, `ruff format --check`, and `pytest` on every push
and pull request (see [.github/workflows/ci.yml](.github/workflows/ci.yml)).
CI runs the same `ruff check`, `ruff format --check`, and `pytest` on every pull
request against `main` (see [.github/workflows/pr.yaml](.github/workflows/pr.yaml)).
8 changes: 8 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ outside that suffix is also rejected with `400` (IP SANs are exempt):
{ "error": "name(s) not permitted by CA policy (must be under .local): app.dev" }
```

A `cn` that already has a certificate returns `409 Conflict` — revoke it first
(`DELETE` is not exposed; use the TUI or CLI to revoke):

```json
{ "error": "a certificate for 'api.test.local' already exists; revoke it before issuing a new one" }
```

### Get certificate metadata

```
Expand Down Expand Up @@ -249,6 +256,7 @@ HTTP status code:
| `400 Bad Request` | Invalid or missing request fields (e.g. no `cn`, bad `key_type`), or a CN/SAN outside the CA's name-suffix policy |
| `401 Unauthorized` | Missing or incorrect bearer token |
| `404 Not Found` | No certificate exists for the given CN, or the file is missing |
| `409 Conflict` | A certificate for the requested `cn` already exists |
| `500 Internal Server Error` | Unexpected server error (`{ "error": "Internal server error" }`) |

```json
Expand Down
25 changes: 22 additions & 3 deletions docs/TUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@ SAN that falls outside it.
| `c` | View the trusted root CA certificate |
| `d` | View the selected certificate |
| `x` | Revoke and delete the selected certificate |
| `p` | Change CA root directory |
| `t` | View the API bearer token |
| `a` | Export the audit log (CSV) |
| `r` | Refresh table |
| `q` | Quit |
| `↑ ↓` | Navigate certificate list |
| `Enter` | Open certificate detail |

There is currently no way to change the CA root directory from within the
TUI — restart with a different `SSLTUI_DIR`/`--dir` (or positional path) to
work against another CA.

### Certificate detail

| Key | Action |
Expand All @@ -80,12 +85,26 @@ SAN that falls outside it.
| Key | Action |
|-----|--------|
| `y` | Copy PEM to clipboard (OSC 52 — works in Windows Terminal) |
| `s` | Save PEM to a file (prompts for path) |
| `k` | Toggle the private key into/out of the view (leaf certs only; each reveal, copy, or save while showing the key is recorded as a `key_download` audit event) |
| `s` | Save PEM to a file (prompts for path) — includes the key if it's currently shown |
| `q` / `Esc` | Back |

### Forms (issue cert, init CA, change dir)
### Forms (issue cert, init CA)

| Key | Action |
|-----|--------|
| `Enter` | Advance to next field; submit on last field |
| `Esc` | Cancel |

## API server screen

Started via `ssltui serve` from an interactive terminal (see
[the README](../README.md#api-mode)); shows connection status and a live
request/event log.

| Key | Action |
|-----|--------|
| `t` | View the API bearer token |
| `i` | Show a curl example for issuing a certificate |
| `c` | Copy the dashboard URL to the clipboard |
| `q` | Stop the server and exit |
2 changes: 1 addition & 1 deletion ssltui/renewal.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Expiry checks and renewal logic for headless --renew mode."""
"""Expiry checks and renewal logic for headless renew mode."""

from __future__ import annotations

Expand Down
2 changes: 1 addition & 1 deletion ssltui/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A single ``ca.db`` in the CA root holds the cert index, revocation list, an
append-only event log, and small counters (serial, CRL number, version). All
access goes through the module functions below; callers never touch the DB
directly. WAL mode plus a busy timeout let the TUI, the cron ``--renew``
directly. WAL mode plus a busy timeout let the TUI, the cron ``renew``
process, and the multi-threaded Flask API read and write concurrently without
explicit file locking.
"""
Expand Down
1 change: 1 addition & 0 deletions ssltui/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ class MainScreen(Screen):
Binding("x", "revoke_selected", "Revoke"),
Binding("t", "view_token", "API Token"),
Binding("a", "export_audit", "Audit"),
Binding("r", "refresh", "Refresh"),
Binding("q", "quit", "Quit"),
]

Expand Down
Loading