- Rust 74.4%
- Shell 24.5%
- Makefile 0.7%
- Dockerfile 0.4%
|
All checks were successful
centralssh-build / freebsd-amd64 (push) Successful in 2m53s
centralssh-build / linux-build-test (push) Successful in 4m31s
centralssh-build / freebsd-aarch64 (push) Successful in 3m23s
centralssh-build / linux-packages-amd64 (push) Successful in 1m25s
centralssh-build / linux-packages-arm64 (push) Successful in 1m26s
centralssh-build / release-publish (push) Successful in 52s
|
||
|---|---|---|
| .forgejo/workflows | ||
| ci | ||
| container | ||
| docker-compose-example | ||
| examples | ||
| expect-test | ||
| packaging | ||
| src | ||
| tests | ||
| tools | ||
| .codex | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| ACCESS.md | ||
| AGENTS.md | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CI.md | ||
| container.md | ||
| disection.md | ||
| Dockerfile | ||
| LICENSE | ||
| Makefile | ||
| Makefile-dist | ||
| Makefile-openrc-dist | ||
| op-guide.md | ||
| README-dist.md | ||
| README-openrc-dist.md | ||
| README.md | ||
| SECURITY-SNDL.md | ||
CentralSSH
CentralSSH is a hardened, OpenSSH-compatible SSH gateway. It is a broker, not a shell host.
Users connect with a normal SSH client, authenticate through CentralSSH (username -> password -> TOTP), select an approved target, and CentralSSH proxies the session to that target with a per-user key.
Protocol Boundary
CentralSSH is strictly an SSH server.
- OpenSSH client compatible.
- No custom SSH protocol extensions.
- Accepts only standard SSH mechanisms needed for gateway flow.
- Proxies standard SSH behavior after target selection, including shell,
exec, subsystem/SFTP, and forwarding requests. - Relays post-selection session requests and data from the
russhserver callback path instead of depending on a gateway-side pseudo-shell. - Treats SSH channel
window-adjustmessages as normal flow control instead of session-fatal input. - Leaves interactive
sftptab completion to the stock OpenSSH client, which keeps working as long as the client binary includes its normal line-editing support. - Denies gateway-local shell, gateway-local command execution, gateway filesystem access, and agent forwarding.
- Gateway login auth is internal only (
username/password/TOTP), not SSH public-key auth.
What You Get
- SSH transport compatible with standard
sshclients. - Internal auth flow:
username -> password -> TOTP. - Argon2id password hashing with automatic bootstrap migration.
- Forced first-login password change and TOTP enrollment.
- Per-user allowed-server authorization.
- Strict outbound host-key verification against known_hosts.
- Structured JSONL audit logging.
- Startup reconciliation for missing per-user outbound keys.
- Hot config reload on
SIGHUP. - Frontend SSH transport now prefers the OpenSSH-aligned hybrid PQ KEX
mlkem768x25519-sha256, with explicit classical Curve25519 fallback unless PQ-only mode is enabled. - Centralized SSH crypto policy; see
SECURITY-SNDL.mdfor Store Now, Decrypt Later limits, the currentsntrup761x25519-sha512gap, and rollout guidance.
Install Targets and Paths
Default runtime paths:
- Config:
/etc/centralssh/config.toml - Servers map:
/etc/centralssh/servers.toml - Known hosts:
/etc/centralssh/known_hosts - User key root:
/var/lib/centralssh/keys - Audit log:
/var/log/centralssh/audit.jsonl - Binary:
/usr/local/sbin/centralssh - Helper tool:
/usr/local/bin/cssh-keyscan - Gateway server host key:
/etc/centralssh/host_ed25519
Quick Start (FreeBSD First)
- Build and install:
cd /path/to/centralSSH
make
sudo make install
- Enable and start service:
sudo sysrc centralssh_enable=YES
sudo service centralssh start
sudo service centralssh status
- Populate host keys for each target server:
sudo cssh-keyscan 192.168.122.123
- Connect from a client:
ssh -p 7788 <gateway-host>
Linux (systemd)
sudo systemctl daemon-reload
sudo systemctl enable --now centralssh
sudo systemctl status centralssh
The shipped unit writes process logs to journald and defaults to:
CENTRALSSH_LOG=infoCENTRALSSH_LOG_FORMAT=systemd
Useful overrides:
sudo systemctl edit centralssh
Example drop-in:
[Service]
Environment=CENTRALSSH_LOG=debug,centralssh=debug
Environment=CENTRALSSH_LOG_FORMAT=json
Containers
The repo now ships container artifacts for Docker and Podman:
Dockerfile.dockerignorecompose.yamlcontainer.md
Use container.md for the full container deployment guide, including bind mounts, health checks, rootless notes, and Podman compatibility guidance.
The shipped docker-compose-example/docker-compose.yml accepts CENTRALSSH_PUBLISH_PORT via .env if the host cannot spare TCP 7788.
FreeBSD rc.conf Overrides
Optional rc.conf keys:
centralssh_enable="YES"
centralssh_listen="0.0.0.0:7788"
centralssh_config="/etc/centralssh/config.toml"
centralssh_servers="/etc/centralssh/servers.toml"
centralssh_known_hosts="/etc/centralssh/known_hosts"
centralssh_user_key_root="/var/lib/centralssh/keys"
centralssh_audit_log="/var/log/centralssh/audit.jsonl"
centralssh_whitelist="/etc/centralssh/whitelist.txt"
centralssh_per_user_per_server="true"
centralssh_drop_to_menu="false"
centralssh_hide_proxy_ip="false"
centralssh_per_user_per_server controls only the key-layout and policy-resolution mode override. The actual allow_local_forwarding, allow_remote_forwarding, allow_sftp, and allow_scp values remain per-user or per-server entries in config.toml, not rc.conf booleans.
Makefile Behavior
Standard pipeline:
cd /path/to/centralSSH
make
sudo make install
make install:
- Installs
centralsshandcssh-keyscan. - Creates
/etc/centralsshlayout. - Installs example
config.tomlandservers.tomlif missing. - Creates
/etc/centralssh/known_hostsif missing. - Creates
/var/log/centralssh/audit.jsonlif missing. - Installs FreeBSD rc script on FreeBSD.
- Installs systemd unit on non-FreeBSD hosts.
Forgejo CI
.forgejo/workflows/build.yml now runs the project-specific CentralSSH build matrix described in CI.md for Linux and FreeBSD only, and it triggers only on version-tag pushes.
linux-build-test: locked host build plus unit tests.linux-packages-amd64:systemdtarball,openrctarball,.deb, and.rpm, then explicit artifact validation and Linux runtime smoke checks.linux-packages-arm64:systemdtarball,openrctarball,.deb, and.rpm, then explicit artifact validation.freebsd-amd64: nativefreebsd-15runner build that emits version-tagged.pkgand.tar.gzartifacts such ascentralssh-<version>-freebsd-15.0-RELEASE-amd64.*, so the filename itself identifies the intended FreeBSD release, then installs and rc-script checks the package on the FreeBSD runner itself when non-interactive root access is available.freebsd-aarch64: nativefreebsd-15runner cross-build that emits version-taggedaarch64.pkgand.tar.gzartifacts such ascentralssh-<version>-freebsd-15.0-RELEASE-aarch64.*, so the filename itself identifies the intended FreeBSD release, without the unstable Linux-hosted QEMU boot path. The cross path uses the FreeBSDaarch64sysroot pluscargo +nightly -Z build-std.
The workflow intentionally avoids third-party uses: steps. Checkout, Rust toolchain setup, packaging, validation, artifact staging, repository mirroring, and release publication are all done with repository-local shell logic so Forgejo and GitHub assumptions do not become hidden dependencies.
Tagged package jobs stage their validated build outputs on a draft Forgejo release. The final release-publish job waits for every required Linux and FreeBSD package job, downloads the expected staged release attachments into one fresh release workspace, writes SHA256SUMS and SHA512SUMS, publishes the final Forgejo release, syncs the repository branches and tags to https://github.com/firebadnofire/centralssh, and then publishes the same asset set on the GitHub release for the tag.
The release pipeline treats the git tag as canonical. CI rewrites Cargo.toml to the normalized tag version, refreshes Cargo.lock, and passes that version through the build and packaging steps. Runtime centralssh --version and centralssh -v report the normalized version, while CI/distribution builds append -dist.
When a CI step fails, including release staging and release publication steps, the workflow now ships a filtered tail of the captured error log to the internal ingestion endpoint defined in CI.md. The release publication path also includes the failing command and log file path so runner-local API or download failures are explicit instead of collapsing to a bare curl exit line.
Frozen CI contract
These pieces are now part of the release contract and should not be changed casually:
ci/release-version.shis the only place that derives the canonical release version from a tag.ci/rewrite-release-version.shrewrites package metadata from that canonical version only.ci/stage-release-artifacts.shstages already-built artifacts on the draft release and should not reconstruct filenames.ci/publish-release.shdownloads staged assets by their attachment UUIDs and publishes the final Forgejo release after validating the expected asset list.ci/sync-github-mirror.shpushes the checked-out repository's origin branches and tags tofirebadnofire/centralsshusingGH_KEY.ci/publish-github-release.shreuses the validated release workspace and publishes the same artifacts plusSHA256SUMSandSHA512SUMSto the GitHub release for the tag.build.rsandsrc/version_support.rscontrol the runtime--version/-vstring. Local builds reportcentralssh <version>, CI distribution builds reportcentralssh <version>-dist.
Do not reintroduce:
- release version parsing from
Cargo.toml - browser download URLs for staged release assets
- ad hoc version string mutation like
0.43vs0.0.43 - extra JS release actions for the Forgejo pipeline
- QEMU-based FreeBSD release staging
Configuration
CentralSSH reads config from TOML files.
/etc/centralssh/config.toml
Example with two users:
[[users]]
name = "alice"
password = "REPLACE_WITH_UNIQUE_TEMPORARY_PASSWORD"
must_change_password = true
allowed_servers = ["git", "httpd"]
[[users]]
name = "bob"
password = "REPLACE_WITH_UNIQUE_TEMPORARY_PASSWORD"
must_change_password = true
allowed_servers = ["dns"]
[settings]
user_key_root = "/var/lib/centralssh/keys"
per_user_per_server = true
drop_to_menu = false
hide_proxy_ip = false
known_hosts_path = "/etc/centralssh/known_hosts"
audit_log_path = "/var/log/centralssh/audit.jsonl"
whitelist_path = "/etc/centralssh/whitelist.txt"
enforce_password_policy = true
min_password_policy = 12
[git.alice]
allow_local_forwarding = true
allow_remote_forwarding = false
allow_sftp = true
allow_scp = true
[httpd.alice]
allow_local_forwarding = false
allow_remote_forwarding = false
allow_sftp = true
allow_scp = false
[dns.bob]
allow_local_forwarding = false
allow_remote_forwarding = false
allow_sftp = true
allow_scp = true
[kex_policy]
frontend_preferred = [
"mlkem768x25519-sha256",
"curve25519-sha256",
"curve25519-sha256@libssh.org",
]
frontend_require_post_quantum = false
backend_preferred = [
"mlkem768x25519-sha256",
"curve25519-sha256",
"curve25519-sha256@libssh.org",
]
backend_require_post_quantum = false
[fail2ban]
enabled = true
max_failures = 5
find_time = "60s"
ban_time = "10m"
max_ban_time = "24h"
backoff_multiplier = 2.0
delay_before_ban = true
delay_time = "2s"
persist_state = true
state_path = "/var/lib/centralssh/fail2ban_state.json"
[fail2ban.whitelist]
ips = ["127.0.0.1/32", "::1/128", "192.168.0.0/16"]
Fields:
users: required non-empty array.users[].name: required, unique,1..64, chars[a-zA-Z0-9._-].users[].password: Argon2id hash or bootstrap plaintext.users[].totp_secret: optional base32 TOTP secret.users[].must_change_password: boolean.users[].allowed_servers: required non-empty list of server names inservers.toml.users[].allow_local_forwarding,users[].allow_remote_forwarding,users[].allow_sftp,users[].allow_scp: only valid whensettings.per_user_per_server=false.settings.user_key_root: optional path override.settings.per_user_per_server: optional bool, defaulttrue. Whentrue, CentralSSH uses one outbound key per user and server. Whenfalse, it uses one outbound key per user.settings.drop_to_menu: optional bool, defaultfalse. Whentrue, a completed interactive shell returns to the server menu on the same shell channel.sftpandscpdo not support an inline post-exit gateway menu with stock OpenSSH clients, so those channels close normally. ChoosingQfrom either selection menu disconnects the SSH session instead of restarting authentication.- Interactive
sftpfilename completion is preserved because CentralSSH proxies the subsystem stream instead of replacing the client editor, so normal OpenSSH builds keep tab completion out of the box. - The inline post-shell menu ignores cursor-control escape sequences such as arrow keys instead of echoing them into the selection line.
- The gateway title is shown on the server-selection screen and is not repeated on each keyboard-interactive auth prompt.
settings.hide_proxy_ip: optional bool, defaultfalse. Whentrue, the logged-in server-selection menu shows only logical server names and omits the configured endpoint IP or hostname from the rendered list.settings.known_hosts_path: optional path override.settings.audit_log_path: optional path override.settings.whitelist_path: optional path to a fail2ban bypass file with one IPv4 or IPv6 address per row.settings.enforce_password_policy: optional bool, defaulttrue.settings.min_password_policy: optional integer minimum password length, default12.- The server-selection prompt accepts
Qto quit. kex_policy.frontend_preferred: ordered frontend SSH KEX allowlist. Supported values today aremlkem768x25519-sha256,curve25519-sha256, andcurve25519-sha256@libssh.org.kex_policy.frontend_require_post_quantum: optional bool, defaultfalse. Whentrue, CentralSSH advertises only supported post-quantum frontend KEX algorithms and classical-only clients fail during SSH negotiation. Legacykex_policy.require_post_quantumis still accepted as a compatibility alias for this frontend-only setting.kex_policy.backend_preferred: ordered gateway-to-target SSH KEX allowlist. Supported values today are the same three names asfrontend_preferred.kex_policy.backend_require_post_quantum: optional bool, defaultfalse. Whentrue, CentralSSH refuses to negotiate a classical-only outbound SSH transport to the selected target.fail2ban.enabled: optional bool, defaulttrue.fail2ban.max_failures: optional integer threshold inside the sliding window, default5.fail2ban.find_time: optional duration string, default60s.fail2ban.ban_time: optional duration string for the first ban, default10m.fail2ban.max_ban_time: optional duration string cap for repeated bans, default24h.fail2ban.backoff_multiplier: optional float, default2.0.fail2ban.delay_before_ban: optional bool, defaulttrue.fail2ban.delay_time: optional duration string, default2s.fail2ban.persist_state: optional bool, defaulttrue.fail2ban.state_path: optional path for persisted abuse state, default/var/lib/centralssh/fail2ban_state.json.fail2ban.whitelist.ips: optional IPv4/IPv6 CIDR list. Defaults include127.0.0.1/32and::1/128.
Authorization policy:
- CentralSSH resolves one effective post-auth policy for each
username + selected target. - Default values for missing policy keys are explicit and deterministic:
allow_local_forwarding=false,allow_remote_forwarding=false,allow_sftp=true,allow_scp=true. - When
settings.per_user_per_server=false, CentralSSH readsallow_*fields directly from each[[users]]entry and rejects[server.user]policy tables at load or reload time. - When
settings.per_user_per_server=true, CentralSSH readsallow_*fields only from[server.user]tables such as[git.alice]and rejects user-levelallow_*fields at load or reload time. - Per-server policy tables must reference an existing server, an existing user, and a user/server pair already allowed by
users[].allowed_servers. allow_local_forwarding=falserejectsdirect-tcpipchannel opens before any backend forwarding channel is established.allow_remote_forwarding=falserejectstcpip-forwardandcancel-tcpip-forwardrequests before any backend listener is created or touched.allow_sftp=falserejectssubsystemrequests where the subsystem name is exactlysftp.allow_scp=falserejects SCP-styleexecrequests after conservative command parsing detectsscpsource or sink mode flags such as-for-t. It does not treat arbitraryexecrequests as SCP.- When SFTP or SCP is denied after successful authentication and target selection, CentralSSH accepts the request long enough to emit explicit stderr text such as
sftp: access deniedorscp: access denied, then disconnects the SSH session cleanly instead of leaving the client with only a generic channel failure.
Notes:
- Outbound target SSH username is always the authenticated CentralSSH username.
- If bootstrap plaintext passwords are present, CentralSSH hashes them with Argon2id at startup and atomically rewrites config.
- The documented placeholder password is rejected at config validation time. Replace it before starting the service.
- The frontend listener and outbound SSH client currently support
mlkem768x25519-sha256but notsntrup761x25519-sha512; configuring the latter in either frontend or backend policy fails startup validation. SIGHUPreload updates auth, authorization, and abuse-control settings, but transport KEX policy is fixed for existing listener/client configs and currently requires a process restart to change the frontend offer set.- No OpenSSH weak-crypto warning on the frontend means the client-to-gateway KEX was hybrid; it does not mean signatures, stored keys, or every outbound target session are post-quantum.
- Denied forwarding, SFTP, and SCP operations are post-auth policy denials. They are audited as
denied_*events and do not increment password/TOTP failure counters or fail2ban bans.
PQ Validation
For a repeatable local validation of frontend PQ negotiation and strict-mode rejection, use tools/validate-pq-kex.sh.
Build first:
CARGO_HOME=/tmp/centralssh-cargo-home CARGO_TARGET_DIR=/tmp/centralssh-target cargo build
chmod +x tools/validate-pq-kex.sh
CENTRALSSH_BIN=/tmp/centralssh-target/debug/centralssh tools/validate-pq-kex.sh
Built-In Abuse Protection
CentralSSH includes an internal fail2ban-style tracker keyed primarily by remote IP.
Behavior:
- Failures are tracked in a sliding window, not a fixed reset bucket.
- Normal SSH auth-method discovery such as
none,publickey, or disabledpasswordprobes does not count as a failed login. - Authenticated policy denials for forwarding, SFTP, and SCP are kept separate from brute-force tracking. They are logged, but they do not poison login-failure counters or trigger fail2ban bans.
max_failuresinsidefind_timecreates a ban forban_time.- Repeated bans for the same IP use exponential backoff and stop growing at
max_ban_time. - Optional tarpitting applies
delay_timejust before the ban threshold whendelay_before_ban=true. - CIDR whitelist entries and
settings.whitelist_pathfile entries bypass bans and failure tracking. - If persistence is enabled, ban state is atomically saved and reloaded from
fail2ban.state_path.
Operational notes:
- Whitelist trusted admin networks deliberately to avoid locking out operators during maintenance.
- Keep the whitelist as narrow as practical; prefer a management subnet over broad RFC1918 ranges.
settings.whitelist_pathexpects one exact IP address per line and accepts both IPv4 and IPv6.- The persisted state file contains timestamps, IPs, usernames, target names, and ban metadata only. It does not contain passwords or TOTP material.
/etc/centralssh/servers.toml
[servers]
git = "192.168.86.44"
httpd = "192.168.86.41"
dns = "192.168.86.53"
Rules:
- User
allowed_serversentries must exactly match keys in this file. - Values are target host/IP strings used for outbound SSH.
Auth and Session Flow
- Client connects to CentralSSH SSH port (default
7788). - CentralSSH prompts for
Username. - CentralSSH prompts for
Password. - Only after password success, CentralSSH prompts for
TOTP Code. - If password change is required, user must change password.
- If TOTP secret is missing, user must complete TOTP enrollment.
- User sees menu of allowed servers.
- User selects a server.
- CentralSSH opens outbound SSH session to
<target>:22. - On proxy session exit, user returns to gateway menu.
Password and TOTP Lifecycle
- Password hashing algorithm: Argon2id.
- Bootstrap plaintext is migrated to Argon2id on startup.
- First-login password change enforced via
must_change_password. - TOTP uses RFC6238 semantics with 30s step and drift tolerance.
- TOTP secrets are base32 and never logged.
How to set user passwords
Supported approaches:
- Recommended bootstrap flow: set a temporary plaintext in
config.tomlandmust_change_password=true; CentralSSH migrates it to Argon2id at startup. - Pre-hash offline and place Argon2id string directly in
users[].password.
Per-User Outbound Keys
Outbound private key path pattern:
- Default:
<user_key_root>/<username>/<server>/id_ed25519 - With
PER_USER_PER_SERVER=false:<user_key_root>/<username>/id_ed25519
Behavior:
- On startup, CentralSSH reconciles configured users and allowed servers.
- Missing user directories, server directories when enabled, and
id_ed25519files are created automatically. - Existing keys are not overwritten.
- Keys are for outbound proxy auth only, not gateway login.
Host Key Management (cssh-keyscan)
cssh-keyscan fetches target host keys and updates CentralSSH known_hosts.
Path resolution:
CENTRALSSH_KNOWN_HOSTSoverrides everything.- On FreeBSD, if that env var is unset, the tool follows
centralssh_known_hostsfrom the samerc.conf/rc.conf.dflow as the service script. - Otherwise it falls back to
/etc/centralssh/known_hosts.
Basic usage:
sudo cssh-keyscan <server IP or domain>
Scriptable verified usage with expected key material:
sudo cssh-keyscan 192.168.122.123 'AAAAC3NzaC1lZDI1NTE5AAAAIHiKldTfYnX3R0tRkMA6Xy1z9NJ+IGp8H7wQy2kCoGM/'
sudo cssh-keyscan 192.168.122.123 'ssh-ed25519 SHA256:FXTNTbOFUWoYI7C4ND351UCvqSY8fhafJsjqqxInbUo'
Accepted expected-key formats:
<base64-key-blob><algorithm> <base64-key-blob>SHA256:<fingerprint><algorithm> SHA256:<fingerprint>
Security behavior:
- New host without expected key: interactive TOFU prompt.
- New host with expected key: requires at least one scanned key to match; TOFU prompt skipped.
- New host with key overlap: TOFU output shows overlapping hostnames.
- Existing host with any newly presented key: hard fail; no file modification.
- MD5 fingerprints are not accepted.
File Ownership and Modes (Production)
Required for strict mode (--enforce-strict-security true, default):
/etc/centralssh/config.toml: ownerroot, mode0600/etc/centralssh/servers.toml: ownerroot, mode0600/etc/centralssh/known_hosts: ownerroot, mode0600/var/lib/centralssh/keys: ownerroot, mode0700/var/log/centralssh/audit.jsonl: ownerroot, mode0600
Post-Install Validation Checklist
Run these checks after installation:
sudo ls -ld /etc/centralssh /var/lib/centralssh/keys /var/log/centralssh
sudo ls -l /etc/centralssh/config.toml /etc/centralssh/servers.toml /etc/centralssh/known_hosts /etc/centralssh/host_ed25519 /var/log/centralssh/audit.jsonl
sudo service centralssh status || sudo systemctl status centralssh
Expected posture:
- Sensitive files are mode
0600. - User key root directory is mode
0700. - Ownership is root in strict production mode.
CLI and Environment Overrides
Show help:
/usr/local/sbin/centralssh --help
Run manually:
/usr/local/sbin/centralssh \
--listen 0.0.0.0:7788 \
--config /etc/centralssh/config.toml \
--servers /etc/centralssh/servers.toml \
--known-hosts /etc/centralssh/known_hosts \
--user-key-root /var/lib/centralssh/keys \
--audit-log /var/log/centralssh/audit.jsonl
Flags:
--listen--config--servers--known-hosts--user-key-root--per-user-per-server--audit-log--enforce-strict-security(defaulttrue)
Environment variables:
CENTRALSSH_LISTENCENTRALSSH_CONFIGCENTRALSSH_SERVERSCENTRALSSH_KNOWN_HOSTSCENTRALSSH_USER_KEY_ROOTPER_USER_PER_SERVERCENTRALSSH_AUDIT_LOGCENTRALSSH_ENFORCE_STRICT_SECURITY
Reload and Operations
FreeBSD service commands
sudo service centralssh start
sudo service centralssh stop
sudo service centralssh restart
sudo service centralssh status
systemd commands
sudo systemctl start centralssh
sudo systemctl stop centralssh
sudo systemctl restart centralssh
sudo systemctl status centralssh
sudo journalctl -u centralssh -f
Reload config without dropping active sessions
sudo kill -HUP $(pgrep -x centralssh)
Reload behavior:
- Valid config: applied in-memory.
- Invalid config: rejected, previous config remains active.
Auditing
Audit file:
/var/log/centralssh/audit.jsonl
Event schema fields:
timestampevent_typerequest_idremote_ipremote_portusernametarget_serverauth_methodresultreasonban_duration_secondsban_until
Representative events:
connection_openedconnection_rejected_bannedauth_attemptauth_successauth_failureunknown_username_attemptauthorization_deniedprotocol_errorban_createdban_extendedban_expiredwhitelist_bypassrate_limit_delay_applied
Example JSONL record:
{"timestamp":"2026-05-02T20:00:00Z","event_type":"ban_created","request_id":"c7b2d8d7-66bc-4b68-a4d1-d7c0a0f4a7d9","remote_ip":"203.0.113.44","remote_port":51422,"username":"alice","target_server":null,"auth_method":"keyboard_interactive","result":"banned","reason":"fail2ban threshold reached","ban_duration_seconds":600,"ban_until":"2026-05-02T20:10:00Z"}
Secrets are not logged.
Troubleshooting
centralssh error: I/O error: No such file or directory (os error 2)
Missing one or more required paths.
Check:
/etc/centralssh/config.toml/etc/centralssh/servers.toml/etc/centralssh/known_hosts/var/log/centralssh/audit.jsonl
Fix:
sudo make install
make install fails under sudo with cargo: No such file or directory
Cause: root shell PATH does not include cargo.
Fix:
- Build as normal user first:
make - Install as root after build:
sudo make install
install ... Operation not permitted or Permission denied
Cause: writing into /usr/local without privileges.
Fix:
- Use
sudo make install.
Service restart reports stale pid or "already running"
Fix:
sudo service centralssh stop
sudo service centralssh start
sudo service centralssh status
invalid configuration: user '<name>' references unknown server '<server>'
Cause: allowed_servers entry does not exactly match any key in servers.toml.
Fix:
- Correct spelling/case in
allowed_servers. - Ensure matching server key exists in
/etc/centralssh/servers.toml.
Host key verification failures when selecting a server
Cause: target host key missing or changed in known_hosts.
Fix:
- Verify target identity out-of-band.
- Update
/etc/centralssh/known_hostsviacssh-keyscan. - Retry connection.
Non-interactive cssh-keyscan fails with TOFU-required message
Cause: new host and no expected key argument in a non-TTY context.
Fix:
- Provide expected key argument for scriptable mode.
- Or run interactively and confirm TOFU prompt.
Development Mode
For local/dev-only runs where production file ownership/modes are not available:
mkdir -p ./tmp/users
cp examples/config.toml ./tmp/config.toml
cp examples/servers.toml ./tmp/servers.toml
touch ./tmp/known_hosts ./tmp/audit.jsonl
cargo run -- \
--config ./tmp/config.toml \
--servers ./tmp/servers.toml \
--known-hosts ./tmp/known_hosts \
--user-key-root ./tmp/users \
--audit-log ./tmp/audit.jsonl
Disable strict-security checks for that run with:
CENTRALSSH_ENFORCE_STRICT_SECURITY=false cargo run -- \
--config ./tmp/config.toml \
--servers ./tmp/servers.toml \
--known-hosts ./tmp/known_hosts \
--user-key-root ./tmp/users \
--audit-log ./tmp/audit.jsonl
Do not use this mode in production.
Connect from Clients
ssh -p 7788 <gateway-host>
OpenSSH client options like -J/ProxyJump work as normal when targeting CentralSSH.
License
See LICENSE.