Repository navigation
Releases: jose-pr/authkeys
Release list
v0.5.0
This release changes the configuration file, the command line and the Python
API. A configuration written for 0.4 does not load: convert it as below.
Changed
-
The configuration is one TOML file,
/etc/authkeys.cfg, read with anycfg
(a new dependency)./etc/authkeys.conf,/etc/authkeys/authkeys.confand
/etc/ssh/authkeys.confare no longer read. Name another file with
--config FILEor the newAUTHKEYS_CONFIGvariable (empty: no file).
A key authkeys does not know, or a value of the wrong type, is now a WARNING
naming the key; a file that does not parse is an error (status 3). -
The per-user delegation file is
~/.ssh/authkeys.cfg, TOML, with no
includes and no${env.}expansion. -
Sources and cache backends are chosen by name (
backend = "file",
"http","github","ldap";[cache] backend = "memory"or"file").
Dotted import paths no longer work asbackend. Other distributions add names
through theauthkeys.sourcesandauthkeys.cachesentry-point groups. -
--config/-cand the logging options go before the command:
authkeys -c FILE resolve %uorauthkeys -c FILE %u, not
authkeys resolve -c FILE %u.--configtakes one file, and a file named
there must exist (status 2).authkeyswith no argument, and an option after
the user (authkeys alice -c FILE), are usage errors;authkeys resolve
resolves the current user. -
servelistens where--listensays ([serve] listen), a netimps listen
specification such as127.0.0.1:8090,[::1]:8090,:8090or several
comma-separated: IPv6 works, and each entry is its own socket.--bindand
--port([serve] bind,port) are gone;listen = "0.0.0.0:8090"replaces
them. A bad specification, a port out of range or a port in use is one line on
stderr with status 3 instead of a traceback. -
servecan speak HTTPS:--certificateand--key([serve] certificate,key), through sslcontext. Without TLS on a non-loopback
address it logs a warning. -
servealso takes--path,--max-usernames,--request-timeoutand
--max-connections, and each option can come fromAUTHKEYS_SERVE_<OPTION>
(command line > variable >[serve]> default). A malformed value is a
one-line usage error (status 2) instead of a traceback. -
authkeys.server.KeyServer(authkeys, listen, *, ..., ssl_context=None)binds
one socket through netimps (wasbind=/port=);bind_servers()builds one
per listen entry andserve()takes one server or a list. netimps
(>=0.4.1, the first withparse_listen) and sslcontext are new
dependencies. -
The http and github sources use syshttp instead of requests (the
http
extra now installs syshttp). The OS trust store verifies servers (truststore
on Python 3.10+, instead of certifi); the system's proxy configuration (proxy
variables, else WinHTTP/macOS/libproxy, PAC, WPAD) routes requests unless the
source setsproxy(a URL or"direct"); a reply is limited to 8 MiB; a
deadline(default 15 s) bounds the whole request andretriesdefaults to 0.
The http source'saddressis nowurl,credentialsis nowcertificate
andkey, and both sources taketoken_envfor a bearer token. With a client
certificate or a token, a redirect is an error instead of being followed (it
would carry the credential to another host). Error messages name the URL
without its query string. -
The ldap source is rewritten. It uses python-ldap when installed (as many
distributions and appliances ship it) and ldap3 otherwise (client), and it
authenticates:auth = "gssapi"(the default) binds with the host's machine
Kerberos credentials, sealing the session, which also makesldap://on port
389 work against an Active Directory that requires signing;simple
(bind_dn,password_env),external(client certificate) andanonymous
are the others. Before, every bind was anonymous, so a directory that refuses
anonymous reads (Active Directory) returned nothing. A configuration that
relied on the anonymous bind must now sayauth = "anonymous". -
The ldap source reads the certificate attribute under any options and case
(userCertificate;binary, as OpenLDAP returns it); before, OpenLDAP entries
gave no keys.tls_cert/tls_keyno longer default to the host's server
certificate and key, and the files need not exist unless used;starttls
enables StartTLS onldap://;issuer_cakeeps only certificates its CA
issued; the key comment no longer contains spaces. With ldap3 the TLS context
is anSSLContextbuilt with sslcontext (host-name checking on), or the one
given toLDAPSource(..., ssl_context=...). -
The
ldapextra requiresldap3>=2.10.2rc4,<2.11(signed and sealed SASL)
andcryptography>=40.0.0,<51; the newkerberosextra adds gssapi (POSIX)
or winkerberos (Windows) for Kerberos through ldap3. -
completionneeds the shell (bash,zsh,fish, or the newpowershell). -
cachegainspurge --dry-run(-n); an unknown action is a usage error. -
The console script calls
authkeys.cli:main(wasauthkeys.cli:run). -
The
duhorequirement moves to its 0.7.x series:>=0.7.4,<0.8.0(was
>=0.6.0,<0.7.0). The floor is 0.7.4 because the entry now uses
duho.run(..., exit=False)and the root's_report_error_, both added in
that patch. -
The
authkeysentry (authkeys.cli.main, the console script) goes through
duho.run(..., exit=False)and always returns anint. Exit statuses and
output are unchanged, with three differences: Ctrl-C outsideresolve,
checkandserve(which handle it themselves) ends with status 130 and no
traceback; a broken pipe there ends with status 1, quietly; andserve's
refusal to start without a usableapi_keyis returned as status 1 with the
same message on stderr instead of raised asSystemExit. -
authkeys cachereports "No cache configured", an unsupported backend action
and an unknown action asCommandErrors (statuses 3, 3 and 2). The log record
is the same: loggerauthkeys, levelERROR, the same message. With
DUHO_TRACEBACKset the error is now raised instead of logged. -
Python API, renamed with no aliases kept:
0.4 0.5 authkeys.AuthkeysSource, constructor(conf: SectionProxy, globals)authkeys.KeySource, constructor(options: Mapping, globals)and anoptionsclass attributeauthkeys.sources.authorizedkeys/AuthorizedKeysFilesauthkeys.sources.FileSource(file)authkeys.sources.http/HttpAuthorizedKeysauthkeys.sources.HTTPSource(http)authkeys.sources.github/GithubKeysauthkeys.sources.GitHubSource(github)authkeys.sources.ldap/LdapAuthorizedKeysauthkeys.sources.LDAPSource(ldap)authkeys.sources.ldap.only_auth_keysauthkeys.sources.only_auth_keysauthkeys.cache.AuthKeysCacheBackend/AuthKeysCacheMemBackend/AuthKeysCacheFileBackendauthkeys.cache.CacheBackend/MemoryCache(memory) /FileCache(file);from_config(options: Mapping)AuthKeys.load_config(AuthkeysConfig)AuthKeys.load_config(Settings), withauthkeys.load_settings(path)AuthKeys.load_user_config(username, parser)AuthKeys.load_user_config(username, Delegation)authkeys.cli.Authkeys,authkeys.cli.runauthkeys.cli.AuthKeysCLI,authkeys.cli.mainErrors raised on purpose derive from
authkeys.exceptions.AuthKeysError:
ConfigError(wasValueError/FileNotFoundErrorfrom the sources and the
file cache) andSourceError(wasRuntimeErrorfrom the ldap source).
Removed
authkeys.config.AuthkeysConfigand itsgetlist,SYSTEM_CONF_PATHS,
USER_CONF_PATH,AuthkeysConfig.CONF_PATHS, the colon-separated--config
list,Source.from_config,authkeys.utils.parse_bool,try_importand
call, and thesourcesattribute aliases.
Migrating a configuration
0.4 (/etc/authkeys.conf, INI) |
0.5 (/etc/authkeys.cfg, TOML) |
|---|---|
[source:files] |
[source.files] |
backend = authkeys.sources.authorizedkeys |
backend = "file" ("http", "github", "ldap") |
[cache] backend = authkeys.cache.AuthKeysCacheFileBackend |
[cache] backend = "file" (or "memory") |
paths = + one value per line |
paths = ["authorized_keys", "authorized_keys2"] |
enabled = 0, expired_on_error = 1 |
enabled = false, expired_on_error = true |
expire = 3600 |
expire = 3600 (a number, unquoted) |
api_key = ${env:AUTHKEYS_APIKEY} |
api_key = "${env.AUTHKEYS_APIKEY}" |
[serve] bind = 0.0.0.0 and port = 8090 |
[serve] listen = "0.0.0.0:8090" |
[source:ldap] with no credentials (anonymous) |
[source.ldap] with auth = "anonymous" (the default is now "gssapi") |
authkeys serve --bind H --port P |
authkeys serve --listen H:P |
[source:http] address = https://... |
[source.http] url = "https://..." |
credentials = cert.pem,key.pem |
certificate = "cert.pem" and key = "key.pem" |
verify = false |
verify = false (a boolean) or verify = "/path/ca.pem" |
~/.ssh/authkeys.conf: users = + lines |
~/.ssh/authkeys.cfg: [authorized] users = ["alice", "bob"] |
Strings are quoted. max_age and max_entries, shown in the 0.4 example's
[cache], were never read: drop them (0.5 warns about unknown keys).
Security
- The file cache no longer reads or writes outside its directory. A username
or source name that is not one plain path component (empty,.,.., or
holding/,\,:or NUL) is refused, so a username such as../../etc/x
fromserveor from a user's[authorized] userscan no longer read another
file as keys or overwrite it with a cache entry. - On POSIX the file cache ignores its root, a source directory or an entry that is
not owned by the user running authk...
v0.4.5
Enables duho's MCP launch trigger, which 0.4.4 had turned off. No other change.
Changed
- MCP is enabled. With
AUTHKEYS_MCP=stdioset,authkeysnow serves
its commands (resolve,check,serve,cache,completion) as MCP
tools over stdio, which is duho's default launch trigger. 0.4.4 opted the
whole CLI out. These tools return realauthorized_keyscontent, and
servestarts a listener, so only set the variable for an MCP client you
trust. Without the variable, the CLI behaves exactly as before.
Full Changelog: v0.4.4...v0.4.5
v0.4.4
Maintenance release: follows duho's 0.6.x ceiling bump and opts the CLI out
of duho's new MCP auto-launch by default — no other change to authkeys' own
behavior or public API.
Changed
- The
duhorequirement moves to its 0.6.x series:>=0.6.0,<0.7.0(was
>=0.5.0,<0.6.0). Ceiling-following bump, not a floor raise for a feature
authkeys uses: the entire duho surface it imports —AUTO,Arg,Args,
Choice,Cli,Cmd,LoggingArgs,app,main,print_completionand
duho.logging— is unchanged by 0.6.0. The full test suite passes against
the installedduho==0.6.0wheel on Python 3.9 and 3.14.
Added
- Opted the whole CLI out of duho's MCP auto-launch. duho 0.6.0 makes
everyapp()/main()call checkAUTHKEYS_MCP=stdioand, when set,
serve MCP tools instead of running the requested command.Authkeys(the
CLI root) now declares_mcp_ = False, disabling that env trigger for the
whole app:authkeysnever listens forAUTHKEYS_MCPand always runs as
a normal CLI. authkeys is a credentials tool — an MCP-exposedresolve/
checkwould hand a caller a user's realauthorized_keyscontent, and
MCP-exposedservewould start a listener as the side effect of a single
tool call — neither is a safe default here.
Full Changelog: v0.4.3...v0.4.4
v0.4.3
Maintenance release — a dependency-range change only, with no change to
authkeys' own behavior or API.
Changed
- The
duhorequirement is now scoped to its minor series:
>=0.5.0,<0.6.0(was>=0.5.1,<0.6.0). The ceiling is unchanged and the
floor moves down to the start of the series, because nothing authkeys uses
postdates it: the entire duho surface it imports —AUTO,Arg,Args,
Choice,Cli,Cmd,LoggingArgs,app,main,print_completionand
duho.logging— is present in 0.5.0. The old 0.5.1 floor recorded which duho
happened to be current when the pin was written rather than anything authkeys
requires; 0.5.1's only change was agetclsdeffix for modules whose
__file__is not on disk (a zipapp), which a normally installed authkeys
never reaches. The full test suite passes against both ends of the range,
duho 0.5.0 and 0.5.4, on Python 3.9 and 3.14.
Full Changelog: v0.4.2...v0.4.3
v0.4.2
Bug-fix release with small additions — no breaking changes.
Added
- A warning when no config file is found. If
--config(or the system
search path) names only paths that do not exist,AuthkeysConfig.from_config
now logs one warning listing every path it searched. The exit code stays0
and stdout stays empty — sshd'sAuthorizedKeysCommandcontract does not
allow failing a login over this — but a typo'd--configis no longer
indistinguishable from a user who genuinely has no keys. Abytes/dict
config never triggers it. - The exit-code convention is now documented in the CLI guide:
0on success
including "no keys",3on a config or internal error (cachealso uses
2for an unknown action).
Fixed
-
Options containing quoted spaces were misparsed.
AuthorizedKey.parse
ended the options token at the first space, so an sshd-legal line such as
command="/usr/bin/tunnel -n 5",no-pty ssh-ed25519 AAAA bobwas split at the
wrong offsets —type,key,commentandoptionsall held fragments of
the wrong fields. The options token is now scanned quote-aware (a space ends
it only outside double quotes;\"escapes a quote inside a quoted value).
A line with an unterminated quote is now rejected as malformed instead of
yielding shifted fields.This was invisible in the default
authorized_keysoutput, which rejoins the
fields with single spaces and so round-tripped anyway; it corrupted
--format json, the(options, type, key)dedup identity used across
sources, and the comment injected bydefault_sanitize.
Full Changelog: v0.4.1...v0.4.2
v0.4.1
Maintenance release.
Changed
- Pinned
duhodependency to>=0.5.1,<0.6.0to ensure stability against future minor updates. - Migrated the CLI to
duho's updatedCli/Cmdmodel, usingapp()as the multi-command runner. - Polished agent documentation in
AGENTS.mdto reflect the latestduhoAPI usage.
Full Changelog: v0.4.0...v0.4.1
v0.4.0
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 setHTTP(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.
- The HTTP key-server tests now bypass any proxy configured in the environment
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 resolveand
authkeys 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 running
authkeys serve, with hardening directives (DynamicUser=,
NoNewPrivileges=true,ProtectSystem=strict,PrivateTmp=true, a minimal
RestrictAddressFamilies=, dropped capabilities) and both ways to supply
AUTHKEYS_APIKEYwithout putting it inauthkeys.conf. Documented in
docs/guide/server.md, including whyProtectHome=isread-onlyrather
than stricter (the service needs to read users'~/.ssh).
Changed
- Internal refactor:
Sourceis now a@dataclassinstead of an
argparse.Namespacesubclass. Same fields (cached/enabled/backend/
sanitize/expire), samefrom_configsignature, no behavior change.
Full Changelog: https://github.com/jose-pr/authkeys/commits/v0.4.0