Skip to content

Releases: jose-pr/authkeys

v0.5.0

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 authk...
Read more

v0.4.5

Choose a tag to compare

@github-actions github-actions released this 02 Oct 19:24

Enables duho's MCP launch trigger, which 0.4.4 had turned off. No other change.

Changed

  • MCP is enabled. With AUTHKEYS_MCP=stdio set, authkeys now 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 real authorized_keys content, and
    serve starts 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

Choose a tag to compare

@github-actions github-actions released this 28 Sep 11:46

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 duho requirement 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_completion and
    duho.logging — is unchanged by 0.6.0. The full test suite passes against
    the installed duho==0.6.0 wheel on Python 3.9 and 3.14.

Added

  • Opted the whole CLI out of duho's MCP auto-launch. duho 0.6.0 makes
    every app()/main() call check AUTHKEYS_MCP=stdio and, 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: authkeys never listens for AUTHKEYS_MCP and always runs as
    a normal CLI. authkeys is a credentials tool — an MCP-exposed resolve/
    check would hand a caller a user's real authorized_keys content, and
    MCP-exposed serve would 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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 23:25

Maintenance release — a dependency-range change only, with no change to
authkeys' own behavior or API.

Changed

  • The duho requirement 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_completion and
    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 a getclsdef fix 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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 17:27

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 stays 0
    and stdout stays empty — sshd's AuthorizedKeysCommand contract does not
    allow failing a login over this — but a typo'd --config is no longer
    indistinguishable from a user who genuinely has no keys. A bytes/dict
    config never triggers it.
  • The exit-code convention is now documented in the CLI guide: 0 on success
    including "no keys", 3 on a config or internal error (cache also uses
    2 for 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 bob was split at the
    wrong offsets — type, key, comment and options all 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_keys output, 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 by default_sanitize.

Full Changelog: v0.4.1...v0.4.2

v0.4.1

Choose a tag to compare

@jose-pr jose-pr released this 28 Jul 05:35

Maintenance release.

Changed

  • Pinned duho dependency to >=0.5.1,<0.6.0 to ensure stability against future minor updates.
  • Migrated the CLI to duho's updated Cli/Cmd model, using app() as the multi-command runner.
  • Polished agent documentation in AGENTS.md to reflect the latest duho API usage.

Full Changelog: v0.4.0...v0.4.1

v0.4.0

Choose a tag to compare

@jose-pr jose-pr released this 28 Jul 05:35

Feature release with fixes — no breaking changes.

Fixed

  • --config with a Windows absolute path was silently ignored. The
    colon-separated path list was split naively, so C:\path\authkeys.conf became
    ["C", "\path\authkeys.conf"] and the file was never read — the caller got an
    empty config with no error. For authkeys serve this also skipped the
    fail-closed api_key check and left the server running unauthenticated.
    Drive-letter prefixes are now preserved; POSIX a.conf:b.conf lists 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 (via pytest-timeout) is configured so any future
      socket/thread stall fails fast with a traceback instead of hanging the job.

Added

  • Options-prefixed authorized_keys lines are now preserved verbatim. A
    real-world line like command="...",no-pty ssh-rsa AAAA bob was previously
    misparsed (the options list was read as the key type). AuthorizedKey gains
    an options: str = "" field; parse now detects an options prefix (a first
    token containing =, or not matching a recognized key-type prefix) and
    keeps it separate from type/key/comment, round-tripping byte-for-byte
    through resolve/serve output. 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|json on authkeys resolve and
    authkeys check. authorized_keys (default) is unchanged; json prints a
    single JSON array of {"type", "key", "comment", "options"} objects instead,
    for scripting/tooling consumers. serve is 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_APIKEY without putting it in authkeys.conf. Documented in
    docs/guide/server.md, including why ProtectHome= is read-only rather
    than stricter (the service needs to read users' ~/.ssh).

Changed

  • Internal refactor: Source is now a @dataclass instead of an
    argparse.Namespace subclass. Same fields (cached/enabled/backend/
    sanitize/expire), same from_config signature, no behavior change.

Full Changelog: https://github.com/jose-pr/authkeys/commits/v0.4.0