Skip to content

Commit acdd507

Browse files
feat: add deployment automation and enhance Consul security configuration
- Added Makefile deployment targets with rollback support for server deployments - Enhanced Consul security with mandatory gossip encryption, localhost-only default client_addr, and configurable script checks - Added branch name validation for git operations to prevent invalid characters and improve error messages
1 parent 3301471 commit acdd507

13 files changed

Lines changed: 1201 additions & 69 deletions

File tree

Makefile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,42 @@ ci: deps fmt-check vet lint test build ## CI pipeline (no auto-fix)
176176

177177
ci-cgo: deps fmt-check vet-cgo lint-cgo test-cgo build ## CI pipeline for CGO packages
178178
@echo "[INFO] CGO CI pipeline complete"
179+
180+
##@ Deployment
181+
182+
DEPLOY_SERVERS ?= vhost2
183+
REMOTE_INSTALL_PATH := /usr/local/bin/eos
184+
185+
deploy: test build ## Deploy to servers (set DEPLOY_SERVERS="host1 host2")
186+
@echo "[INFO] Deploying Eos to servers: $(DEPLOY_SERVERS)"
187+
@for server in $(DEPLOY_SERVERS); do \
188+
echo "[INFO] → Deploying to $$server..."; \
189+
scp $(BUILD_DIR)/$(BINARY_NAME) $$server:/tmp/eos-new || exit 1; \
190+
ssh $$server "sudo mv /tmp/eos-new $(REMOTE_INSTALL_PATH) && sudo chmod +x $(REMOTE_INSTALL_PATH)" || exit 1; \
191+
version=$$(ssh $$server "$(REMOTE_INSTALL_PATH) --version 2>/dev/null || echo 'unknown'"); \
192+
echo "[INFO] ✓ Deployed to $$server (version: $$version)"; \
193+
done
194+
@echo "[INFO] Deployment complete!"
195+
196+
deploy-check: ## Verify deployment on all servers
197+
@echo "[INFO] Checking Eos version on servers..."
198+
@for server in $(DEPLOY_SERVERS); do \
199+
echo "[INFO] → Checking $$server..."; \
200+
ssh $$server "$(REMOTE_INSTALL_PATH) --version" || echo "[ERROR] Failed to get version from $$server"; \
201+
done
202+
203+
deploy-all: test build ## Deploy to all production servers
204+
@$(MAKE) deploy DEPLOY_SERVERS="vhost2 vhost3 vhost4"
205+
206+
deploy-rollback: ## Rollback to previous version (if backup exists)
207+
@echo "[INFO] Rolling back Eos on servers: $(DEPLOY_SERVERS)"
208+
@for server in $(DEPLOY_SERVERS); do \
209+
echo "[INFO] → Rolling back $$server..."; \
210+
backup=$$(ssh $$server "ls -t $(REMOTE_INSTALL_PATH).backup.* 2>/dev/null | head -1"); \
211+
if [ -z "$$backup" ]; then \
212+
echo "[ERROR] No backup found on $$server"; \
213+
exit 1; \
214+
fi; \
215+
ssh $$server "sudo cp $$backup $(REMOTE_INSTALL_PATH) && sudo chmod +x $(REMOTE_INSTALL_PATH)"; \
216+
echo "[INFO] ✓ Rolled back to $$backup"; \
217+
done

outputs/eos-consul-audit-report.md

Lines changed: 453 additions & 0 deletions
Large diffs are not rendered by default.

pkg/consul/config/generator.go

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ func Generate(rc *eos_io.RuntimeContext, cfg *ConsulConfig) error {
8484
bootstrapExpect = 1
8585
}
8686

87+
if cfg.GossipKey == "" {
88+
return fmt.Errorf("gossip encryption key is required (set GeneratorConfig.GossipKey)")
89+
}
90+
8791
// Build server mode configuration
8892
var serverConfig string
8993
if bootstrapExpect == 1 {
@@ -96,6 +100,39 @@ bootstrap = true # Single-node cluster`
96100
bootstrap_expect = %d # Multi-node cluster`, bootstrapExpect)
97101
}
98102

