Changelog
Changelog
All notable changes to this project are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[0.4.0] - 2026-07-20
Feature release with fixes — no breaking changes.
Fixed
--configwith a Windows absolute path was silently ignored. The colon-separated path list was split naively, soC:\path\authkeys.confbecame["C", "\path\authkeys.conf"]and the file was never read — the caller got an empty config with no error. Forauthkeys servethis also skipped the fail-closedapi_keycheck and left the server running unauthenticated. Drive-letter prefixes are now preserved; POSIXa.conf:b.conflists still split.- CI reliability (test-only, no runtime code changed):
- The HTTP key-server tests now bypass any proxy configured in the environment
(GitHub's Windows runners set
HTTP(S)_PROXY), which otherwise routed the localhost test requests through an unreachable proxy and hung. - A concurrency test joined its worker threads without a timeout; the join is now bounded and the threads are daemons.
- A hard per-test
timeout(viapytest-timeout) is configured so any future socket/thread stall fails fast with a traceback instead of hanging the job.
Added
- Options-prefixed
authorized_keyslines are now preserved verbatim. A real-world line likecommand="...",no-pty ssh-rsa AAAA bobwas previously misparsed (the options list was read as the key type).AuthorizedKeygains anoptions: str = ""field;parsenow detects an options prefix (a first token containing=, or not matching a recognized key-type prefix) and keeps it separate fromtype/key/comment, round-tripping byte-for-byte throughresolve/serveoutput. Deduplication now keys on(options, type, key), so an option-bearing key is kept distinct from the bare form of the same key. --format authorized_keys|jsononauthkeys resolveandauthkeys check.authorized_keys(default) is unchanged;jsonprints a single JSON array of{"type", "key", "comment", "options"}objects instead, for scripting/tooling consumers.serveis unaffected (always sshd wire format).examples/authkeys-serve.service: a template systemd unit for runningauthkeys serve, with hardening directives (DynamicUser=,NoNewPrivileges=true,ProtectSystem=strict,PrivateTmp=true, a minimalRestrictAddressFamilies=, dropped capabilities) and both ways to supplyAUTHKEYS_APIKEYwithout putting it inauthkeys.conf. Documented indocs/guide/server.md, including whyProtectHome=isread-onlyrather than stricter (the service needs to read users'~/.ssh).
Changed
- Internal refactor:
Sourceis now a@dataclassinstead of anargparse.Namespacesubclass. Same fields (cached/enabled/backend/sanitize/expire), samefrom_configsignature, no behavior change.
[0.3.0] - 2026-07-19
Additive feature release — no breaking changes.
Added
authkeys.sources.github: fetch a user's public keys from GitHub's plain-text.keysendpoint (https://github.com/{username}.keysby default). Theurltemplate can be overridden to point at any forge with the same convention (e.g. GitLab), so no separate source class is needed. Uses the existing[http]extra (requests); no new extra.authkeys check <user>: a debugging counterpart toresolvethat prints the same resolved keys to stdout while surfacing the per-source resolution trace (which source served/cached which key) on stderr.authkeys completion [bash|zsh|fish]: prints a self-contained shell completion script (generated from the CLI's own argument parser viaduho's completion support) for the given shell.- Source registry:
authkeys.sources.register_source(alias, cls)andget_source(alias)for programmatic lookup of source classes by short name. The four built-in sources (authorizedkeys,http,github,ldap) are registered through it; existingbackend = authkeys.sources.<alias>config resolution is unchanged.
Changed
KeyServer.__init__gained arequire_auth: bool = Falseparameter: whenTrueandapi_keyis falsy, construction raisesValueErrorinstead of silently starting with authentication disabled.authkeys servenow passesrequire_auth=Truewhenever[serve]configures anapi_key, in addition to its existing friendlySystemExitmessage — the user-facing CLI behavior is unchanged, but the fail-closed invariant now also holds for directKeyServerconstruction (e.g. embedding authkeys as a library).
[0.2.0] - 2026-07-19
A security and robustness release from a second review pass. One breaking change in the LDAP source (see below).
Security
- LDAP now verifies the server certificate by default (breaking). Previously
the LDAPS connection trusted any peer certificate. A new
tls_verifyoption controls it: unset/systemverifies against the OS trust store (default), a path verifies against that CA file/directory, andnonedisables verification (insecure; logs a warning). Deployments that relied on no verification must settls_verify = none.tls_cert/tls_keyare now documented as the client mutual-TLS credentials, not server verification. - The HTTP key server no longer 500s (with a traceback) on a non-ASCII
apikey; it returns a clean 401. authkeys servecaps the number of?username=parameters per request (max_usernames, default 16), bounding the work an unauthenticated request can force.
Fixed
- A single malformed line in a source (e.g. a corrupt
authorized_keys) is now skipped and logged instead of discarding all of that user's keys — which, withexpired_on_error, could previously serve stale/revoked keys. - The HTTP source now raises on a non-2xx status, so a transient 5xx/403 routes
through
expired_on_error(stale fallback) instead of being cached as "this user has no keys". Errored fetches are never cached as empty. - The resolve path never emits a traceback: on a config/internal error it logs one
line to stderr and exits non-zero (exit 3), keeping
auth.logclean. - Bounded LDAP connection/receive timeouts, and the resolver no longer holds its
lock across the upstream fetch, so one slow/hung source can't stall other
logins (or, in
serve, other concurrent requests). - Emitted keys are deduplicated by
(type, key), so the same public key coming from two sources is no longer emitted twice with different comments.
Added
- Negative caching with its own
negative_expireTTL for keyless principals. - Per-source
expireoverriding[cache] expire(0= don't cache a source). - Atomic file-cache writes (temp file + rename) with
0600/0700perms. authkeys cachesubcommand:show,purge(--user/--source/--expired/--all), andwarm <users...>; plus optionalmax_age/max_entrieseviction bounds for the file backend.
Changed
authkeys.__version__is now read from the installed distribution metadata (single source of truth withpyproject.toml).
[0.1.1] - 2026-07-18
Hardening release from a code-review pass. No API breaks.
Security
- LDAP source now escapes the username in the search filter
(
escape_filter_chars), preventing LDAP filter injection when the username is attacker-controlled (e.g. via the HTTP key server). - HTTP source percent-encodes the username before templating it into the request URL, preventing path/query tampering of the upstream key service.
authkeys servenow fails closed: if[serve] api_keyis present but resolves empty (e.g. an unset${env:...}), it refuses to start instead of silently disabling authentication. A truly absentapi_keystill runs unauthenticated with a warning.
Fixed
- HTTP source
verifynow interprets bool-like values (true/false/...) as a boolean; other values remain a CA-bundle path. Previouslyverify = falsewas passed to requests as the string"false"(a CA path) and did not disable verification. - The key server (
ThreadingHTTPServer) now resolves under a lock, so concurrent requests no longer race on the shared cache backend or issue duplicate upstream fetches for the same key. - A malformed cache
expirevalue falls back to the default TTL instead of disabling caching entirely (and no longer masks genuine backend errors).
Changed
authkeys servenow honors each user's~/.ssh/authkeys.confuser/group delegation, matchingauthkeys resolve(both go through the newAuthKeys.resolve()).authkeys <user>default-command insertion is more robust: a leading flag before a bare username (authkeys -v alice) is routed to theresolvesubparser instead of erroring.
[0.1.0] - 2026-07-18
Initial packaged release: a src/ layout, a duho-based CLI, and a published
authkeys console script.
Added
- Declarative CLI (
authkeys resolve/authkeys serve) built onduho, with--verbose/--quiet/--logleveland--version.resolveis the default command soauthkeys %uworks directly as anAuthorizedKeysCommand. - Config-driven HTTP key server (
authkeys serve): bind/port/path/API key from a[serve]section, constant-time API-key comparison, threaded server with clean shutdown, and${env:VAR}interpolation to keep secrets off disk. file,http, andldapkey sources with lazy imports of their optional dependencies (requests,ldap3,cryptography);httpgains request timeouts and configurable TLS verification.- Packaging:
pyproject.tomlwithhttp/ldap/all/devextras,py.typed, MIT license, and PyPI classifiers. Supports Python 3.9–3.14.
Fixed
- In-memory and file cache backends now store and retrieve entries by
(uid, source)and carry a timestamp, so cache lookups actually hit and the TTL is honored (the previous mem backend never returned a cached value). Source.from_configis a realclassmethod; source defaults (e.g. the default sanitizer) are applied when a section omits them.- Group resolution failures during user-config loading are logged and skipped instead of aborting key resolution.
- Blank lines in
authorized_keysfiles are no longer emitted as empty keys.
Changed
- Replaced the shell
bin/authkeyswrapper and standalonekeygrabberscript with the unifiedauthkeysCLI andauthkeys servesubcommand.