Repository navigation
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 authkeys or that group or others can write, and
logs one WARNING naming the path. Make the cache directory owned by, and
writable only by, the account that runs authkeys. - A user's
~/.ssh/authkeys.cfgdoes not expand${env.NAME}. It is
written by the user it delegates for, so it could read the resolver's
environment (underserve, the API key) into a username that was logged and
sent to every source.${env.NAME}still works in/etc/authkeys.cfg. ~/.ssh/authorized_keys*(file source) and~/.ssh/authkeys.cfgare read
under sshd's StrictModes rules on POSIX: the file must be a regular file, not a
symbolic link, of at most 1 MiB, and it and every directory up to the home
must be owned by the user or root and not writable by group or others. A file
that fails is skipped with one WARNING naming the path and the reason.serveno longer logs the API key. Each request is logged as the method,
the path without its query string, and the status. Before, the whole request
line,apikey=included, was logged at INFO, wrong guesses too.servecloses a connection that sends nothing for[serve] request_timeout
seconds (default 10) and serves at most[serve] max_connectionsrequests at
once (default 64); a connection over the limit gets503and is closed.- A refused LDAP search is an error, not "no keys". ldap3 returns the same
Falsefor "no match" and for a search the server refused (a missing bind,
an expired password, missing rights); the source now checks the result code,
so a refusal goes through the error path andexpired_on_errorinstead of
being cached over the user's last good keys as an empty result. - The ldap source no longer searches in clear text. ldap3 let the URL's
scheme overrideuse_ssl, soldap://silently sent the search unencrypted
and ignoredtls_verify.ldap://is now accepted only withauth = "gssapi"
(a sealed session) orstarttls = true; otherwise useldaps://or a bare
host name. - The ldap source applies a certificate filter by default. Before, no filter
ran unlesscert_filterwas set, and every certificate in the attribute
became a key, encryption and recovery certificates included. The default is
nowauthkeys.sources.only_auth_keys, which keeps a certificate only when
it is within its validity period, is not self-signed, its KeyUsage (if any)
allows digital signature and its ExtendedKeyUsage (if any) includes clientAuth
or anyExtendedKeyUsage.cert_filter = nonekeeps every certificate.
Fixed
- A cache entry that cannot be read or parsed is treated as a miss and logged,
instead of aborting the whole resolution with exit status 3. - A failed cache write (a full or read-only disk) is a WARNING; the keys the
source returned are still printed. Before, they were discarded and the
resolver fell back to expired keys or to none. - The ldap source reads only directory entries from a search answer: an Active
Directory search returns referrals beside the entries, and a referral first in
the list raisedKeyErrorfor every unknown user. A search matching more than
one entry is now an error rather than the first entry's keys. - No "No config file found" WARNING is logged for a user without
~/.ssh/authkeys.cfg; that file is optional. - A delegation that stops existing stops working: when a user's
~/.ssh/authkeys.cfgis removed or no longer parses, a long-runningserve
no longer keeps the users and groups it granted before. - A broken delegation file is logged by error type and line number; the file's
text is no longer copied into the log. %in a configuration value (an API key, a password, an${env.NAME}
expansion) is read as written. Before, it raisedInterpolationSyntaxError:
serveexited 1 with a traceback andresolveexited 3.- A key line whose type is not an OpenSSH key type (
ssh-*,ecdsa-*,sk-*,
after any options) is malformed and skipped with a WARNING, instead of being
returned as a key. The WARNING gives the reason, not the line.
Added
[cache] max_stale(seconds): withexpired_on_error, an entry is served
after a source error only while it is at mostmax_staleseconds past its
TTL. Unset (the default) keeps today's unbounded fallback.serveaccepts the API key asAuthorization: Bearer <key>as well as the
apikeyquery parameter, so a client can keep it out of URLs.[serve] request_timeoutand[serve] max_connections(see Security).
Documentation
- The README and docs recommend a dedicated
AuthorizedKeysCommandUserinstead
ofnobody: with the on-disk cache, whatever runs as that account can write
cache entries, and so keys. - The github source's docs warn that with the default identity usermap local
accountXtrusts the forge accountX, which forroot,adminorgit
belongs to someone else. - The shipped API header documents
completionwith its positional shell
argument, and whenresolveis inserted before a--.
Full Changelog: v0.4.5...v0.5.0