8f0158eb9f
See CHANGELOG.md for the full breakdown. Summary:
- ldap_tunnel.go: serves a local unix socket for SSSD/PAM and relays raw
bytes to the SSO over the existing WSS channel (ldap_tunnel messages);
the agent never parses LDAP (DESIGN.md §4). Adds safeWriter to
serialize WebSocket writes now that telemetry, heartbeat, the LDAP
tunnel, and command responses all share one connection.
- secrets.go: renders local templates ({{ bao "path#key" }} placeholders)
by fetching node-scoped values from the SSO and writing the target
atomically at 0600, on a signed render_secrets command (DESIGN.md §5).
demo/ has minimal bash + Node consumers of the rendered file.
- iam.go: applies signed node IAM pushes -- sudoers.d rules (visudo -c
validated), SSH AuthorizedKeysCommand keys, /etc/security/access.conf,
and revocation via sss_cache -E + pkill -u (DESIGN.md §6).
- Capability reporting: the agent's enabled capabilities ride along in
its discovery frame so the SSO can show them in the Directory.
- DESIGN.md: the v2 protocol design this implements.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.8 KiB
8.8 KiB
Changelog
All notable changes to the theta-agent daemon will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased] - LDAP byte-pump tunnel (DESIGN.md §4)
The agent now serves a local LDAP socket for SSSD/PAM. It is a pure byte pump: it forwards raw LDAP bytes to the SSO over the WSS channel, and the SSO relays them into its real OpenLDAP and pipes the response back. The agent never parses LDAP.
Added
ldap_tunnelcapability +ldap_socketconfig. When enabled, the agent binds a unix socket (default/run/theta/ldap.sock, root:theta0660) and relays bytes bidirectionally asldap_tunnelmessages over the existing WSS channel. Point SSSD at it withldap_uri = ldapi://%2frun%2ftheta%2fldap.sock.safeWriter— serializes WebSocket writes. Gorilla allows only one concurrent writer, but telemetry, heartbeat, the LDAP tunnel and command responses all write to the same socket; without this, concurrent writes corrupt the stream.- Offline behavior: when the WSS is down the agent cannot forward bytes, so it closes local socket connections; SSSD sees a connection failure and falls back to its local cache.
Added — secrets engine (DESIGN.md §5)
secretscapability +secretsconfig. The agent renders local templates that embed OpenBao secrets ({{ bao "secret/data/nodes/<id>/<name>#<key>" }}). It parses the placeholders, fetches the values from the SSO (which holds the OpenBao access; the agent never holds a Vault token), renders each target atomically at0600, and runs the configured reload. Triggered by a signedrender_secretscommand.
Added — capability reporting
- The agent reports its enabled capabilities in its
discoveryframe. The SSO stores them and the Directory UI shows them as badges on the host's Metrics tab, so an operator can see at a glance what an agent is allowed to do (telemetry, LDAP tunnel, secrets, IAM, reboot, bash, service control).
Added — IAM engine (DESIGN.md §6)
iamcapability. The SSO pushes node-scoped identity config as a signediam_applycommand; the agent verifies the Ed25519 signature (fail-closed) and applies it locally:- Sudo rules — writes
/etc/sudoers.d/theta-iam-<node_id>, validates withvisudo -c, atomic swap. - SSH keys — stores per-user keys and installs the
AuthorizedKeysCommandscript (/usr/local/bin/theta-authorized-keys) that sshd calls per login. - Access control — writes
/etc/security/access.confwith allowed login groups. - Revocation — flushes the SSSD cache (
sss_cache -E) and drops active sessions (pkill -u) for revoked users.
- Sudo rules — writes
[v1.5.1] - 2026-08-06
Fixed
- Rebuilt the prebuilt
theta-agent-linux-amd64. theta-suite'ssetup.shinstalls that committed binary rather than building from source, so a stale one means the fix in this repo never reaches the host. The v1.5.0 binary predated join-key support: an install would have written ajoin_keyintoagent.ymlthat the running agent did not understand, and it would have looped onclose 4001: Unauthorized. (Same trap as the v1.3.0 heartbeat fix.)
[v1.5.0] - 2026-08-06
Join-key enrollment (protocol v1.2.0 §1.1). Installing the agent with one key is now all it takes to add a host.
Added
join_keyconfig field. Presented whileauth_tokenis empty. The SSO exchanges it for this agent's own token and the public key it must pin, both delivered in theconfigframe; the agent writes them intoagent.ymland blanks the join key. No value has to be copied between two machines by hand any more.ConfigManager.PersistEnrollmentrewrites only the credential lines, line-based rather than a YAML round-trip, so operator comments, the capability matrix and formatting survive. Re-reads the file afterwards, so the new credential is live without a restart, and keeps the file at0600.Config.Credential()— the agent's own token when it has one, otherwise the join key.- The connect URL carries
?hostname=, so a self-enrolling host is named after itself instead of a generated placeholder. install.sh --join-key.
Fixed
- The agent now refuses to connect (with a clear message and a long back-off) when it has neither an
auth_tokennor ajoin_key, rather than repeatedly presenting an empty credential.
[v1.4.0] - 2026-08-05
Implements Protocol v1.2.0. See PROTOCOL.md §1.1, §5.1–5.3.
Security
- Fail-closed signature verification.
verifySignaturereturnedtruewhen nopublic_keywas configured, logging "skipping signature verification". Combined with an installer that never wrote apublic_key, that meant a default install would executereboot,service_restart,configure_ldap,arbitrary_bashandupdate_binaryunverified from anything that could reach its socket. An agent that cannot verify a high-risk command now refuses it. - The token must be issued by the server. The SSO now rejects tokens it did not mint (close code
4001). Agents carrying a token generated by the old browser-side installer will not connect until re-enrolled.
Fixed
- Canonicalization mismatch broke signatures for most real scripts. Go's
encoding/jsonescapes<,>and&by default; the server'sJSON.stringifydoes not. Any payload containing them — anarbitrary_bashscript using>redirection or&&, which is most of them — hashed differently on each side and failed verification.canonicalize()now usesjson.EncoderwithSetEscapeHTML(false)and trims the encoder's trailing newline. - Auth failures no longer hot-loop. A rejected credential was retried every 5 seconds forever, flooding the SSO and its audit log. Close codes
4001/4003/4004now back off for 5 minutes and log what to do about it. - The auth token no longer appears in logs. The connect line logged the full URL, including
?token=.... It now logs only host + path, and the token is URL-escaped.
Added
- Close-code handling for the SSO's enrollment signals:
4001unauthorized,4002superseded,4003revoked,4004token rotated. install.sh --public-key <base64>, written into the generatedagent.yml. The installer warns loudly when no public key is configured, since such an agent can report telemetry but will refuse every high-risk command.- Tests: fail-closed with no key, wrong key, payload tampered after signing, shell metacharacters (
>,&&,<) round-tripping, and canonical-form equality with the server.interop_check_test.goverifies a signature produced by the live SSO against the agent's own verifier (skipped unlessINTEROP_FIXTUREis set).
Changed
- Existing tests no longer rely on verification being skipped; high-risk cases now sign with a real test key.
agent.yml.example,README.md,INSTALL.md: enrollment is a prerequisite, andpublic_keyis the base64 of the raw 32-byte Ed25519 key — not a PEM body. The previous documented example (MCowBQYDK2VwAyEA...) decodes to 44 bytes and would have been rejected.
[v1.3.0] - 2026-08-04
Fixed
- Silently ignore
heartbeat_ack— the server replies to the agent's own periodic heartbeat withheartbeat_ack. The agent had no case for it, so it fell through to the unknown-command handler, loggedUnknown command type: heartbeat_ackevery minute, and answered with a spurious error response. Heartbeat acks are fire-and-forget; the agent now ignores them silently.
[v1.2.0] - 2026-08-03
Added
- Protocol v1.1.0 Compliance: Full alignment with
PROTOCOL.md(v1.1.0) specification. - Ed25519 Cryptographic Verification: Verification of Ed25519 Base64 signatures for high-risk C2 commands (
reboot,service_restart,configure_ldap,arbitrary_bash,update_binary). - Enhanced Journal Log Fetcher (
fetch_logs): Support for querying service-specific systemd logs (serviceparameter) with configurable line count (linesparameter). - Pure Go Self-Update Engine (
update_binary): Replaced shell script execution with pure Go HTTP client fetching, SHA256 verification, atomic file replacement, and clean daemon restart.
Fixed
- Goroutine Leak Prevention: Added
stopChlifecycle management to terminate background telemetry and heartbeat tickers upon WebSocket disconnection. - Dynamic Config Rerenders: Resolved data races when toggling capabilities or reloading
agent.ymlviareload_config. - Config Path Uniformity: Standardized canonical configuration file path across codebase, installer, and documentation to
/etc/theta42/agent.yml.
[v0.1.0] - 2026-08-01
Added
- Initial release of
theta-agentGo daemon replacing legacy bash metric scripts. - Persistent outbound WebSocket telemetry and local capability matrix enforcement.