103+
clientAddr := cfg.ClientAddr
104+
if clientAddr == "" {
105+
clientAddr = "127.0.0.1"
106+
}
107+
if clientAddr != "127.0.0.1" {
108+
log.Warn("Consul client_addr overrides secure default",
109+
zap.String("client_addr", clientAddr),
110+
zap.String("recommendation", "Use 127.0.0.1 unless ACLs/TLS are enforced"))
111+
}
112+
113+
scriptCheckConfig := "enable_script_checks = false\n"
114+
if cfg.EnableLocalScriptChecks {
115+
scriptCheckConfig += "enable_local_script_checks = true\n"
116+
}
117+
118+
watcherConfig := ""
119+
if cfg.EnableVaultWatcher && !cfg.EnableLocalScriptChecks {
120+
log.Warn("Vault watcher requires local script checks; disabling watcher for security",
121+
zap.String("handler", "consul-vault-helper"),
122+
zap.Bool("requested", cfg.EnableVaultWatcher))
123+
}
124+
if cfg.EnableVaultWatcher && cfg.EnableLocalScriptChecks {
125+
watcherConfig = fmt.Sprintf(`# Watches for external integration
126+
watches = [
127+
{
128+
type = "services"
129+
handler_type = "script"
130+
args = ["%s", "watch"]
131+
}
132+
]
133+
`, consulVaultHelperPath)
134+
}
135+
99136
config := fmt.Sprintf(`# Consul Configuration for Scaling and Service Discovery
100137
# Generated by Eos at %s
101138
@@ -120,9 +157,9 @@ ports {
120157
server = %d # Keep default for RPC
121158
}
122159
123-
# Network configuration
124-
client_addr = "0.0.0.0" # Accept connections from anywhere
125-
bind_addr = "%s" # Primary interface IP
160+
# Network configuration
161+
client_addr = "%s" # Default: localhost only for security
162+
bind_addr = "%s" # Primary interface IP
126163
127164
# Advertise addresses for when you add more nodes
128165
advertise_addr = "%s"
@@ -177,8 +214,7 @@ autopilot {
177214
}
178215
179216
# Script checks disabled for security (enable only with ACLs)
180-
# enable_script_checks = false # Use enable_local_script_checks instead
181-
enable_local_script_checks = true
217+
%s
182218
183219
# Security settings
184220
# ACLs enabled by default for secure token management and Vault integration
@@ -191,7 +227,7 @@ acl = {
191227
}
192228
193229
# Encryption settings (prepared for production)
194-
# encrypt = "base64-key-here" # Uncomment and set for production
230+
encrypt = "%s" # Gossip encryption key
195231
196232
# TLS settings (prepared for production)
197233
# tls {
@@ -204,17 +240,9 @@ acl = {
204240
# }
205241
# }
206242
207-
# Watches for external integration
208-
watches = [
209-
{
210-
type = "services"
211-
handler_type = "script"
212-
args = ["%s", "watch"]
213-
}
214-
]
215-
`, time.Now().Format(time.RFC3339), cfg.DatacenterName, nodeName, consulDefaultDataDir, serverConfig,
243+
%s`, time.Now().Format(time.RFC3339), cfg.DatacenterName, nodeName, consulDefaultDataDir, serverConfig,
216244
shared.PortConsul, portHTTP, portgRPC, portDNS, portSerfLAN, portSerfWAN, portServer,
217-
iface.IP, iface.IP, iface.IP, logLevel, shared.GetInternalHostname(), consulVaultHelperPath)
245+
clientAddr, iface.IP, iface.IP, iface.IP, logLevel, shared.GetInternalHostname(), cfg.GossipKey, scriptCheckConfig, watcherConfig)
218246

219247
configPath := consulConfigFile
220248

pkg/consul/config/generator_test.go

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ func TestGenerate(t *testing.T) {
1515
wantErr bool
1616
checkFn func(t *testing.T, cfg *ConsulConfig)
1717
}{
18-
{
19-
name: "valid production config",
20-
config: &ConsulConfig{
21-
DatacenterName: "production",
22-
EnableDebugLogging: false,
23-
VaultAvailable: true,
24-
},
18+
{
19+
name: "valid production config",
20+
config: &ConsulConfig{
21+
DatacenterName: "production",
22+
EnableDebugLogging: false,
23+
VaultAvailable: true,
24+
GossipKey: "test-gossip-key",
25+
},
2526
wantErr: false,
2627
checkFn: func(t *testing.T, cfg *ConsulConfig) {
2728
assert.Equal(t, "production", cfg.DatacenterName)
@@ -31,11 +32,12 @@ func TestGenerate(t *testing.T) {
3132
},
3233
{
3334
name: "valid development config with debug",
34-
config: &ConsulConfig{
35-
DatacenterName: "development",
36-
EnableDebugLogging: true,
37-
VaultAvailable: false,
38-
},
35+
config: &ConsulConfig{
36+
DatacenterName: "development",
37+
EnableDebugLogging: true,
38+
VaultAvailable: false,
39+
GossipKey: "test-gossip-key",
40+
},
3941
wantErr: false,
4042
checkFn: func(t *testing.T, cfg *ConsulConfig) {
4143
assert.Equal(t, "development", cfg.DatacenterName)
@@ -45,23 +47,25 @@ func TestGenerate(t *testing.T) {
4547
},
4648
{
4749
name: "empty datacenter name",
48-
config: &ConsulConfig{
49-
DatacenterName: "",
50-
EnableDebugLogging: false,
51-
VaultAvailable: false,
52-
},
50+
config: &ConsulConfig{
51+
DatacenterName: "",
52+
EnableDebugLogging: false,
53+
VaultAvailable: false,
54+
GossipKey: "test-gossip-key",
55+
},
5356
wantErr: false, // Should handle empty datacenter gracefully
5457
checkFn: func(t *testing.T, cfg *ConsulConfig) {
5558
assert.Equal(t, "", cfg.DatacenterName)
5659
},
5760
},
5861
{
5962
name: "datacenter with special characters",
60-
config: &ConsulConfig{
61-
DatacenterName: "test-dc_1",
62-
EnableDebugLogging: true,
63-
VaultAvailable: true,
64-
},
63+
config: &ConsulConfig{
64+
DatacenterName: "test-dc_1",
65+
EnableDebugLogging: true,
66+
VaultAvailable: true,
67+
GossipKey: "test-gossip-key",
68+
},
6569
wantErr: false,
6670
checkFn: func(t *testing.T, cfg *ConsulConfig) {
6771
assert.Equal(t, "test-dc_1", cfg.DatacenterName)
@@ -94,11 +98,12 @@ func TestGenerate(t *testing.T) {
9498

9599
func TestConsulConfig(t *testing.T) {
96100
t.Run("config creation", func(t *testing.T) {
97-
cfg := &ConsulConfig{
98-
DatacenterName: "test",
99-
EnableDebugLogging: true,
100-
VaultAvailable: false,
101-
}
101+
cfg := &ConsulConfig{
102+
DatacenterName: "test",
103+
EnableDebugLogging: true,
104+
VaultAvailable: false,
105+
GossipKey: "test-gossip-key",
106+
}
102107

103108
assert.Equal(t, "test", cfg.DatacenterName)
104109
assert.True(t, cfg.EnableDebugLogging)
@@ -116,11 +121,12 @@ func TestConsulConfig(t *testing.T) {
116121
}
117122

118123
for _, dc := range datacenters {
119-
cfg := &ConsulConfig{
120-
DatacenterName: dc,
121-
EnableDebugLogging: false,
122-
VaultAvailable: true,
123-
}
124+
cfg := &ConsulConfig{
125+
DatacenterName: dc,
126+
EnableDebugLogging: false,
127+
VaultAvailable: true,
128+
GossipKey: "test-gossip-key",
129+
}
124130

125131
assert.Equal(t, dc, cfg.DatacenterName)
126132
}
@@ -135,11 +141,12 @@ func TestGeneratePermissions(t *testing.T) {
135141
t.Run("handles permission errors gracefully", func(t *testing.T) {
136142
rc := testutil.TestRuntimeContext(t)
137143

138-
cfg := &ConsulConfig{
139-
DatacenterName: "test",
140-
EnableDebugLogging: false,
141-
VaultAvailable: false,
142-
}
144+
cfg := &ConsulConfig{
145+
DatacenterName: "test",
146+
EnableDebugLogging: false,
147+
VaultAvailable: false,
148+
GossipKey: "test-gossip-key",
149+
}
143150

144151
// Function should handle permission errors without panicking
145152
err := Generate(rc, cfg)
@@ -165,4 +172,4 @@ func TestGenerateWithNilConfig(t *testing.T) {
165172

166173
// This will currently panic - should be fixed to return error instead
167174
_ = Generate(rc, nil)
168-
}
175+
}

pkg/consul/config/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ type GeneratorConfig struct {
88
EnableDebugLogging bool
99
VaultAvailable bool
1010
BootstrapExpect int // Number of expected servers (1 = use bootstrap mode, >1 = use bootstrap_expect)
11+
ClientAddr string
12+
GossipKey string
13+
// EnableLocalScriptChecks explicitly re-enables local script checks.
14+
// Default is false to align with HashiCorp guidance (script checks disabled).
15+
EnableLocalScriptChecks bool
16+
// EnableVaultWatcher adds the consul-vault-helper script watcher.
17+
// Disabled by default because it relies on script handlers.
18+
EnableVaultWatcher bool
1119
}
1220

1321
// DEPRECATED: ConsulConfig is renamed to GeneratorConfig for clarity

pkg/consul/lifecycle/gossip_key.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package lifecycle
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/user"
7+
"path/filepath"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/CodeMonkeyCybersecurity/eos/pkg/consul/config"
12+
"github.com/CodeMonkeyCybersecurity/eos/pkg/consul/secrets"
13+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
14+
"go.uber.org/zap"
15+
)
16+
17+
const (
18+
consulConfigPath = "/etc/consul.d/consul.hcl"
19+
gossipKeyPath = "/etc/consul.d/gossip.key"
20+
)
21+
22+
func loadExistingGossipKey(logger otelzap.LoggerWithCtx) string {
23+
if data, err := os.ReadFile(gossipKeyPath); err == nil {
24+
if key := strings.TrimSpace(string(data)); key != "" {
25+
logger.Info("Reusing gossip encryption key from cache file",
26+
zap.String("path", gossipKeyPath))
27+
return key
28+
}
29+
}
30+
31+
if data, err := os.ReadFile(consulConfigPath); err == nil {
32+
if parsed, parseErr := config.ParseHCL(string(data)); parseErr == nil && parsed.Encrypt != "" {
33+
logger.Info("Reusing gossip encryption key from existing configuration",
34+
zap.String("path", consulConfigPath))
35+
return parsed.Encrypt
36+
}
37+
}
38+
return ""
39+
}
40+
41+
func persistGossipKey(logger otelzap.LoggerWithCtx, key string) error {
42+
if err := os.WriteFile(gossipKeyPath, []byte(key+"\n"), 0o600); err != nil {
43+
return fmt.Errorf("failed to persist gossip key: %w", err)
44+
}
45+
46+
if err := os.Chmod(gossipKeyPath, 0o600); err != nil {
47+
logger.Warn("Failed to set gossip key permissions",
48+
zap.String("path", gossipKeyPath),
49+
zap.Error(err))
50+
}
51+
52+
if err := chownConsul(gossipKeyPath); err != nil {
53+
logger.Warn("Failed to set gossip key ownership",
54+
zap.String("path", gossipKeyPath),
55+
zap.Error(err))
56+
}
57+
58+
return nil
59+
}
60+
61+
func chownConsul(path string) error {
62+
owner, err := user.Lookup("consul")
63+
if err != nil {
64+
return err
65+
}
66+
uid, err := strconv.Atoi(owner.Uid)
67+
if err != nil {
68+
return err
69+
}
70+
gid, err := strconv.Atoi(owner.Gid)
71+
if err != nil {
72+
return err
73+
}
74+
return os.Chown(path, uid, gid)
75+
}
76+
77+
func ensureGossipKey(logger otelzap.LoggerWithCtx) (string, error) {
78+
if key := loadExistingGossipKey(logger); key != "" {
79+
return key, nil
80+
}
81+
82+
key, err := secrets.GenerateGossipKey()
83+
if err != nil {
84+
return "", fmt.Errorf("failed to generate gossip encryption key: %w", err)
85+
}
86+
87+
if err := os.MkdirAll(filepath.Dir(gossipKeyPath), 0o755); err != nil {
88+
logger.Warn("Failed to ensure directory for gossip key",
89+
zap.Error(err),
90+
zap.String("path", gossipKeyPath))
91+
} else if err := persistGossipKey(logger, key); err != nil {
92+
logger.Warn("Failed to persist gossip key to disk", zap.Error(err))
93+
}
94+
95+
logger.Info("Generated new gossip encryption key",
96+
zap.String("path", gossipKeyPath))
97+
return key, nil
98+
}

0 commit comments

Comments
 (0)