commit 7150a6dcd9660f5f2340a6f4aa5a56fae820c453 Author: William Mantly Date: Sun Aug 2 01:33:38 2026 -0400 feat: initial theta-agent repository structure and security model diff --git a/README.md b/README.md new file mode 100644 index 0000000..10f47c2 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Theta Agent + +Theta Agent is a unified endpoint management daemon for the theta42 stack. It replaces legacy bash installation scripts (like `ldap-client`) and one-way metric scripts (`telemetry-agent`) with a powerful, 2-way Command & Control (C2) Go daemon. + +The agent dials out to the central SSO Manager via a persistent WebSocket connection, enabling: +- **Continuous Telemetry:** Streams CPU/RAM/ZFS/GPU health to the central inventory. +- **Dynamic Discovery:** Automatically updates host IP and metadata on changes. +- **Remote Operations:** Allows SSO Manager administrators to remotely configure LDAP, restart systemd services, or execute maintenance scripts. + +## The Security Model (Blast Radius & Zero-Trust) + +Because Theta Agent runs as `root` (required to configure `/etc/sssd/sssd.conf`, restart services, and read hardware sensors), it represents a high-value target. If the central SSO Manager were compromised, a naive agent would allow an attacker to gain root shell execution on every server in the fleet. + +To prevent lateral movement and contain the blast radius, **Theta Agent operates on a strict, local-first capability matrix.** + +### 1. Local Configuration Wins +The agent will **only** execute commands that are explicitly enabled in its local configuration file (`/etc/theta/agent.yml`). +- By default, the agent is locked down to read-only telemetry and basic LDAP configuration. +- The central SSO Manager cannot override these settings. An administrator must physically (or via local config management) edit the local `agent.yml` file to grant the agent more permissions. + +### 2. The Capability Matrix +Capabilities are segmented into modules. You only enable what a specific server needs: + +| Capability | Risk Level | Description | +|------------|------------|-------------| +| `telemetry` | Safe | Read-only. Pushes system metrics back to the SSO Manager. | +| `configure_ldap` | Moderate | Allows the SSO manager to push down an updated SSSD configuration file. | +| `reboot` | High | Allows the SSO Manager to trigger a system reboot. | +| `service_control` | High | Allows starting/stopping/restarting systemd services. **Must be scoped** to specific services (e.g., `['gitea', 'nginx']`). | +| `arbitrary_bash` | CRITICAL | Allows the execution of raw bash scripts sent from the SSO Manager. Useful for GitOps deployments on worker nodes, but highly dangerous. | + +### 3. Outbound-Only Communication +The agent does not open any listening ports on the host firewall. It uses a long-lived outbound WebSocket connection to the SSO Manager. + +### 4. Cryptographic Authentication +Every agent is issued a unique, long-lived host token during installation. The SSO Manager verifies this token to ensure commands are only routed to the intended host, and telemetry is properly attributed. + +## Example Configuration + +See `agent.yml.example` for a secure baseline configuration. + +## Installation + +*(Coming soon: Build instructions and `theta-agent install` guide)* diff --git a/agent.yml.example b/agent.yml.example new file mode 100644 index 0000000..7e4066f --- /dev/null +++ b/agent.yml.example @@ -0,0 +1,34 @@ +# theta-agent configuration file +# Default location: /etc/theta/agent.yml + +server_url: "https://sso.example.com" +auth_token: "REPLACE_WITH_AGENT_TOKEN" +location: "default" # Location identifier (e.g., site, datacenter) for naming + +capabilities: + # --------------------------------------------------------- + # Basic Capabilities (Safe, read-only or infrastructure management) + # --------------------------------------------------------- + + # Push CPU, RAM, GPU, and ZFS metrics to the SSO Manager + telemetry: true + + # Allow the SSO Manager to push down SSSD and SSH keys configuration + configure_ldap: true + + # --------------------------------------------------------- + # Advanced Capabilities (High risk, remote operations) + # --------------------------------------------------------- + + # Allow remote system reboots via the SSO Manager + reboot: false + + # Allow restarting, starting, or stopping specific systemd services. + # Must be an explicit list of allowed service names. + # Example: ["gitea", "nginx", "docker"] + # Setting to true or [] denies all. + service_control: [] + + # CRITICAL: Allow the execution of raw bash scripts sent from the SSO Manager. + # Useful for GitOps deployments, but allows remote code execution. + arbitrary_bash: false diff --git a/config.go b/config.go new file mode 100644 index 0000000..53905ac --- /dev/null +++ b/config.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type Capabilities struct { + Telemetry bool `yaml:"telemetry"` + ConfigureLDAP bool `yaml:"configure_ldap"` + Reboot bool `yaml:"reboot"` + ServiceControl []string `yaml:"service_control"` + ArbitraryBash bool `yaml:"arbitrary_bash"` +} + +type Config struct { + ServerURL string `yaml:"server_url"` + AuthToken string `yaml:"auth_token"` + Location string `yaml:"location"` + Capabilities Capabilities `yaml:"capabilities"` +} + +func LoadConfig(path string) (*Config, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open config file: %w", err) + } + defer file.Close() + + var cfg Config + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(&cfg); err != nil { + return nil, fmt.Errorf("failed to decode YAML config: %w", err) + } + + return &cfg, nil +} + +// CanManageService checks if a specific service is permitted to be restarted/stopped +func (c *Capabilities) CanManageService(serviceName string) bool { + for _, allowed := range c.ServiceControl { + if allowed == serviceName { + return true + } + } + return false +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dfe7619 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/theta42/theta-agent + +go 1.22.2 + +require gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4bc0337 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8bbc5b8 --- /dev/null +++ b/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" +) + +func main() { + log.Println("Starting Theta Agent...") + + // Attempt to load configuration + configPath := "/etc/theta/agent.yml" + if len(os.Args) > 1 { + configPath = os.Args[1] + } + + cfg, err := LoadConfig(configPath) + if err != nil { + log.Fatalf("Error loading configuration from %s: %v", configPath, err) + } + + log.Printf("Connecting to SSO Manager at %s", cfg.ServerURL) + log.Printf("Loaded capabilities: Telemetry=%v, LDAP=%v, Reboot=%v, Bash=%v", + cfg.Capabilities.Telemetry, + cfg.Capabilities.ConfigureLDAP, + cfg.Capabilities.Reboot, + cfg.Capabilities.ArbitraryBash, + ) + + // TODO: Initialize WebSocket connection to SSO Manager + // TODO: Start telemetry background loop if capabilities.Telemetry == true + + // Block until signal is received + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + + fmt.Println("Shutting down Theta Agent...") +} diff --git a/theta-agent b/theta-agent new file mode 100755 index 0000000..f6936a7 Binary files /dev/null and b/theta-agent differ