Skip to content

Security: AFK-surf/sykit

Security

SECURITY.md

Script security review

Reviewed on 2026-09-08: the shipped C programs, node-key gate, vendored SDK helper use, Dockerfile, shell scripts and CI workflow. Host implementations were consulted to check API contracts. This is a source and regression-test review, not a proof of absence of vulnerabilities or a comprehensive audit of Synchronicity, russh, LLVM, Docker or their dependencies.

Findings addressed

  • SSH parsing: C-string comparison accepted sftp followed by an embedded NUL and extra bytes as the exact subsystem name. Comparisons now use the full JSON string length and bytes. The node-key gate still applied before this request; this was not a node-authentication bypass.
  • SSH resource cleanup: closing a session before selecting its backend retained the connection's only session slot. Receive EOF is now watched before a shell or SFTP service starts, releasing the channel and any PTY. Exploitation required an authorized node and affected its own connection.
  • Artifact integrity: a shared mutable image tag could be replaced by another build before extraction. Extraction now uses the exact immutable image ID returned by this build, with registry pulls disabled.
  • Test helper filesystem handling: checking existence before copying followed dangling symlinks and left a check/write race. Exclusive creation now refuses existing paths atomically. This affected a local test helper, not the deployed socket runtime.

The CI extraction failure was also fixed: objects live in /artifacts so Docker's injected root files do not enter the artifact comparison.

Checks

./scripts/test.sh covers malformed/mismatched origins and z-base-32 node keys, complete allowlist validation, immutable artifact selection, stale artifact detection and dangling-symlink refusal. ./scripts/test-runtime.sh /path/to/synchronicity executes committed BPF objects against the actual runtime. It covers transport identity despite spoofed metadata, binary echo under backpressure, permitted shell/SFTP operations, embedded-NUL subsystem refusal, rejected exec/environment/forwarding requests, one active session, abandoned-session recovery, and SFTP read/write/delete traversal attempts. ./scripts/build.sh --check performs a fresh amd64 build and compares the entire artifact directory byte for byte.

Trust and access

SSH authorization deliberately grants each configured node a shell as the serving daemon's OS account. SSH usernames do not select or authenticate a separate OS account. The SFTP scope does not sandbox that shell. Node-key validation runs before SSH starts; connection metadata never supplies policy.

Runtime admission, authenticated transport, capability enforcement, SFTP path normalization, process cleanup and resource deadlines remain host obligations. Echo and whoami are intentionally available to any caller able to reach their socket. Keep activated script paths, activation configuration, the allowed node's identity and the build host under operator control. Reproducible builds and checksums establish consistency, not the trustworthiness of their source.

allowed_peers accepts up to 16 comma-separated named origins or unpadded z-base-32 public keys. Parsing validates every entry and the final unused key encoding bits before granting access. Origin rules compare the authenticated sy_peer_origin value after normalizing the configured name, and therefore trust the membership authority to associate that name with a device key. Hex and the former allowed_node_key setting are not accepted.

allowlist=<tree path> selects a newline-delimited file instead of inline allowed_peers; configuring both is refused. The server reads one selected object snapshot in full for each new connection, with limits of 64 KiB, 1,024 peer entries, 1,023 bytes per line and 10 seconds for range reads. Failures and malformed later lines deny access even after a match. Native tests inject cold/short reads, timeouts, premature EOF and host errors and check cleanup; runtime tests cover large files, rejected policy and revocation on a new connection. File writers are trusted to grant SSH access. Existing sessions retain their authorization until they end.

The TCP proxy uses the same shared authorization function before opening its upstream. The shipped manifest permits any TCP port on literal 127.0.0.1; operator-controlled upstream_port selects one port, with strict decimal/range validation. Neither upstream selection nor authorization reads caller metadata. Tests run the committed object against loopback TCP to check denied dials, invalid configuration, host egress enforcement, backpressure, both half-close orders and asynchronous connection failure. Local listener clients inherit the forwarding node's identity and receive the selected service's protocol access.

Temporary support devices

ssh-session shares the capabilities and request handling of ssh-shell, with additional absolute-expiry admission and active-session deadline checks. The bootstrap creates a private device identity locally and prints only its public key and registration scope/deadline. It neither accepts nor embeds control-plane credentials and never distributes a device database or private invitation.

An authorized operator registers/revokes the key through the hosted delegation API; the data plane signs the existing delegation record. The socket separately pins the controller's public key. Network admission is not application access, and delegation revocation is not the timer for an existing connection.

The bootstrap checks runtime/program downloads, asks explicit consent before starting, and owns the temporary daemon's normal shutdown and cleanup. See README for clock, direct-process, detached-child and crash-cleanup boundaries. This is authorized remote administration, not sandboxed or exactly-once execution.

There aren't any published security advisories