Skip to content

Commit f7f5abc

Browse files
fix: remediate CodeQL security alerts (#361)
1 parent 323d30f commit f7f5abc

14 files changed

Lines changed: 283 additions & 193 deletions

File tree

.github/workflows/docker.yml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/functional-tests.yml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

control-plane/cmd/af/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ func loadConfig(configFile string) (*config.Config, error) {
252252
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
253253
}
254254

255+
config.ApplyEnvOverrides(&cfg)
256+
255257
// Apply sensible defaults for user experience
256258
if cfg.AgentField.Port == 0 {
257259
cfg.AgentField.Port = 8080

control-plane/config/agentfield.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

control-plane/internal/config/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type UIConfig struct {
3434
// AgentFieldConfig holds the core AgentField server configuration.
3535
type AgentFieldConfig struct {
3636
Port int `yaml:"port"`
37+
Registration RegistrationConfig `yaml:"registration" mapstructure:"registration"`
3738
NodeHealth NodeHealthConfig `yaml:"node_health" mapstructure:"node_health"`
3839
LLMHealth LLMHealthConfig `yaml:"llm_health" mapstructure:"llm_health"`
3940
ExecutionCleanup ExecutionCleanupConfig `yaml:"execution_cleanup" mapstructure:"execution_cleanup"`
@@ -43,6 +44,11 @@ type AgentFieldConfig struct {
4344
ExecutionLogs ExecutionLogsConfig `yaml:"execution_logs" mapstructure:"execution_logs"`
4445
}
4546

47+
// RegistrationConfig governs validation of agent-supplied registration endpoints.
48+
type RegistrationConfig struct {
49+
ServerlessDiscoveryAllowedHosts []string `yaml:"serverless_discovery_allowed_hosts" mapstructure:"serverless_discovery_allowed_hosts"`
50+
}
51+
4652
// NodeLogProxyConfig limits the control plane proxy to agent process logs (NDJSON).
4753
type NodeLogProxyConfig struct {
4854
ConnectTimeout time.Duration `yaml:"connect_timeout" mapstructure:"connect_timeout"`
@@ -361,6 +367,17 @@ func ApplyEnvOverrides(cfg *Config) {
361367
cfg.API.Auth.APIKey = apiKey
362368
}
363369

370+
if val := os.Getenv("AGENTFIELD_REGISTRATION_SERVERLESS_DISCOVERY_ALLOWED_HOSTS"); val != "" {
371+
parts := strings.Split(val, ",")
372+
cfg.AgentField.Registration.ServerlessDiscoveryAllowedHosts = cfg.AgentField.Registration.ServerlessDiscoveryAllowedHosts[:0]
373+
for _, part := range parts {
374+
trimmed := strings.TrimSpace(part)
375+
if trimmed != "" {
376+
cfg.AgentField.Registration.ServerlessDiscoveryAllowedHosts = append(cfg.AgentField.Registration.ServerlessDiscoveryAllowedHosts, trimmed)
377+
}
378+
}
379+
}
380+
364381
// Node health monitoring overrides
365382
if val := os.Getenv("AGENTFIELD_HEALTH_CHECK_INTERVAL"); val != "" {
366383
if d, err := time.ParseDuration(val); err == nil {

control-plane/internal/encryption/encryption.go

Lines changed: 87 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,163 @@
11
package encryption
22

33
import (
4+
"bytes"
45
"crypto/aes"
56
"crypto/cipher"
67
"crypto/rand"
78
"crypto/sha256"
89
"encoding/base64"
910
"fmt"
1011
"io"
12+
"strings"
13+
14+
"golang.org/x/crypto/pbkdf2"
15+
)
16+
17+
const (
18+
encryptionStringVersion = "v2"
19+
encryptionBinaryMagic = "AFENC2"
20+
encryptionSaltSize = 16
21+
encryptionKeySize = 32
22+
encryptionPBKDF2Rounds = 600000
1123
)
1224

1325
// EncryptionService provides encryption and decryption for sensitive configuration values
1426
type EncryptionService struct {
15-
key []byte
27+
passphrase []byte
1628
}
1729

18-
// NewEncryptionService creates a new encryption service with a derived key
30+
// NewEncryptionService creates a new encryption service with a PBKDF2-hardened passphrase.
1931
func NewEncryptionService(passphrase string) *EncryptionService {
20-
// Derive a 32-byte key from the passphrase using SHA-256
21-
hash := sha256.Sum256([]byte(passphrase))
2232
return &EncryptionService{
23-
key: hash[:],
33+
passphrase: []byte(passphrase),
2434
}
2535
}
2636

27-
// Encrypt encrypts a plaintext string and returns a base64-encoded ciphertext
28-
func (es *EncryptionService) Encrypt(plaintext string) (string, error) {
29-
if plaintext == "" {
30-
return "", nil
37+
func (es *EncryptionService) deriveKey(salt []byte) []byte {
38+
return pbkdf2.Key(es.passphrase, salt, encryptionPBKDF2Rounds, encryptionKeySize, sha256.New)
39+
}
40+
41+
func (es *EncryptionService) encryptRaw(plaintext []byte) ([]byte, error) {
42+
if len(plaintext) == 0 {
43+
return nil, nil
3144
}
3245

33-
// Create AES cipher
34-
block, err := aes.NewCipher(es.key)
46+
salt := make([]byte, encryptionSaltSize)
47+
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
48+
return nil, fmt.Errorf("failed to generate salt: %w", err)
49+
}
50+
51+
block, err := aes.NewCipher(es.deriveKey(salt))
3552
if err != nil {
36-
return "", fmt.Errorf("failed to create AES cipher: %w", err)
53+
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
3754
}
3855

39-
// Create GCM mode
4056
gcm, err := cipher.NewGCM(block)
4157
if err != nil {
42-
return "", fmt.Errorf("failed to create GCM: %w", err)
58+
return nil, fmt.Errorf("failed to create GCM: %w", err)
4359
}
4460

45-
// Generate a random nonce
4661
nonce := make([]byte, gcm.NonceSize())
4762
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
48-
return "", fmt.Errorf("failed to generate nonce: %w", err)
63+
return nil, fmt.Errorf("failed to generate nonce: %w", err)
4964
}
5065

51-
// Encrypt the plaintext
52-
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
53-
54-
// Return base64-encoded ciphertext
55-
return base64.StdEncoding.EncodeToString(ciphertext), nil
66+
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
67+
encoded := make([]byte, 0, len(encryptionBinaryMagic)+len(salt)+len(nonce)+len(ciphertext))
68+
encoded = append(encoded, encryptionBinaryMagic...)
69+
encoded = append(encoded, salt...)
70+
encoded = append(encoded, nonce...)
71+
encoded = append(encoded, ciphertext...)
72+
return encoded, nil
5673
}
5774

58-
// Decrypt decrypts a base64-encoded ciphertext and returns the plaintext
59-
func (es *EncryptionService) Decrypt(ciphertext string) (string, error) {
60-
if ciphertext == "" {
61-
return "", nil
75+
func (es *EncryptionService) decryptRaw(ciphertext []byte) ([]byte, error) {
76+
if len(ciphertext) == 0 {
77+
return nil, nil
6278
}
6379

64-
// Decode base64
65-
data, err := base64.StdEncoding.DecodeString(ciphertext)
66-
if err != nil {
67-
return "", fmt.Errorf("failed to decode base64: %w", err)
80+
if !bytes.HasPrefix(ciphertext, []byte(encryptionBinaryMagic)) {
81+
return nil, fmt.Errorf("unsupported legacy ciphertext format")
6882
}
6983

70-
// Create AES cipher
71-
block, err := aes.NewCipher(es.key)
84+
data := ciphertext[len(encryptionBinaryMagic):]
85+
if len(data) < encryptionSaltSize {
86+
return nil, fmt.Errorf("ciphertext too short")
87+
}
88+
89+
salt, encryptedData := data[:encryptionSaltSize], data[encryptionSaltSize:]
90+
91+
block, err := aes.NewCipher(es.deriveKey(salt))
7292
if err != nil {
73-
return "", fmt.Errorf("failed to create AES cipher: %w", err)
93+
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
7494
}
7595

76-
// Create GCM mode
7796
gcm, err := cipher.NewGCM(block)
7897
if err != nil {
79-
return "", fmt.Errorf("failed to create GCM: %w", err)
98+
return nil, fmt.Errorf("failed to create GCM: %w", err)
8099
}
81100

82-
// Check minimum length
83101
nonceSize := gcm.NonceSize()
84-
if len(data) < nonceSize {
85-
return "", fmt.Errorf("ciphertext too short")
102+
if len(encryptedData) < nonceSize {
103+
return nil, fmt.Errorf("ciphertext too short")
86104
}
87105

88-
// Extract nonce and encrypted data
89-
nonce, encryptedData := data[:nonceSize], data[nonceSize:]
90-
91-
// Decrypt
92-
plaintext, err := gcm.Open(nil, nonce, encryptedData, nil)
106+
nonce, sealed := encryptedData[:nonceSize], encryptedData[nonceSize:]
107+
plaintext, err := gcm.Open(nil, nonce, sealed, nil)
93108
if err != nil {
94-
return "", fmt.Errorf("failed to decrypt: %w", err)
109+
return nil, fmt.Errorf("failed to decrypt: %w", err)
95110
}
96111

97-
return string(plaintext), nil
112+
return plaintext, nil
98113
}
99114

100-
// EncryptBytes encrypts raw bytes and returns the ciphertext as bytes (nonce prepended).
101-
func (es *EncryptionService) EncryptBytes(plaintext []byte) ([]byte, error) {
102-
if len(plaintext) == 0 {
103-
return nil, nil
115+
// Encrypt encrypts a plaintext string and returns a versioned, base64-encoded ciphertext.
116+
func (es *EncryptionService) Encrypt(plaintext string) (string, error) {
117+
if plaintext == "" {
118+
return "", nil
104119
}
105120

106-
block, err := aes.NewCipher(es.key)
121+
encoded, err := es.encryptRaw([]byte(plaintext))
107122
if err != nil {
108-
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
123+
return "", err
109124
}
110125

111-
gcm, err := cipher.NewGCM(block)
112-
if err != nil {
113-
return nil, fmt.Errorf("failed to create GCM: %w", err)
114-
}
126+
return encryptionStringVersion + ":" + base64.StdEncoding.EncodeToString(encoded), nil
127+
}
115128

116-
nonce := make([]byte, gcm.NonceSize())
117-
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
118-
return nil, fmt.Errorf("failed to generate nonce: %w", err)
129+
// Decrypt decrypts a base64-encoded ciphertext and returns the plaintext
130+
func (es *EncryptionService) Decrypt(ciphertext string) (string, error) {
131+
if ciphertext == "" {
132+
return "", nil
119133
}
120134

121-
return gcm.Seal(nonce, nonce, plaintext, nil), nil
122-
}
123-
124-
// DecryptBytes decrypts ciphertext bytes (nonce prepended) and returns the plaintext bytes.
125-
func (es *EncryptionService) DecryptBytes(ciphertext []byte) ([]byte, error) {
126-
if len(ciphertext) == 0 {
127-
return nil, nil
135+
encoded := ciphertext
136+
if strings.HasPrefix(ciphertext, encryptionStringVersion+":") {
137+
encoded = strings.TrimPrefix(ciphertext, encryptionStringVersion+":")
128138
}
129139

130-
block, err := aes.NewCipher(es.key)
140+
data, err := base64.StdEncoding.DecodeString(encoded)
131141
if err != nil {
132-
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
142+
return "", fmt.Errorf("failed to decode base64: %w", err)
133143
}
134144

135-
gcm, err := cipher.NewGCM(block)
145+
plaintext, err := es.decryptRaw(data)
136146
if err != nil {
137-
return nil, fmt.Errorf("failed to create GCM: %w", err)
147+
return "", err
138148
}
139149

140-
nonceSize := gcm.NonceSize()
141-
if len(ciphertext) < nonceSize {
142-
return nil, fmt.Errorf("ciphertext too short")
143-
}
150+
return string(plaintext), nil
151+
}
144152

145-
nonce, encryptedData := ciphertext[:nonceSize], ciphertext[nonceSize:]
146-
plaintext, err := gcm.Open(nil, nonce, encryptedData, nil)
147-
if err != nil {
148-
return nil, fmt.Errorf("failed to decrypt: %w", err)
149-
}
153+
// EncryptBytes encrypts raw bytes and returns the versioned ciphertext bytes.
154+
func (es *EncryptionService) EncryptBytes(plaintext []byte) ([]byte, error) {
155+
return es.encryptRaw(plaintext)
156+
}
150157

151-
return plaintext, nil
158+
// DecryptBytes decrypts versioned ciphertext bytes and returns the plaintext bytes.
159+
func (es *EncryptionService) DecryptBytes(ciphertext []byte) ([]byte, error) {
160+
return es.decryptRaw(ciphertext)
152161
}
153162

154163
// EncryptConfigurationValues encrypts sensitive values in a configuration map

control-plane/internal/encryption/encryption_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func TestEncryptionService_EncryptDecrypt_Roundtrip(t *testing.T) {
2222
decrypted, err := service.Decrypt(ciphertext)
2323
require.NoError(t, err)
2424
require.Equal(t, plaintext, decrypted)
25+
require.Contains(t, ciphertext, "v2:")
2526
}
2627

2728
func TestEncryptionService_EncryptDecrypt_EmptyString(t *testing.T) {
@@ -121,7 +122,7 @@ func TestEncryptionService_EncryptDecrypt_TooShort(t *testing.T) {
121122

122123
_, err := service.Decrypt(shortCiphertext)
123124
require.Error(t, err)
124-
require.Contains(t, err.Error(), "ciphertext too short")
125+
require.Contains(t, err.Error(), "unsupported legacy ciphertext format")
125126
}
126127

127128
func TestEncryptionService_EncryptConfigurationValues(t *testing.T) {
@@ -283,6 +284,20 @@ func TestEncryptionService_LongPlaintext(t *testing.T) {
283284
require.Equal(t, string(longPlaintext), decrypted)
284285
}
285286

287+
func TestEncryptionService_EncryptBytesDecryptBytes_Roundtrip(t *testing.T) {
288+
service := NewEncryptionService("test-passphrase")
289+
290+
plaintext := []byte("sensitive-bytes")
291+
ciphertext, err := service.EncryptBytes(plaintext)
292+
require.NoError(t, err)
293+
require.NotEmpty(t, ciphertext)
294+
require.NotEqual(t, plaintext, ciphertext)
295+
296+
decrypted, err := service.DecryptBytes(ciphertext)
297+
require.NoError(t, err)
298+
require.Equal(t, plaintext, decrypted)
299+
}
300+
286301
func TestEncryptionService_SpecialCharacters(t *testing.T) {
287302
passphrase := "test-passphrase"
288303
service := NewEncryptionService(passphrase)

0 commit comments

Comments
 (0)