Skip to content

v0.5.0

Latest

Choose a tag to compare

@github-actions github-actions released this 21 Oct 02:15

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.conf and
    /etc/ssh/authkeys.conf are no longer read. Name another file with
    --config FILE or the new AUTHKEYS_CONFIG variable (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 as backend. Other distributions add names
    through the authkeys.sources and authkeys.caches entry-point groups.

  • --config/-c and the logging options go before the command:
    authkeys -c FILE resolve %u or authkeys -c FILE %u, not
    authkeys resolve -c FILE %u. --config takes one file, and a file named
    there must exist (status 2). authkeys with no argument, and an option after
    the user (authkeys alice -c FILE), are usage errors; authkeys resolve
    resolves the current user.

  • serve listens where --listen says ([serve] listen), a netimps listen
    specification such as 127.0.0.1:8090, [::1]:8090, :8090 or several
    comma-separated: IPv6 works, and each entry is its own socket. --bind and
    --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.

  • serve can speak HTTPS: --certificate and --key ([serve] certificate, key), through sslcontext. Without TLS on a non-loopback
    address it logs a warning.

  • serve also takes --path, --max-usernames, --request-timeout and
    --max-connections, and each option can come from AUTHKEYS_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 (was bind=/port=); bind_servers() builds one
    per listen entry and serve() takes one server or a list. netimps
    (>=0.4.1, the first with parse_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 sets proxy (a URL or "direct"); a reply is limited to 8 MiB; a
    deadline (default 15 s) bounds the whole request and retries defaults to 0.
    The http source's address is now url, credentials is now certificate
    and key, and both sources take token_env for 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 makes ldap:// on port
    389 work against an Active Directory that requires signing; simple
    (bind_dn, password_env), external (client certificate) and anonymous
    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 say auth = "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_key no longer default to the host's server
    certificate and key, and the files need not exist unless used; starttls
    enables StartTLS on ldap://; issuer_ca keeps only certificates its CA
    issued; the key comment no longer contains spaces. With ldap3 the TLS context
    is an SSLContext built with sslcontext (host-name checking on), or the one
    given to LDAPSource(..., ssl_context=...).

  • The ldap extra requires ldap3>=2.10.2rc4,<2.11 (signed and sealed SASL)
    and cryptography>=40.0.0,<51; the new kerberos extra adds gssapi (POSIX)
    or winkerberos (Windows) for Kerberos through ldap3.

  • completion needs the shell (bash, zsh, fish, or the new powershell).

  • cache gains purge --dry-run (-n); an unknown action is a usage error.

  • The console script calls authkeys.cli:main (was authkeys.cli:run).

  • The duho requirement 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 authkeys entry (authkeys.cli.main, the console script) goes through
    duho.run(..., exit=False) and always returns an int. Exit statuses and
    output are unchanged, with three differences: Ctrl-C outside resolve,
    check and serve (which handle it themselves) ends with status 130 and no
    traceback; a broken pipe there ends with status 1, quietly; and serve's
    refusal to start without a usable api_key is returned as status 1 with the
    same message on stderr instead of raised as SystemExit.

  • authkeys cache reports "No cache configured", an unsupported backend action
    and an unknown action as CommandErrors (statuses 3, 3 and 2). The log record
    is the same: logger authkeys, level ERROR, the same message. With
    DUHO_TRACEBACK set 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 an options class attribute
    authkeys.sources.authorizedkeys / AuthorizedKeysFiles authkeys.sources.FileSource (file)
    authkeys.sources.http / HttpAuthorizedKeys authkeys.sources.HTTPSource (http)
    authkeys.sources.github / GithubKeys authkeys.sources.GitHubSource (github)
    authkeys.sources.ldap / LdapAuthorizedKeys authkeys.sources.LDAPSource (ldap)
    authkeys.sources.ldap.only_auth_keys authkeys.sources.only_auth_keys
    authkeys.cache.AuthKeysCacheBackend / AuthKeysCacheMemBackend / AuthKeysCacheFileBackend authkeys.cache.CacheBackend / MemoryCache (memory) / FileCache (file); from_config(options: Mapping)
    AuthKeys.load_config(AuthkeysConfig) AuthKeys.load_config(Settings), with authkeys.load_settings(path)
    AuthKeys.load_user_config(username, parser) AuthKeys.load_user_config(username, Delegation)
    authkeys.cli.Authkeys, authkeys.cli.run authkeys.cli.AuthKeysCLI, authkeys.cli.main

    Errors raised on purpose derive from authkeys.exceptions.AuthKeysError:
    ConfigError (was ValueError/FileNotFoundError from the sources and the
    file cache) and SourceError (was RuntimeError from the ldap source).

Removed

  • authkeys.config.AuthkeysConfig and its getlist, SYSTEM_CONF_PATHS,
    USER_CONF_PATH, AuthkeysConfig.CONF_PATHS, the colon-separated --config
    list, Source.from_config, authkeys.utils.parse_bool, try_import and
    call, and the sources attribute 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
    from serve or from a user's [authorized] users can 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.cfg does not expand ${env.NAME}. It is
    written by the user it delegates for, so it could read the resolver's
    environment (under serve, 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.cfg are 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.
  • serve no 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.
  • serve closes a connection that sends nothing for [serve] request_timeout
    seconds (default 10) and serves at most [serve] max_connections requests at
    once (default 64); a connection over the limit gets 503 and is closed.
  • A refused LDAP search is an error, not "no keys". ldap3 returns the same
    False for "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 and expired_on_error instead 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 override use_ssl, so ldap:// silently sent the search unencrypted
    and ignored tls_verify. ldap:// is now accepted only with auth = "gssapi"
    (a sealed session) or starttls = true; otherwise use ldaps:// or a bare
    host name.
  • The ldap source applies a certificate filter by default. Before, no filter
    ran unless cert_filter was set, and every certificate in the attribute
    became a key, encryption and recovery certificates included. The default is
    now authkeys.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 = none keeps 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 raised KeyError for 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.cfg is removed or no longer parses, a long-running serve
    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 raised InterpolationSyntaxError:
    serve exited 1 with a traceback and resolve exited 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): with expired_on_error, an entry is served
    after a source error only while it is at most max_stale seconds past its
    TTL. Unset (the default) keeps today's unbounded fallback.
  • serve accepts the API key as Authorization: Bearer <key> as well as the
    apikey query parameter, so a client can keep it out of URLs.
  • [serve] request_timeout and [serve] max_connections (see Security).

Documentation

  • The README and docs recommend a dedicated AuthorizedKeysCommandUser instead
    of nobody: 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
    account X trusts the forge account X, which for root, admin or git
    belongs to someone else.
  • The shipped API header documents completion with its positional shell
    argument, and when resolve is inserted before a --.

Full Changelog: v0.4.5...v0.5.0