All notable changes to this project will be documented in this file.
ziti-ssh-ca now has an enroll subcommand that mirrors ziti-ssh enroll and ziti-scp enroll exactly.
- Reads the one-time JWT file, calls
enroll.EnrollwithKeyAlg = "EC", and writes the enrolled identity JSON to/etc/ziti-ssh-ca/identity.json(mode 0600) by default. --out <path>overrides the output path.--jwt <path>is required.- No CA-specific post-enrollment steps are performed (unlike
ziti-ssh-host enroll, which also fetches CA public keys and configures sshd).
Updated docs:
docs/provisioning.md: "Setting upziti-ssh-ca" step 3 now usesziti-ssh-ca enroll --jwt /tmp/ssh-ca-server.jwtinstead of theziti edge enroll+chmodtwo-step.docs/configuration.md:ziti-ssh-ca enrollflag table added before the existingconfigsubcommands section.CLAUDE.md: updated theziti-ssh-cadescription and component section to listenrollas a subcommand.
New -A / --forward-agent flag on ziti-ssh connect. When set, the local SSH agent (identified by SSH_AUTH_SOCK) is forwarded to the remote session so that processes on the remote host can use keys held by the local agent — for example, to make onward SSH hops without copying private keys to the remote.
Behaviour:
- On the fast path (no
-L/-R/-Dforwards),forwardAgentis passed directly toclient.RunSession, which callsclient.ForwardAgentbeforesession.Shell(). - On the forwarding path (has
-L/-R/-Dforwards, usesNewSSHClient),runSSHClientSessioncallsclient.ForwardAgent(sshClient, session)beforesession.Shell(). - If
SSH_AUTH_SOCKis not set or the agent cannot be reached, aWARN-level log message is emitted and the session opens normally — agent forwarding failure is non-fatal. - The SSH certificates issued by
ziti-ssh-caalready include thepermit-agent-forwardingextension, so no CA or sshd configuration changes are needed.
New exported function in client/ssh.go:
ForwardAgent(sshClient *ssh.Client, session *ssh.Session) error— connects to the local SSH agent viaSSH_AUTH_SOCK, callsagent.ForwardToAgent(sshClient, agentClient)to register the agent-channel handler, spawns a goroutine to close the agent connection when the SSH client closes, and callsagent.RequestAgentForwarding(session)to send theauth-agent-req@openssh.comchannel request. Returns an error on any failure; the caller treats this as a warning.
RunSession signature change:
RunSession(conn, user, host, signer, forwardAgent bool)—forwardAgent boolparameter added as the fifth argument. All call sites updated.
docs/usage.md: new "SSH agent forwarding (-A)" section with usage examples, agent setup instructions, and a note about combining with port forwarding.docs/configuration.md:-A/--forward-agentrow added to theziti-ssh connectflag table.CLAUDE.md:connectbullet updated to describe-Abehaviour;client/ssh.goAPI section updated withForwardAgentand updatedRunSessionsignature.
Port forwarding flags added to ziti-ssh connect, mirroring ssh(1) syntax. All flags may be repeated to open multiple forwards simultaneously.
New flags on connect:
-L/--local-forward[bind:]localport:remotehost:remoteport— local port forward;ziti-sshlistens locally and forwards each accepted connection toremotehost:remoteportthrough the SSH tunnel via adirect-tcpipchannel. Default bind:127.0.0.1.-R/--remote-forward[bind:]remoteport:localhost:localport— remote port forward; uses(*ssh.Client).Listento ask sshd to bind on the remote side; each incoming connection is forwarded tolocalhost:localporton the client machine.-D/--dynamic[bind:]port— dynamic SOCKS5 proxy; implements RFC 1928 CONNECT with no-auth (METHOD=0x00); for each SOCKS5 CONNECT request adirect-tcpipchannel is opened to the requested destination. Supports IPv4 (ATYP 0x01), domain name (ATYP 0x03), and IPv6 (ATYP 0x04). Default bind:127.0.0.1.-N/--no-shell— do not open a shell; only run the requested forwards; block until SIGINT/SIGTERM.
Implementation:
- When any forward flag is present,
runConnectcallsclient.NewSSHClientto obtain a*ssh.Client, then starts each forward in a goroutine under a sharedcontext.Context. - SIGINT/SIGTERM cancels the context, closing all local listeners and unblocking all goroutines cleanly.
- Without
-N, a shell (or remote command) runs concurrently; when the shell exits it cancels the context and terminates all forwards. Async.WaitGroupensures all goroutines finish beforerunConnectreturns. - Without any forward flags the previous fast path (
client.RunSession/client.RunCommand) is used unchanged — no regression for existing users. - Forward-spec parse helpers added to
cmd/ziti-ssh/main.go:parseLocalForward,parseRemoteForward,parseDynamicForward.
New functions in client/ssh.go:
NewSSHClient(conn net.Conn, user, host string, signer ssh.Signer) (*ssh.Client, error)— performs the SSH handshake and returns a*ssh.Clientwithout opening any session.RunLocalForward(ctx context.Context, sshClient *ssh.Client, spec LocalForwardSpec) error— local forward loop; cancels cleanly on ctx.RunRemoteForward(ctx context.Context, sshClient *ssh.Client, spec RemoteForwardSpec) error— remote forward loop; usessshClient.Listen.RunDynamicProxy(ctx context.Context, sshClient *ssh.Client, spec DynamicForwardSpec) error— SOCKS5 proxy loop.LocalForwardSpec,RemoteForwardSpec,DynamicForwardSpec— typed spec structs for each forwarding mode.biCopy(ctx, a, b net.Conn)— bidirectionalio.Copyhelper; half-closes the write side when one direction closes.
New helper in cmd/ziti-ssh/main.go:
runSSHClientSession(*ssh.Client) error— PTY shell on a pre-built*ssh.Client; mirrorsclient.RunSessionbut avoids repeating the SSH handshake.runSSHClientCommand(*ssh.Client, string) error— non-interactive command on a pre-built*ssh.Client.
New proxy [user@]<target> subcommand. Dials the Ziti service raw (no SSH handshake) and bridges os.Stdin/os.Stdout to the connection via two io.Copy goroutines. Exits when either side closes (EOF).
Intended for use as a ProxyCommand in ~/.ssh/config:
Host web-server-prod
ProxyCommand ziti-ssh proxy %h
User ziggy
With this entry, ssh, git, rsync, ansible, VS Code Remote SSH, and any other SSH-based tool work through the Ziti overlay transparently without any Ziti awareness.
- Auto-refreshes the SSH certificate (same 5-minute threshold as
connect) so the caller'ssshprocess finds a valid cert in~/.ssh/<key>-cert.pub. - Uses the same Ziti service resolution as
connect: direct service name check, then terminator address on--ssh-service. - Shares the same
--identity,--ssh-service,--ca-service,--key, and--oidc-issuerflags asconnect. - The
user@prefix is accepted for compatibility withProxyCommand ziti-ssh proxy %r@%hbut is not used. proxyParamsstruct andrunProxyfunction added tocmd/ziti-ssh/main.go.
docs/usage.md: new "Port forwarding" section (local, remote, dynamic,-N, combined with shell) and "Usingziti-sshas a ProxyCommand" section (config examples, wildcard patterns, VS Code/git/rsync/ansible usage).docs/configuration.md:-N,-L,-R,-Dflags added to theziti-ssh connecttable; newziti-ssh proxyflag table.CLAUDE.md:connectcomponent description updated to cover all forwarding flags and-N;proxysubcommand added;client/ssh.goAPI section updated with new exported functions.
New file cmd/ziti-ssh-ca/config.go (package main). Adds a config cobra parent command registered with the root, containing three subcommands:
config print— no flags, no controller connection. Prints the human-readable field table and the indented JSON schema to stdout.config apply— idempotent create-or-update. Authenticates to the controller, callsListConfigTypeswith aname="ziti-ssh-host.v1"filter, then eitherCreateConfigType(not found) orUpdateConfigType(found). Printscreated ...orupdated ... (id: <id>).config remove— locates the type by name and callsDeleteConfigType. Exits 0 with a human-readable message if the type is not found.
Flags (--controller, --username, --password, --insecure, --controller-ca) are defined as PersistentFlags on the config parent so both apply and remove inherit them. All flags have corresponding env var overrides (ZITI_CTRL_ADDRESS, ZITI_CTRL_USERNAME, ZITI_CTRL_PASSWORD, ZITI_CTRL_INSECURE, ZITI_CTRL_CA). --insecure and --controller-ca are validated as mutually exclusive before any API call. Default controller port is 443 when no port is present in --controller. Uses github.com/openziti/edge-api/rest_management_api_client (promoted from indirect to direct dependency).
New types in host/host.go:
IdentityPermissionsstruct:Groups []string+SudoersRule string; holds the resolved Linux permissions for one connecting identityPermissionsConfigstruct:Permissions map[string]IdentityPermissions; parsed from aziti-ssh-host.v1Ziti service config attached to a bound service(*PermissionsConfig).Resolve(zitiIdentity string, globalGroups []string, globalSudoersRule string) IdentityPermissions: returns the config entry for the identity if present (globals ignored), otherwise returns globals; handles nil receiver
UserManager changes:
NewUserManagersignature changed:sudoersRule stringparameter removed; permissions are now passed per-callEnsureUser(username string, perms IdentityPermissions) error: accepts resolved permissions for this connection; on first session callsusermod -aG <groups> <username>whenperms.Groupsis non-empty (non-fatal logged error if a group does not exist), and writes/etc/sudoers.d/<username>whenperms.SudoersRuleis non-empty; subsequent sessions for the same username skip account setup (ref-count only)ReleaseUsernow always attemptsremoveSudoers(was conditioned onm.sudoersRule != ""; idempotent with missing file)
run subcommand changes:
--ssh-serviceflag changed fromStringVartoStringArrayVar; may be specified multiple times;ZITI_SSH_SERVICEenv var still supported as a comma-separated list; fallback to"ssh"runProxynow acceptssshServices []stringand opens onezitiEdge.Listenerper service; all listeners share oneUserManagerand one connection-levelsync.WaitGroup- Ziti context initialised via
ziti.NewConfigFromFile+ziti.NewContext(rather thanziti.NewContextFromFile) so thatcfg.ConfigTypescan include"ziti-ssh-host.v1"beforeAuthenticate - For each service,
loadPermissionsConfigfetches and parses theziti-ssh-host.v1config usingzitiEdge.ParseServiceConfig; stores it in aserviceStatewith anRWMutexfor safe concurrent reads and hot-reload writes zitiCtx.Events().AddServiceChangedListenersubscribed after listeners open; on a changed event for a bound service, re-fetches and atomically swaps the in-memory*PermissionsConfig; logged at info level; existing sessions unaffectedZITI_SSH_GROUPSenv var read at startup; parsed as comma-separated group names (whitespace-trimmed, empty strings skipped); passed asglobalGroupsto each service'sProxyHooks- Signal handler closes all listeners on
SIGTERM/SIGINT; per-connectionsync.WaitGroup(connWg) passed to eachhost.Proxycall for graceful drain
New inspect subcommand:
ziti-ssh-host inspect --service <name> [--service <name> ...]- Authenticates with the same identity (same
--identity/ZITI_IDENTITY) and"ziti-ssh-host.v1"inConfigTypes - For each named service: checks visibility (bind policy), parses
ziti-ssh-host.v1config, prints a formatted table of Ziti identity → derived Linux username → groups → sudoers rule - Prints global fallback values (
ZITI_SSH_GROUPS,ZITI_SUDOERS_RULE) alongside each service block - Exits without opening any listeners; safe to run while
runis active on the same identity
Tests added (host/host_test.go):
TestResolve_*: six table-driven cases covering nil config, empty config, matched identity, unmatched identity, case-sensitive matching, globals-not-merged-for-matched-entry, no-globals caseTestEnsureUser_AcceptsIdentityPermissions: integration test (skipped without root) verifying newIdentityPermissionsparameterTestEnsureUser_RefCounting: integration test (skipped without root) verifying session ref-count and deletion on last releaseTestNewUserManager_Signature: compile-time signature check embedded as runtime test
--ziti-timeout/ZITI_TIMEOUTpersistent flag added to all four binaries (ziti-ssh,ziti-ssh-ca,ziti-ssh-host,ziti-scp), following the existingconfig.EnvOrFlagpattern; accepts anytime.Durationstring (e.g.30s,1m); default30s; must be > 0config.RunWithTimeouthelper added toconfig/config.go: runs afunc() errorin a goroutine with atime.Aftertimeout; returns a clear user-facing error on timeout (not a raw context deadline error)config.ZitiTimeoutErrreturns the standard timeout error message:timed out after <duration> waiting for Ziti network during <op> — check that the controller is reachable and the identity is validzitiCtx.Authenticate()wrapped withconfig.RunWithTimeoutin all four binarieszitiCtx.Listen()/zitiCtx.ListenWithOptions()wrapped withconfig.RunWithTimeoutinziti-ssh-caandziti-ssh-hostzitiCtx.Dial()/zitiCtx.DialWithOptions()replaced withzitiCtx.DialContext()/zitiCtx.DialContextWithOptions()(context-aware SDK variants) inziti-sshandziti-scp; context is acontext.WithTimeoutderived from the configured duration; a clear timeout error is returned if the context deadline firesdocs/configuration.mdupdated with--ziti-timeout/ZITI_TIMEOUTin all four binary sections; newziti-scpsection added
- New binary at
cmd/ziti-scp/main.gomirroringscp(1)behaviour over the Ziti overlay using the SFTP subsystem - Parses
[user@]host:pathremote specs and bare local paths from positional arguments; the last argument is always the destination (same convention asscpandrsync) - Upload (local → remote) and download (remote → local) in a single binary; direction is determined by which side carries the
host:pathform -r/--recursiveflag for recursive directory copy-p/--preserveflag to copy file timestamps and permissions (uses SFTPChtimeson remote,os.Chtimeslocally)-q/--quietflag to suppress per-file progress output- scp-style progress reporting to stderr: filename, percentage, bytes transferred, transfer rate, and ETA; updates at 500ms intervals, final line at 100%
- Same cert auto-refresh logic as
ziti-ssh connect: callsclient.CertNeedsRefreshand auto-signs via the CA when fewer than 5 minutes of validity remain - Same Ziti service resolution: checks the service list for a direct service-name match, falls back to terminator address on
--ssh-service - Same config file (
~/.config/ziti-ssh/config.yaml) and same flag/env/default precedence viaconfig.EnvOrFlag enrollsubcommand for identity enrollment (mirrorsziti-ssh enroll; writes identity JSON to~/.config/ziti-ssh/<name>.jsonby default)- Flags:
--identity/ZITI_IDENTITY,--ca-service/ZITI_CA_SERVICE,--ssh-service/ZITI_SSH_SERVICE,--key,--config
RunSFTP(conn, user, host, signer, isUpload, localPaths, remotePath, recursive, preserve, quiet) erroradded to theclientpackage- Establishes an SSH client connection over the pre-dialled
net.Conn(samessh.ClientConfigpattern asRunSession/RunCommand, withInsecureIgnoreHostKey— host identity is proven by Ziti mTLS) - Opens SFTP subsystem via
sftp.NewClient(sshClient)fromgithub.com/pkg/sftp v1.13.10 - Upload path: walks local paths with
os.ReadDir; creates remote directories withsftp.MkdirAll; writes files withsftp.OpenFile(..., O_WRONLY|O_CREATE|O_TRUNC); applies chmod/chtimes whenpreserveis set - Download path: reads remote directories with
sftpClient.ReadDir; creates local directories withos.MkdirAll; writes files withos.OpenFile(..., O_WRONLY|O_CREATE|O_TRUNC); appliesos.Chtimeswhenpreserveis set progressWriterwraps the destinationio.Writer, tracks bytes written, and prints rate/ETA to stderr at 500ms intervals;printFinalProgressemits the 100% completion line- Helper functions:
formatBytes(SI prefixes),formatBytesRate,formatDuration
github.com/pkg/sftp v1.13.10added as a direct dependency (go.mod);github.com/kr/fs v0.1.0added as indirectscripts/build-deb.shextended with aziti-scpbuild step and a newziti-scpDebian package section (control file, postinst); binary copied to repo root alongside the other threedist/ziti-scp_<version>_amd64.debproduced by the build script
- README: four-binary introduction,
go buildexample updated, new "Copying files withziti-scp" section with upload/download/recursive examples, flags table, config file note, andenrollsubcommand note; project structure updated - CLAUDE.md: solution updated to four binaries;
ziti-sshandziti-scpdesign rationale section;client/sftp.goadded to component descriptions; project structure diagram updated
client.RunCommand(conn, user, host, command, signer)added toclient/ssh.go: sets up the SSH client connection identically toRunSessionbut does not request a PTY; wiresos.Stdin/os.Stdout/os.Stderrdirectly; executes the command viasession.Run; propagates the remote exit code viaos.Exitwhen the error is*ssh.ExitError, and returns other errors normallyconnectParams.commandfield added;runConnectbranches on whethercommandis non-empty — callsclient.RunCommandif so,client.RunSessionotherwiseconnectcobra command updated fromcobra.ExactArgs(1)tocobra.MinimumNArgs(1):args[0]remains the target; any remaining args are joined with spaces and used as the remote command- Root command updated from
cobra.MaximumNArgs(1)tocobra.ArbitraryArgsso bare invocations such asziti-ssh alice@host -- ls -laalso work UseandLonghelp text on both the root command andconnectsubcommand updated to document the new[-- <command> [args...]]form- README "Connecting to a host" section extended with a "Non-interactive command execution" subsection covering usage, piping, and exit-code propagation
- Both binaries now import
github.com/coreos/go-systemd/v22/daemon(promoted from indirect to direct dependency, upgraded to v22.7.0) ziti-ssh-ca: callsdaemon.SdNotify(false, daemon.SdNotifyReady)immediately after the Ziti listener is bound; callsdaemon.SdNotify(false, "STOPPING=1")in the signal watcher goroutine before closing the listenerziti-ssh-host run: same pattern —READY=1afterListenWithOptionssucceeds,STOPPING=1when SIGTERM/SIGINT is received- Both calls are no-ops when
NOTIFY_SOCKETis unset (i.e. when not running under systemd); errors are logged at Debug level only - Systemd unit file examples in README updated from
Type=simpletoType=notify
- New OIDC authentication section covering: how the browser flow works, what to provision on the Ziti controller side (ext-jwt-signer + auth policy), and how to configure
oidc.*in the config file or via--oidc-issuer - Config file example updated to show the
oidc:block with all four fields (issuer,client_id,client_secret,callback_port) --oidc-issuerdescription in the configuration reference table corrected (was incorrectly marked "not yet implemented")
--rate-limit/ZITI_RATE_LIMITand--rate-burst/ZITI_RATE_BURSTadded to theziti-ssh-caconfiguration reference table
- New Graceful shutdown section describing the 30-second drain window and
Type=notifysupport
internal/ratelimit: new package providing a per-identity token-bucket rate limiter backed bygolang.org/x/time/rate- Each Ziti identity gets an independent
rate.Limiter; one abusive caller cannot exhaust the allowance of any other identity - Idle entries are evicted by a background goroutine after 10 minutes of inactivity, bounding memory growth over long uptimes; the goroutine is stopped cleanly on graceful shutdown via a stop channel
- Rate limiting applies only to cert signing requests; CA public key fetches (empty-line requests) are not rate-limited
- When a request is denied, the client receives
error: rate limit exceeded\nand aWarn-level log line records the identity name server-side - Two new flags and env vars following the existing
EnvOrFlagpattern:--rate-limit/ZITI_RATE_LIMIT: maximum cert signing requests per minute per identity (default5; accepts decimal values for sub-minute rates)--rate-burst/ZITI_RATE_BURST: burst allowance (default3)
golang.org/x/time v0.12.0promoted from transitive to direct dependency ingo.mod- Unit tests in
internal/ratelimit/ratelimit_test.go: within-burst, burst-exhaustion, per-identity isolation, and idle-eviction cases
- Browser-based OIDC authorization code flow (with PKCE when no client secret is set) implemented in
cmd/ziti-ssh/oidc.go runOIDCFlowstarts a local HTTP server on the callback port, opens the browser, and blocks until the user completes authentication or the 2-minute timeout elapsesaddOIDCCredentialswires the resulting access token into the Ziti context viaGetCredentials().AddJWT()beforeAuthenticate()— satisfies ext-jwt-signer secondary authentication on the controller- Both
connectandsigncommands run the OIDC flow whenoidc.issueris configured (config file) or--oidc-issueris passed; when no issuer is set the behavior is unchanged - OIDC parameters (
issuer,client_id,client_secret,callback_port) are read from the existingoidc.*config file block;callback_portdefaults to63275 --oidc-issuerflag onconnectoverridesoidc.issuerfrom the config file- Dependencies promoted from indirect:
github.com/gorilla/securecookie,github.com/zitadel/oidc/v3
- Both binaries now catch
SIGTERMandSIGINTviasignal.Notify - On signal: the Ziti listener is closed, stopping new connections from being accepted
- In-flight connections are tracked with a
sync.WaitGroupand allowed up to 30 seconds to finish before the process exits ziti-ssh-ca: WaitGroup wraps eachhandleConngoroutine; drain happens inrun()after the accept loop exitsziti-ssh-host:host.Proxygains an optional*sync.WaitGroupparameter (nil-safe);runProxypasses a WaitGroup and drains it after the listener closes; signal handling is wired inrunProxy- Per-identity mode: active sessions call
OnDisconnectvia the existingProxyHookswhen connections close naturally during the drain window; orphaned users (if the drain timeout fires) are cleaned up byCleanupOrphanson the next startup
- Binds to a named Ziti service (default:
ssh-ca) and signs short-lived SSH certificates for authorized callers - Extracts caller Ziti identity from the mTLS connection via SPIFFE ID (cryptographically verified — not self-reported)
- Issues certificates with configurable principal (default:
ziggy) and configurable TTL via--cert-ttl/ZITI_CERT_TTL(default:8h) - Embeds Ziti identity name as certificate
KeyId(ziti:<identity>) for per-identity audit trail in/var/log/auth.log - Serves CA public key on empty request — allows hosts and clients to fetch the public key over Ziti
--mode shared|per-identity(ZITI_SSH_MODE): inper-identitymode, derives a Linux username from the Ziti identity (DeriveUsername) and uses it as the cert principal- Configured via
/etc/ziti-ssh-ca/env(installed by.debpackage)
enroll --jwt <path>: enrolls a Ziti identity from a JWT, extracts the intermediate CA public key from the enrollment certificate chain, writes/etc/ssh/sshd_config.d/ziti-ssh.confand/etc/ssh/ziti_ca.pub, reloads sshdrun: binds thesshZiti service with identity name as addressable terminator, proxies inbound connections to127.0.0.1:22- Per-identity mode (
ZITI_SSH_MODE=per-identity): creates ephemeral Linux accounts on connect viauseradd -m -s /bin/bash, removes them on disconnect vialoginctl terminate-user→ process polling →userdel -r ZITI_SUDOERS_RULE: optional sudoers rule written to/etc/sudoers.d/<username>on connect; validated withvisudo -cbefore installation; removed on disconnectZITI_USER_CLEANUP=true: deletes user accounts on disconnect; default isfalse(accounts kept); orphan cleanup always runs on startup- Reference-counted
UserManager— safe for concurrent sessions from the same identity; state persisted at/var/lib/ziti-ssh-host/managed-usersfor crash recovery - Configured via
/etc/ziti-ssh-host/env(installed by.debpackage)
connect [user@]<target>(default command): opens an interactive SSH session over Ziti; auto-runssignif cert is missing or expires within 5 minutes- Target resolution:
--servicefor explicit service name; otherwise checks service list for exact match, falls back to terminator address on--ssh-service sign: obtains a signed SSH certificate fromziti-ssh-ca; discovers SSH keys in standard order (id_ed25519,id_ecdsa,id_rsa); prompts to generate viassh-keygenif none found; writes cert to~/.ssh/<key>-cert.pub; displays cert details viassh-keygen -Lenroll --jwt <path>: enrolls a Ziti identity from a JWTlist: lists Ziti services accessible to the current identitymfa enable/verify/remove: manages TOTP MFA on the Ziti identity- SSH agent fallback: passphrase-protected keys fall back to
SSH_AUTH_SOCK; agent signer is wrapped with cert viassh.NewCertSigner - Config file at
~/.config/ziti-ssh/config.yaml(XDG_CONFIG_HOME respected); three-tier precedence: CLI flag > config file > default
ca/ca.go:LoadKey,SignCert,PublicKeyBytes,DeriveUsername(identity → Linux username sanitization: lowercase, replace invalid chars, prefixzif digit-leading, truncate to 32 chars)host/host.go:ProxywithProxyHooks,UserManager,WriteSSHConfig,ReloadSSHD,createSudoers,removeSudoersclient/ssh.go:NewCertSigner(key + cert loading with agent fallback),CertNeedsRefresh,RunSession(PTY + interactive shell)config/config.go:EnvOrFlag— flag > env var > default- Debian packages for all three binaries built by
scripts/build-deb.sh; binaries also copied to repo root for local testing - Unit tests:
ca/ca_test.go—SignCertfield validation,DeriveUsernamesanitization rules (15 cases)
- Ziti identity name is cryptographically verified via SPIFFE ID in the mTLS certificate (not self-reported by the client SDK)
- CA key is the Ziti controller's intermediate CA private key — co-located deployment, no credential distribution required
- SSH host stores only the intermediate CA public key (
TrustedUserCAKeys) — not a credential, cannot authenticate to anything - Certificates are short-lived (default 8h); no revocation infrastructure required
- Port 22 is never exposed externally; all SSH traffic flows through the Ziti overlay