5.8 KiB
5.8 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
openvpncertupdate is a single-file Python + curses TUI tool for managing OpenVPN user certificates via EasyRSA 3.2.x. It lists expiring/expired certs, re-issues them with fresh keys, creates new certs, and delivers configs via Cryptgeon (one-time password URL) and email. It also supports a non-interactive CLI mode for scripting and cron use.
Running
pip install -r requirements.txt # just: cryptography>=41
python3 openvpncertupdate.py # interactive TUI
python3 openvpncertupdate.py --create CN --email user@example.com
python3 openvpncertupdate.py --reissue CN [--email user@example.com]
python3 openvpncertupdate.py --revoke CN
python3 openvpncertupdate.py --gen-crl
python3 openvpncertupdate.py --list
python3 openvpncertupdate.py --list-all
Edit the SETTINGS block at the top of openvpncertupdate.py before first run.
CLI flags
| Flag | Effect |
|---|---|
--create CN |
Issue new cert (requires --email) |
--reissue CN |
Revoke + reissue cert (--email optional, falls back to stored) |
--revoke CN |
Revoke cert and regenerate CRL |
--gen-crl |
Regenerate and copy CRL only |
--list |
List recently-expired/soon-to-expire CNs (per DAYS_PAST/DAYS_AHEAD) with email; read-only, no CA passphrase needed |
--list-all |
List all CNs with email; read-only, no CA passphrase needed |
--email EMAIL |
Recipient address |
--send-email |
Force email delivery |
--no-send-email |
Skip email; print URL to stdout |
--show-eml |
Print base64-encoded .eml to stdout (implies --no-send-email unless --send-email also given) |
--config PATH |
External .conf file overriding SETTINGS (overrides CONFIG_PATH; missing file here is an error) |
Tests
python3 -m pytest tests/ -v
python3 -m pytest tests/test_password.py -v # single file
python3 -m pytest tests/test_pki.py::test_sorted_ascending -v # single test
File layout — sections inside openvpncertupdate.py
| Section | Key symbols |
|---|---|
| SETTINGS | all-caps constants |
| SETTINGS OVERRIDE | ConfigError, load_settings_overrides(), _OVERRIDABLE_SETTINGS |
| PKI | CertInfo, load_expiring_certs(), load_all_certs(), _parse_index_line(), get_email() |
| PASSWORD | generate_password() |
| EASYRSA | EasyRSAError, revoke_issued(), build_client_full(), gen_crl(), copy_crl(), is_ca_key_encrypted(), resolve_ca_passphrase() |
| CONFIG | build_ovpn() → vpn-configs/<CN>_<YYYY-MM-DD>_<NN>/CONFIG_NAME |
| CRYPTGEON | CryptgeonError, create_note() |
| MAILER | build_mime_message(), send_email() |
| TUI WIDGETS | InputField, clamp(), draw_box(), init_colors(), COLOR_* |
| TUI DIALOGS | show_confirm(), show_cert_form(), CertFormResult |
| TUI SCREEN | show_main_screen(), Action, ScreenResult |
| APP | CursesApp |
| CLI | CliRunner, _build_parser() |
| ENTRY POINT | main() |
Re-issue workflow
revoke-issued <CN>— archives old key + CSR topki/revoked/build-client-full <CN> --passout=pass:<pw>— generates new key + cert- CRL not auto-updated during renewal; use "Regenerate CRL" menu item or
rhotkey
TUI key bindings
| Key | Action |
|---|---|
↑/↓ |
Navigate list |
Space |
Toggle checkbox selection |
Enter |
Confirm / open selected item |
r |
Revoke selected cert |
A |
Toggle between expiring-only and all-certs view |
q/Esc |
Quit |
Key constraints
- External config file (
load_settings_overrides(), run once inmain()right after arg parsing, before dispatch): resolution order is--config PATH>CONFIG_PATHsetting ><this-script-path>.confnext to the script. The CLI flag orCONFIG_PATHmake the path explicit — a missing file there is a fatalConfigError; the default<script>.confpath is optional and silently skipped if absent. The file is executed as Python (same syntax as theSETTINGSblock, so only run trusted files) and only names listed in_OVERRIDABLE_SETTINGSare applied —CONFIG_PATHitself is deliberately not overridable this way - Email: set
SMTP_HOSTto use smtplib (SMTP_TLS:"starttls"/"ssl"/""); leave empty to useMAIL_BINARY. Auth skipped whenSMTP_USER="" - EasyRSA called with
--batch;--passin=pass:<passphrase>omitted when the resolved passphrase is empty - CA passphrase resolution (
resolve_ca_passphrase(), run once inmain()right after arg parsing, before dispatch):CA_PASSPHRASE=""→ auto-detect viais_ca_key_encrypted()(checks<PKI_DIR>/private/ca.keyPEM header forENCRYPTED) and prompt only if encrypted;"!empty"→ never check/prompt, passphrase is"";"!ask"→ always prompt, skip detection; any other value → used literally.--list/--list-allskip this resolution entirely since they only readindex.txtand never touch the CA - Cryptgeon: matches the
occultobrowser client —key=os.urandom(32)used directly (no derivation) for AES-256-GCM;contents=base64(b"AES-GCM") + "--" + base64(nonce) + "--" + base64(ciphertext);meta= JSON string{"type": "text"}; URL =<base>/note/<id>#<key.hex()> copy_crl()doeschmod 644after copy- Password: pos 1=uppercase, pos 2=lowercase (no j), pos 3-27=alphanumeric, pos 28=lowercase (no j);
oO01lIQ5S2Z8Bbanned everywhere - Inline file path:
<PKI_DIR>/inline/private/<CN>.inline - User emails are not stored separately:
build_client_full()setsEASYRSA_REQ_EMAILwhenever an email is known, which EasyRSA embeds asemailAddress=in the cert subject — so it round-trips through<PKI_DIR>/index.txtitself.get_email()reads it back from there (load_all_certs()+ CN match); there is noopenvpncertupdate-metadata.json