package production import ( "fmt" "os" "os/exec" "path/filepath" "strings" ) // oramaServiceHardening contains common systemd security directives for orama services. const oramaServiceHardening = `User=orama Group=orama ProtectSystem=strict ProtectHome=yes NoNewPrivileges=yes PrivateDevices=yes ProtectKernelTunables=yes ProtectKernelModules=yes RestrictNamespaces=yes` // SystemdServiceGenerator generates systemd unit files type SystemdServiceGenerator struct { oramaHome string oramaDir string } // NewSystemdServiceGenerator creates a new service generator func NewSystemdServiceGenerator(oramaHome, oramaDir string) *SystemdServiceGenerator { return &SystemdServiceGenerator{ oramaHome: oramaHome, oramaDir: oramaDir, } } // GenerateIPFSService generates the IPFS daemon systemd unit func (ssg *SystemdServiceGenerator) GenerateIPFSService(ipfsBinary string) string { ipfsRepoPath := filepath.Join(ssg.oramaDir, "data", "ipfs", "repo") logFile := filepath.Join(ssg.oramaDir, "logs", "ipfs.log") return fmt.Sprintf(`[Unit] Description=IPFS Daemon After=network-online.target Wants=network-online.target [Service] Type=simple %[6]s ReadWritePaths=%[3]s Environment=HOME=%[1]s Environment=IPFS_PATH=%[2]s ExecStartPre=/bin/bash -c 'if [ -f %[3]s/secrets/swarm.key ] && [ ! -f %[2]s/swarm.key ]; then cp %[3]s/secrets/swarm.key %[2]s/swarm.key && chmod 600 %[2]s/swarm.key; fi' ExecStart=%[5]s daemon --enable-pubsub-experiment --repo-dir=%[2]s Restart=always RestartSec=5 StandardOutput=append:%[4]s StandardError=append:%[4]s SyslogIdentifier=orama-ipfs PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=4G [Install] WantedBy=multi-user.target `, ssg.oramaHome, ipfsRepoPath, ssg.oramaDir, logFile, ipfsBinary, oramaServiceHardening) } // GenerateIPFSClusterService generates the IPFS Cluster systemd unit func (ssg *SystemdServiceGenerator) GenerateIPFSClusterService(clusterBinary string) string { clusterPath := filepath.Join(ssg.oramaDir, "data", "ipfs-cluster") logFile := filepath.Join(ssg.oramaDir, "logs", "ipfs-cluster.log") // Read cluster secret from file to pass to daemon clusterSecretPath := filepath.Join(ssg.oramaDir, "secrets", "cluster-secret") clusterSecret := "" if data, err := os.ReadFile(clusterSecretPath); err == nil { clusterSecret = strings.TrimSpace(string(data)) } return fmt.Sprintf(`[Unit] Description=IPFS Cluster Service After=orama-ipfs.service Wants=orama-ipfs.service Requires=orama-ipfs.service [Service] Type=simple %[6]s ReadWritePaths=%[7]s WorkingDirectory=%[1]s Environment=HOME=%[1]s Environment=IPFS_CLUSTER_PATH=%[2]s Environment=CLUSTER_SECRET=%[5]s ExecStartPre=/bin/bash -c 'mkdir -p %[2]s && chmod 700 %[2]s' ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf -X POST http://127.0.0.1:4501/api/v0/id > /dev/null 2>&1 && exit 0; sleep 1; done; echo "IPFS API not ready after 30s"; exit 1' ExecStart=%[4]s daemon Restart=always RestartSec=5 StandardOutput=append:%[3]s StandardError=append:%[3]s SyslogIdentifier=orama-ipfs-cluster PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=2G [Install] WantedBy=multi-user.target `, ssg.oramaHome, clusterPath, logFile, clusterBinary, clusterSecret, oramaServiceHardening, ssg.oramaDir) } // GenerateRQLiteService generates the RQLite systemd unit func (ssg *SystemdServiceGenerator) GenerateRQLiteService(rqliteBinary string, httpPort, raftPort int, joinAddr string, advertiseIP string) string { dataDir := filepath.Join(ssg.oramaDir, "data", "rqlite") logFile := filepath.Join(ssg.oramaDir, "logs", "rqlite.log") // Use public IP for advertise if provided, otherwise default to localhost if advertiseIP == "" { advertiseIP = "127.0.0.1" } // Bind RQLite to localhost only - external access via SNI gateway args := fmt.Sprintf( `-http-addr 127.0.0.1:%d -http-adv-addr %s:%d -raft-adv-addr %s:%d -raft-addr 127.0.0.1:%d`, httpPort, advertiseIP, httpPort, advertiseIP, raftPort, raftPort, ) if joinAddr != "" { args += fmt.Sprintf(` -join %s -join-attempts 30 -join-interval 10s`, joinAddr) } args += fmt.Sprintf(` %s`, dataDir) return fmt.Sprintf(`[Unit] Description=RQLite Database After=network-online.target Wants=network-online.target [Service] Type=simple %[6]s ReadWritePaths=%[7]s Environment=HOME=%[1]s ExecStart=%[5]s %[2]s Restart=always RestartSec=5 StandardOutput=append:%[3]s StandardError=append:%[3]s SyslogIdentifier=orama-rqlite PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed [Install] WantedBy=multi-user.target `, ssg.oramaHome, args, logFile, dataDir, rqliteBinary, oramaServiceHardening, ssg.oramaDir) } // GenerateOlricService generates the Olric systemd unit func (ssg *SystemdServiceGenerator) GenerateOlricService(olricBinary string) string { olricConfigPath := filepath.Join(ssg.oramaDir, "configs", "olric", "config.yaml") logFile := filepath.Join(ssg.oramaDir, "logs", "olric.log") return fmt.Sprintf(`[Unit] Description=Olric Cache Server After=network-online.target Wants=network-online.target [Service] Type=simple %[6]s ReadWritePaths=%[4]s Environment=HOME=%[1]s Environment=OLRIC_SERVER_CONFIG=%[2]s ExecStart=%[5]s Restart=always RestartSec=5 StandardOutput=append:%[3]s StandardError=append:%[3]s SyslogIdentifier=olric PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=4G [Install] WantedBy=multi-user.target `, ssg.oramaHome, olricConfigPath, logFile, ssg.oramaDir, olricBinary, oramaServiceHardening) } // GenerateNodeService generates the Orama Node systemd unit func (ssg *SystemdServiceGenerator) GenerateNodeService() string { configFile := "node.yaml" logFile := filepath.Join(ssg.oramaDir, "logs", "node.log") // Note: systemd StandardOutput/StandardError paths should not contain substitution variables // Use absolute paths directly as they will be resolved by systemd at runtime return fmt.Sprintf(`[Unit] Description=Orama Network Node After=orama-ipfs-cluster.service orama-olric.service wg-quick@wg0.service Wants=orama-ipfs-cluster.service orama-olric.service Requires=wg-quick@wg0.service [Service] Type=simple %[5]s ReadWritePaths=%[2]s WorkingDirectory=%[1]s Environment=HOME=%[1]s ExecStart=%[1]s/bin/orama-node --config %[2]s/configs/%[3]s Restart=always RestartSec=5 StandardOutput=append:%[4]s StandardError=append:%[4]s SyslogIdentifier=orama-node PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=8G OOMScoreAdjust=-500 [Install] WantedBy=multi-user.target `, ssg.oramaHome, ssg.oramaDir, configFile, logFile, oramaServiceHardening) } // GenerateVaultService generates the Orama Vault Guardian systemd unit. // The vault guardian runs on every node, storing Shamir secret shares. // It binds to the WireGuard overlay only (no public exposure). func (ssg *SystemdServiceGenerator) GenerateVaultService() string { logFile := filepath.Join(ssg.oramaDir, "logs", "vault.log") dataDir := filepath.Join(ssg.oramaDir, "data", "vault") return fmt.Sprintf(`[Unit] Description=Orama Vault Guardian After=network-online.target wg-quick@wg0.service Wants=network-online.target Requires=wg-quick@wg0.service PartOf=orama-node.service [Service] Type=simple User=orama Group=orama ProtectSystem=strict ProtectHome=yes NoNewPrivileges=yes PrivateDevices=yes ProtectKernelTunables=yes ProtectKernelModules=yes RestrictNamespaces=yes ReadWritePaths=%[2]s ExecStart=%[1]s/bin/vault-guardian --config %[2]s/vault.yaml Restart=on-failure RestartSec=5 StandardOutput=append:%[3]s StandardError=append:%[3]s SyslogIdentifier=orama-vault PrivateTmp=yes LimitMEMLOCK=67108864 MemoryMax=512M TimeoutStopSec=30 KillMode=mixed [Install] WantedBy=multi-user.target `, ssg.oramaHome, dataDir, logFile) } // GenerateGatewayService generates the Orama Gateway systemd unit func (ssg *SystemdServiceGenerator) GenerateGatewayService() string { logFile := filepath.Join(ssg.oramaDir, "logs", "gateway.log") return fmt.Sprintf(`[Unit] Description=Orama Gateway After=orama-node.service orama-olric.service Wants=orama-node.service orama-olric.service [Service] Type=simple %[4]s ReadWritePaths=%[2]s WorkingDirectory=%[1]s Environment=HOME=%[1]s ExecStart=%[1]s/bin/gateway --config %[2]s/data/gateway.yaml Restart=always RestartSec=5 StandardOutput=append:%[3]s StandardError=append:%[3]s SyslogIdentifier=orama-gateway PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=4G [Install] WantedBy=multi-user.target `, ssg.oramaHome, ssg.oramaDir, logFile, oramaServiceHardening) } // GenerateAnyoneClientService generates the Anyone Client SOCKS5 proxy systemd unit. // Uses the same anon binary as the relay, but with a client-only config (SocksPort only, no relay). func (ssg *SystemdServiceGenerator) GenerateAnyoneClientService() string { logFile := filepath.Join(ssg.oramaDir, "logs", "anyone-client.log") return fmt.Sprintf(`[Unit] Description=Anyone Client SOCKS5 Proxy After=network-online.target Wants=network-online.target [Service] Type=simple User=debian-anon Group=debian-anon ExecStart=/usr/bin/anon -f /etc/anon/anonrc Restart=on-failure RestartSec=5 StandardOutput=append:%[1]s StandardError=append:%[1]s SyslogIdentifier=anyone-client PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=1G [Install] WantedBy=multi-user.target `, logFile) } // GenerateAnyoneRelayService generates the Anyone Relay operator systemd unit // Uses debian-anon user created by the anon apt package func (ssg *SystemdServiceGenerator) GenerateAnyoneRelayService() string { return `[Unit] Description=Anyone Relay (Orama Network) Documentation=https://docs.anyone.io After=network-online.target Wants=network-online.target [Service] Type=simple User=debian-anon Group=debian-anon ExecStart=/usr/bin/anon --agree-to-terms Restart=always RestartSec=10 SyslogIdentifier=anon-relay # Security hardening NoNewPrivileges=yes ProtectSystem=full ProtectHome=read-only PrivateTmp=yes ProtectKernelTunables=yes ProtectKernelModules=yes RestrictRealtime=yes RestrictSUIDSGID=yes ReadWritePaths=/var/lib/anon /var/log/anon /etc/anon LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=2G [Install] WantedBy=multi-user.target ` } // GenerateCoreDNSService generates the CoreDNS systemd unit func (ssg *SystemdServiceGenerator) GenerateCoreDNSService() string { return fmt.Sprintf(`[Unit] Description=CoreDNS DNS Server with RQLite backend Documentation=https://coredns.io After=network-online.target orama-node.service Wants=network-online.target orama-node.service [Service] Type=simple %[1]s ReadWritePaths=%[2]s AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE ExecStart=/usr/local/bin/coredns -conf /etc/coredns/Corefile Restart=on-failure RestartSec=5 SyslogIdentifier=coredns PrivateTmp=yes LimitNOFILE=65536 TimeoutStopSec=30 KillMode=mixed MemoryMax=1G [Install] WantedBy=multi-user.target `, oramaServiceHardening, ssg.oramaDir) } // GenerateCaddyService generates the Caddy systemd unit for SSL/TLS func (ssg *SystemdServiceGenerator) GenerateCaddyService() string { return fmt.Sprintf(`[Unit] Description=Caddy HTTP/2 Server Documentation=https://caddyserver.com/docs/ After=network-online.target orama-node.service coredns.service Wants=network-online.target Wants=orama-node.service [Service] Type=simple %[1]s ReadWritePaths=%[2]s /var/lib/caddy /etc/caddy AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile TimeoutStopSec=5s LimitNOFILE=1048576 LimitNPROC=512 PrivateTmp=true Restart=on-failure RestartSec=5 SyslogIdentifier=caddy KillMode=mixed MemoryMax=2G [Install] WantedBy=multi-user.target `, oramaServiceHardening, ssg.oramaDir) } // SystemdController manages systemd service operations type SystemdController struct { systemdDir string } // NewSystemdController creates a new controller func NewSystemdController() *SystemdController { return &SystemdController{ systemdDir: "/etc/systemd/system", } } // WriteServiceUnit writes a systemd unit file func (sc *SystemdController) WriteServiceUnit(name string, content string) error { unitPath := filepath.Join(sc.systemdDir, name) if err := os.WriteFile(unitPath, []byte(content), 0644); err != nil { return fmt.Errorf("failed to write unit file %s: %w", name, err) } return nil } // DaemonReload reloads the systemd daemon func (sc *SystemdController) DaemonReload() error { cmd := exec.Command("systemctl", "daemon-reload") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to reload systemd daemon: %w", err) } return nil } // EnableService enables a service to start on boot func (sc *SystemdController) EnableService(name string) error { cmd := exec.Command("systemctl", "enable", name) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to enable service %s: %w", name, err) } return nil } // StartService starts a service immediately func (sc *SystemdController) StartService(name string) error { cmd := exec.Command("systemctl", "start", name) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to start service %s: %w", name, err) } return nil } // RestartService restarts a service func (sc *SystemdController) RestartService(name string) error { cmd := exec.Command("systemctl", "restart", name) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to restart service %s: %w", name, err) } return nil } // StopService stops a service func (sc *SystemdController) StopService(name string) error { cmd := exec.Command("systemctl", "stop", name) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to stop service %s: %w", name, err) } return nil } // DisableService disables a service from starting on boot func (sc *SystemdController) DisableService(name string) error { cmd := exec.Command("systemctl", "disable", name) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to disable service %s: %w", name, err) } return nil } // RemoveServiceUnit removes a systemd unit file from disk func (sc *SystemdController) RemoveServiceUnit(name string) error { unitPath := filepath.Join(sc.systemdDir, name) if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove unit file %s: %w", name, err) } return nil } // StatusService gets the status of a service func (sc *SystemdController) StatusService(name string) (bool, error) { cmd := exec.Command("systemctl", "is-active", "--quiet", name) err := cmd.Run() if err == nil { return true, nil } // Check for "inactive" vs actual error if strings.Contains(err.Error(), "exit status 3") { return false, nil // Service is inactive } return false, fmt.Errorf("failed to check service status %s: %w", name, err) }