Files
Redflag/aggregator-server/internal/services/install_template_service.go
jpetree331 f97d4845af feat(security): A-1 Ed25519 key rotation + A-2 replay attack fixes
Complete RedFlag codebase with two major security audit implementations.

== A-1: Ed25519 Key Rotation Support ==

Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management

Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing

== A-2: Replay Attack Fixes (F-1 through F-7) ==

F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH     - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH     - Migration 026: expires_at column with partial index
F-6 HIGH     - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH     - Agent-side executedIDs dedup map with cleanup
F-4 HIGH     - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt

Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.

All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:25:47 -04:00

226 lines
6.2 KiB
Go

package services
import (
"bytes"
"embed"
"fmt"
"log"
"strings"
"text/template"
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
"github.com/google/uuid"
)
//go:embed templates/install/scripts/*.tmpl
var installScriptTemplates embed.FS
// InstallTemplateService renders installation scripts from templates
type InstallTemplateService struct{}
// NewInstallTemplateService creates a new template service
func NewInstallTemplateService() *InstallTemplateService {
return &InstallTemplateService{}
}
// RenderInstallScript renders an installation script for the specified platform
func (s *InstallTemplateService) RenderInstallScript(agent *models.Agent, binaryURL, configURL string) (string, error) {
// Define template data
data := struct {
AgentID string
BinaryURL string
ConfigURL string
Platform string
Architecture string
Version string
AgentUser string
AgentHome string
ConfigDir string
LogDir string
AgentConfigDir string
AgentLogDir string
}{
AgentID: agent.ID.String(),
BinaryURL: binaryURL,
ConfigURL: configURL,
Platform: agent.OSType,
Architecture: agent.OSArchitecture,
Version: agent.CurrentVersion,
AgentUser: "redflag-agent",
AgentHome: "/var/lib/redflag/agent",
ConfigDir: "/etc/redflag",
LogDir: "/var/log/redflag",
AgentConfigDir: "/etc/redflag/agent",
AgentLogDir: "/var/log/redflag/agent",
}
// Choose template based on platform
var templateName string
if strings.Contains(agent.OSType, "windows") {
templateName = "templates/install/scripts/windows.ps1.tmpl"
} else {
templateName = "templates/install/scripts/linux.sh.tmpl"
}
// Load and parse template
tmpl, err := template.ParseFS(installScriptTemplates, templateName)
if err != nil {
return "", fmt.Errorf("failed to load template: %w", err)
}
// Render template
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("failed to render template: %w", err)
}
return buf.String(), nil
}
// RenderInstallScriptFromBuild renders script using build response
func (s *InstallTemplateService) RenderInstallScriptFromBuild(
agentIDParam string,
platform string,
architecture string,
version string,
serverURL string,
registrationToken string,
) (string, error) {
// Extract or generate agent ID
agentID := s.extractOrGenerateAgentID(agentIDParam)
// Build correct URLs in Go, not templates
binaryURL := fmt.Sprintf("%s/api/v1/downloads/%s-%s?version=%s", serverURL, platform, architecture, version)
configURL := fmt.Sprintf("%s/api/v1/downloads/config/%s", serverURL, agentID)
data := struct {
AgentID string
BinaryURL string
ConfigURL string
Platform string
Architecture string
Version string
ServerURL string
RegistrationToken string
AgentUser string
AgentHome string
ConfigDir string
LogDir string
AgentConfigDir string
AgentLogDir string
}{
AgentID: agentID,
BinaryURL: binaryURL,
ConfigURL: configURL,
Platform: platform,
Architecture: architecture,
Version: version,
ServerURL: serverURL,
RegistrationToken: registrationToken,
AgentUser: "redflag-agent",
AgentHome: "/var/lib/redflag/agent",
ConfigDir: "/etc/redflag",
LogDir: "/var/log/redflag",
AgentConfigDir: "/etc/redflag/agent",
AgentLogDir: "/var/log/redflag/agent",
}
templateName := "templates/install/scripts/linux.sh.tmpl"
if strings.Contains(platform, "windows") {
templateName = "templates/install/scripts/windows.ps1.tmpl"
}
tmpl, err := template.ParseFS(installScriptTemplates, templateName)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
// BuildAgentConfigWithAgentID builds config for an existing agent (for upgrades)
func (s *InstallTemplateService) BuildAgentConfigWithAgentID(
agentID string,
platform string,
architecture string,
version string,
serverURL string,
) (string, error) {
// Validate agent ID
if _, err := uuid.Parse(agentID); err != nil {
return "", fmt.Errorf("invalid agent ID: %w", err)
}
// Build correct URLs using existing agent ID
binaryURL := fmt.Sprintf("%s/api/v1/downloads/%s-%s?version=%s", serverURL, platform, architecture, version)
configURL := fmt.Sprintf("%s/api/v1/downloads/config/%s", serverURL, agentID)
data := struct {
AgentID string
BinaryURL string
ConfigURL string
Platform string
Architecture string
Version string
ServerURL string
AgentUser string
AgentHome string
ConfigDir string
LogDir string
}{
AgentID: agentID,
BinaryURL: binaryURL,
ConfigURL: configURL,
Platform: platform,
Architecture: architecture,
Version: version,
ServerURL: serverURL,
AgentUser: "redflag-agent",
AgentHome: "/var/lib/redflag-agent",
ConfigDir: "/etc/redflag",
LogDir: "/var/log/redflag",
}
templateName := "templates/install/scripts/linux.sh.tmpl"
if strings.Contains(platform, "windows") {
templateName = "templates/install/scripts/windows.ps1.tmpl"
}
tmpl, err := template.ParseFS(installScriptTemplates, templateName)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
// extractOrGenerateAgentID extracts or generates a valid agent ID
func (s *InstallTemplateService) extractOrGenerateAgentID(param string) string {
log.Printf("[DEBUG] extractOrGenerateAgentID received param: %s", param)
// If we got a real agent ID (UUID format), validate and use it
if param != "" && param != "<AGENT_ID>" {
// Validate it's a UUID
if _, err := uuid.Parse(param); err == nil {
log.Printf("[DEBUG] Using passed UUID: %s", param)
return param
}
log.Printf("[DEBUG] Invalid UUID format, generating new one")
}
// Placeholder case - generate new UUID for fresh installation
newID := uuid.New().String()
log.Printf("[DEBUG] Generated new UUID: %s", newID)
return newID
}