Installing the server
Getting one ssoosshd process running on a Linux host. Everything here is
the same whether you end up running one instance or twenty; the split-process
and multi-instance work comes later.
Packages
Section titled “Packages”The quickest path is the signed repository – sudo dnf install ssoosshd, or
sudo apt install ssoosshd, once the release package is in place. See
Installing from packages for the one-time setup and for
what each package manager verifies.
The server ships as a ssoosshd package in .deb, .rpm and .apk, all
carrying the same statically linked binary, built for linux/amd64 and
linux/arm64. There is no libc to choose: the default build is cgo-free, so
one binary per architecture runs on any distribution.
An ssoosshd-pkcs11 package (.deb and .rpm) exists for operators who
need to load a PKCS#11 module into the server process itself. It is
dynamically linked and needs glibc 2.28 or newer, and it declares
Conflicts/Replaces/Provides against ssoosshd because both install the
same binary path. Most HSM deployments do not need it – an ssh-agent
holding the token reaches the same hardware from the default package. See
HSM and PKCS#11 and
The CA key in an ssh-agent.
On rpm, both packages recommend ssoosshd-selinux, the SELinux policy that
lets the shipped ssoossh-agent.service start ssh-agent on an enforcing
host. A server that reads its key from config does not need it; see
SELinux on EL hosts.
| What | Where it lands |
|---|---|
| the binary | /usr/sbin/ssoosshd |
| the annotated config sample | /etc/ssoossh/ssoosshd.yaml |
| man pages | ssoosshd(8) and one page per subcommand (ssoosshd-serve(8), ssoosshd-serve-api(8), ssoosshd-serve-full(8), ssoosshd-sign(8), ssoosshd-config(8), ssoosshd-config-check(8), ssoosshd-ldap(8), ssoosshd-ldap-probe(8), ssoosshd-ldap-sync(8), ssoosshd-version(8)), and ssoosshd.yaml(5) |
| reference mail templates | /usr/share/ssoossh/mail-templates/ |
The config sample is installed as a no-replace config file, so an upgrade
never overwrites your edits. It is generated from the server’s config structs,
which makes it the same content as ssoosshd.yaml(5) and the
configuration reference on this site.
A key the server does not recognise, a typo or one from a newer release,
does nothing, so the server logs a config key is not recognised warning at
startup naming the key, the file, and the closest real key when one is close.
ssoosshd config check reports the same
without a start.
There are also container images, ghcr.io/mnestor/ssoossh-server:<version>
(the default, on distroless/static-debian12) and
ghcr.io/mnestor/ssoossh-server:<version>-pkcs11 (on
distroless/cc-debian12, the one that can load a module).
image tag 1.2.4, and points the floating tags 1.2 and 1 at it too.
Which one you pin decides how much a restart is allowed to change: 1.2.4
never moves, 1.2 follows patch releases, 1 follows minor releases as
well. Both floating tags move only for a final release that is the newest in
its series, so a release candidate never becomes what 1.2 means. There is
no latest and there will not be: every tag published here names a piece of
a version, so the one you pin says how far an upgrade may carry you, and
latest is the tag that would say nothing at all. Each build also keeps an
immutable sha-<commit> tag, which is what all three version tags are
promoted from; pinning that works too, it just names a commit rather than a
release. A Compose deployment ships at
deploy/docker-compose.yml.
The CA key
Section titled “The CA key”ssoosshd signs with an SSH CA keypair. Generate one:
ssh-keygen -t ed25519 -f ca -C "ssoossh CA" -N ""That writes ca (private) and ca.pub (public). The private key goes into
ssh_key as inline PEM, not
as a file path, using a YAML literal block:
ssh_key: | -----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----Once it is in the config file, delete the plaintext ca file or move it
somewhere access-controlled.
Exactly one of ssh_key,
ssh_key_file,
ssh_key_agent or
hsm may be set, and a signing process
needs one of them: startup fails with none. To
keep the private key out of the process entirely, source it from a PKCS#11
token (HSM and PKCS#11) or split the signer into
its own process (Startup modes) – or
both.
Ed25519 is a good default for ssh_key. It is the one algorithm the PKCS#11
path does not support, so choose ECDSA or RSA if you intend to move the key to
a token later.
The systemd unit
Section titled “The systemd unit”The package installs the unit at /usr/lib/systemd/system/ssoosshd.service and
creates the ssoossh service account it runs as. It does not enable or start
anything: ssoosshd will not run without a CA key and an identity provider
configured, so a service started at install time would only fail on every fresh
install. Configure it first, then:
install -o ssoossh -g ssoossh -m 0750 -d /var/lib/ssoosshchown -R ssoossh:ssoossh /etc/ssoosshsystemctl enable --now ssoosshdThe shipped unit is deploy/ssoosshd.service. What it does, and why:
| Directive | Why |
|---|---|
User=ssoossh, Group=ssoossh |
a dedicated unprivileged account rather than root or DynamicUser, because the identity has to be stable enough to chown the config and data directories to it ahead of time |
StateDirectory=ssoossh, mode 0750 |
/var/lib/ssoossh, where the default SQLite file belongs |
LogsDirectory=ssoossh, mode 0750 |
/var/log/ssoossh, for any logging.filename you set rather than leaving output to journald |
ReadOnlyPaths=/etc/ssoossh |
the config directory holds the CA private key and is never written by the service |
NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables, ProtectKernelModules, ProtectControlGroups, RestrictSUIDSGID, RestrictRealtime, LockPersonality, MemoryDenyWriteExecute |
the sandbox |
Restart=on-failure, RestartSec=5s |
a config error fails at startup, so a crash loop here is a signal, not a nuisance |
The unit also carries a commented Environment=GODEBUG=fips140=only line. It
is left commented because it would enforce FIPS on every server that installs
the unit, regardless of whether that deployment sets
fips.
Point db.connection_string
at a path inside the state directory so the SQLite file persists across
restarts:
db: connection_string: /var/lib/ssoossh/ssoossh.dbThe minimum config
Section titled “The minimum config”A CA key, a public URL, and an OIDC client. Everything else has a working default.
ssh_key: | -----BEGIN OPENSSH PRIVATE KEY----- -----END OPENSSH PRIVATE KEY-----
http: # The scheme and host browsers actually reach this deployment at. public_url: "https://ssh.example.com"
authentication: client_id: "..." client_secret: "..." provider_url: "https://idp.example.com"http.public_url is required
and is an origin only – a path, query, or fragment is rejected at startup.
Four things derive from it: the OIDC redirect URI, the origin the CSRF
middleware compares against, the host name requests must be addressed to
(anything else gets 421 Misdirected Request), and whether the deployment is
HTTPS at all, which decides the session cookie’s Secure attribute.
More complete starting points, including TLS, PostgreSQL, and multi-instance: Server configuration examples.
Checking the config before a start
Section titled “Checking the config before a start”ssoosshd config check loads the configuration exactly as serve and sign
do (the same file search or --config, defaults.yaml, the ldap.schema
preset, then the file) and runs the same validation, but reports every
problem instead of stopping at the first:
- settings a start refuses, in every section, each tagged with its block
- keys nothing reads, each with the file it is in and the closest real key
- the TLS key pair, and the
cert_optionskey ID templates,requireconditions and lifetime policies the certificate service parses at start - the warnings a start logs: a non-https
public_url, a plaintext or unauthenticated mail relay,ldap.tls_insecure_skip_verify, and the rest
sudo ssoosshd config check # the file serve would findsudo ssoosshd -c /etc/ssoossh/ssoosshd.yaml config check --strictssoosshd -c ssoosshd.yaml config check --json # machine-readablessoosshd -c ssoosshd.yaml config check --show-sourcesIt exits 0 when the configuration is valid, warnings included, and 1 when it
has any error. Unknown keys are warnings, because a file written for a newer
release must still load; --strict makes them errors. --show-sources adds
every effective setting and the layer it came from (default, preset or
file), with secrets shown as [redacted], as on the admin configuration
page. The server reads no configuration keys from the environment, so there is
no environment layer.
Nothing is contacted: no database, directory, NATS broker, CA key source or
identity provider. Use ldap probe
and the diagnostics page for those. Local
files the configuration names are read, as a start reads them: ssh_key_file,
the TLS pair, the NATS certificates, password files and mail.template_dir.
Run it as the user the service runs as, on a host where those paths exist, or
expect it to report them missing.
In CI for a config repository, where the CA key and certificates are not
present, point those paths at stand-ins in the job and keep --strict, so a
renamed or misspelt key fails the pipeline rather than the next restart:
- name: Check ssoosshd.yaml run: ssoosshd -c ssoosshd.yaml config check --strict --json > config-check.jsonIn a package postinstall, report a problem but do not fail the install: an upgrade that leaves a key the new release no longer reads is something to say, not a reason to leave the package half-configured.
if ! ssoosshd config check >/dev/null 2>&1; then echo "ssoosshd: the configuration has problems; run 'ssoosshd config check' before restarting" >&2fiFirst start and health check
Section titled “First start and health check”systemctl status ssoosshdjournalctl -u ssoosshd -fssoosshd validates its whole configuration at startup and refuses to run on
a bad one rather than failing at the first request, naming the problem in
each section that has one. A malformed key ID
template, an unparseable CIDR in a source policy, a mail template override
that does not compile, an unreadable SMTP password file, a lifetime policy
with no default_duration, multi_instance: true with no http.cookie_key
– all of these stop the process with a message naming the setting.
ssoosshd config check finds them
without a start.
Two liveness endpoints answer without authentication, and both are registered ahead of the host-name check so a probe addressing the server by IP still reaches them:
curl -f http://localhost:8080/healthz # {"status":"ok"}curl -f http://localhost:8080/ping # pongThen confirm the CA key the running server actually holds, rather than trusting the file you generated:
ssoossh --server https://ssh.example.com caThat is the same key to install on your hosts as TrustedUserCAKeys –
Trusting the CA in sshd.
What the version endpoint discloses
Section titled “What the version endpoint discloses”/api/version answers without authentication, and the web UI’s footer
renders what it says: a tagged build as its version linked to the GitHub
release, an untagged one as development with a short commit. That is
convenient for support, and it is also enough for anyone to match a build
against known issues. version.mode
decides how much to say:
version.mode |
/api/version returns |
The footer shows |
|---|---|---|
show (default) |
the real version, commit, and release URL | the version, linked to its release |
hide |
every field empty | no version at all |
| any other string | that string as the version, with no commit, repository, or release link | that string, unlinked |
show and hide are reserved words, so neither can itself be used as a
decoy version.
Point it at your identity provider. Until an OIDC client exists, nobody can log in to approve anything.