Harden config perms and key derivation
CI / check-and-test (pull_request) Successful in 11s

Write generated configs 0600, treat new keys as raw AES-256, keep
PBKDF2 for unprefixed material, and cache derivation at startup.
This commit is contained in:
s1d3sw1ped_bot
2026-09-01 04:22:57 +00:00
parent 45778ac528
commit bc486eb49d
8 changed files with 293 additions and 52 deletions
+19 -12
View File
@@ -1,8 +1,6 @@
package config
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net"
"os"
@@ -609,24 +607,33 @@ func GenerateExampleConfig(filename string) error {
return fmt.Errorf("failed to marshal config: %v", err)
}
err = os.WriteFile(filename, data, 0644)
err = os.WriteFile(filename, data, 0o600)
if err != nil {
return fmt.Errorf("failed to write config file: %v", err)
}
if err := os.Chmod(filename, 0o600); err != nil {
return fmt.Errorf("failed to set config file permissions: %v", err)
}
fmt.Printf("Generated example configuration: %s\n", filename)
fmt.Printf("Edit the configuration file and run: ./teleport -config %s\n", filename)
return nil
}
// generateStrongEncryptionKey generates a cryptographically secure encryption key
// generateStrongEncryptionKey generates a raw 256-bit key (raw: + 64 hex chars).
func generateStrongEncryptionKey() (string, error) {
// Generate 32 random bytes (256 bits) for a strong encryption key
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random key: %v", err)
}
// Convert to hexadecimal string for easy copying
return hex.EncodeToString(bytes), nil
return encryption.GenerateRawKey()
}
// ApplyTCPKeepAlive enables TCP keep-alive when enabled is true.
func ApplyTCPKeepAlive(conn net.Conn, enabled bool) {
if conn == nil || !enabled {
return
}
tcp, ok := conn.(*net.TCPConn)
if !ok {
return
}
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
+60
View File
@@ -7,6 +7,8 @@ import (
"strings"
"testing"
"time"
"teleport/pkg/encryption"
)
func TestLoadConfig(t *testing.T) {
@@ -585,3 +587,61 @@ func TestGenerateExampleConfigUsesLoopback(t *testing.T) {
t.Fatalf("generated client local listen %q", got)
}
}
func TestGenerateExampleConfigMode0600(t *testing.T) {
tempDir := t.TempDir()
configFile := filepath.Join(tempDir, "server.yaml")
if err := GenerateExampleConfig(configFile); err != nil {
t.Fatal(err)
}
st, err := os.Stat(configFile)
if err != nil {
t.Fatal(err)
}
if st.Mode().Perm() != 0o600 {
t.Fatalf("generated config mode %04o want 0600", st.Mode().Perm())
}
cfg, err := LoadConfig(configFile)
if err != nil {
t.Fatalf("reload generated config: %v", err)
}
if !encryption.IsRawKey(cfg.EncryptionKey) {
t.Fatalf("generated encryption_key is not raw: %q", cfg.EncryptionKey[:min(8, len(cfg.EncryptionKey))])
}
key, err := encryption.ResolveKey(cfg.EncryptionKey)
if err != nil {
t.Fatal(err)
}
if len(key) != 32 {
t.Fatalf("resolved key len %d", len(key))
}
}
func TestApplyTCPKeepAlive(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
errCh := make(chan error, 1)
go func() {
c, err := ln.Accept()
if err != nil {
errCh <- err
return
}
defer c.Close()
ApplyTCPKeepAlive(c, true)
errCh <- nil
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
ApplyTCPKeepAlive(conn, true)
ApplyTCPKeepAlive(conn, false)
if err := <-errCh; err != nil {
t.Fatal(err)
}
}
+69 -5
View File
@@ -6,9 +6,11 @@ import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"io"
"math"
"strings"
"sync"
"time"
@@ -24,22 +26,69 @@ const (
// Replay protection parameters
MaxPacketAge = 5 * time.Minute // Maximum age for UDP packets
NonceWindow = 1000 // Number of nonces to track for replay protection
// RawKeyPrefix marks a hex-encoded 32-byte AES key that must not go through PBKDF2.
RawKeyPrefix = "raw:"
RawKeyHexLen = 64 // 32 bytes
)
// DeriveKey derives an encryption key from a password using PBKDF2
// deriveCache memoizes PBKDF2 so handshake/UDP paths never pay 100k iterations twice.
var deriveCache sync.Map // map[string][]byte
// GenerateRawKey returns a raw: prefixed hex encoding of 32 random bytes.
// Paste the entire string into encryption_key; it is used as an AES-256 key (no PBKDF2).
func GenerateRawKey() (string, error) {
key := make([]byte, PBKDF2KeyLength)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return "", fmt.Errorf("failed to generate random key: %v", err)
}
return RawKeyPrefix + hex.EncodeToString(key), nil
}
// IsRawKey reports whether material is a raw 256-bit key (raw: + 64 hex chars).
func IsRawKey(material string) bool {
return strings.HasPrefix(material, RawKeyPrefix)
}
// ResolveKey returns a 32-byte AES key from config material.
//
// - raw:<64 hex>: hex-decode, no PBKDF2 (new --generate-key / generated configs)
// - anything else, including legacy unprefixed 64-hex from older --generate-key:
// PBKDF2 with the historical password-derived salt (existing configs keep working)
func ResolveKey(material string) ([]byte, error) {
if strings.HasPrefix(material, RawKeyPrefix) {
hexStr := material[len(RawKeyPrefix):]
key, err := hex.DecodeString(hexStr)
if err != nil {
return nil, fmt.Errorf("invalid raw encryption key: %w", err)
}
if len(key) != PBKDF2KeyLength {
return nil, fmt.Errorf("raw encryption key must be %d bytes, got %d", PBKDF2KeyLength, len(key))
}
return key, nil
}
return DeriveKey(material), nil
}
// DeriveKey derives an encryption key from a password using PBKDF2.
// The first call for a given password runs 100k iterations; later calls return the cached key.
func DeriveKey(password string) []byte {
// Use a deterministic salt derived from the password hash for consistent key derivation
// This ensures the same password always produces the same key while avoiding rainbow tables
if v, ok := deriveCache.Load(password); ok {
return v.([]byte)
}
// Historical salt is SHA256(password)[:16]. That is not a random salt; keep it only
// so existing passphrase and unprefixed hex configs still derive the same key.
hasher := sha256.New()
hasher.Write([]byte(password))
passwordHash := hasher.Sum(nil)
// Create a deterministic salt from the password hash
salt := make([]byte, PBKDF2SaltLength)
copy(salt, passwordHash[:PBKDF2SaltLength])
key := pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, PBKDF2KeyLength, sha256.New)
return key
actual, _ := deriveCache.LoadOrStore(password, key)
return actual.([]byte)
}
// DeriveKeyWithSalt derives an encryption key from a password using PBKDF2 with a custom salt
@@ -176,6 +225,21 @@ func ConstantTimeCompare(a, b []byte) bool {
// ValidateEncryptionKey validates that an encryption key meets security requirements
func ValidateEncryptionKey(key string) error {
if strings.HasPrefix(key, RawKeyPrefix) {
hexStr := key[len(RawKeyPrefix):]
if len(hexStr) != RawKeyHexLen {
return fmt.Errorf("raw encryption key must be %d hex characters (32 bytes), got %d", RawKeyHexLen, len(hexStr))
}
decoded, err := hex.DecodeString(hexStr)
if err != nil {
return fmt.Errorf("raw encryption key is not valid hex: %w", err)
}
if len(decoded) != PBKDF2KeyLength {
return fmt.Errorf("raw encryption key must decode to %d bytes", PBKDF2KeyLength)
}
return nil
}
if len(key) < 32 {
return fmt.Errorf("encryption key must be at least 32 characters long")
}
+94
View File
@@ -1,6 +1,8 @@
package encryption
import (
"bytes"
"encoding/hex"
"testing"
"time"
)
@@ -240,3 +242,95 @@ func TestConstantTimeCompare(t *testing.T) {
t.Error("Empty slices should compare equal")
}
}
func TestResolveKeyRaw(t *testing.T) {
raw, err := GenerateRawKey()
if err != nil {
t.Fatalf("GenerateRawKey: %v", err)
}
if !IsRawKey(raw) {
t.Fatalf("generated key is not raw: %q", raw[:4])
}
key, err := ResolveKey(raw)
if err != nil {
t.Fatalf("ResolveKey raw: %v", err)
}
if len(key) != 32 {
t.Fatalf("raw key length %d", len(key))
}
decoded, err := hex.DecodeString(raw[len(RawKeyPrefix):])
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(key, decoded) {
t.Fatal("raw key was not hex-decoded as-is")
}
if bytes.Equal(key, DeriveKey(raw)) {
t.Fatal("raw key must not go through PBKDF2")
}
}
func TestResolveKeyLegacyHexStillPBKDF2(t *testing.T) {
legacy := "a0e3dd20a761b118ca234160dd8b87230a001e332a97c9cfe3b8b9c99efaae03"
decoded, err := hex.DecodeString(legacy)
if err != nil {
t.Fatal(err)
}
got, err := ResolveKey(legacy)
if err != nil {
t.Fatalf("ResolveKey legacy hex: %v", err)
}
want := DeriveKey(legacy)
if !bytes.Equal(got, want) {
t.Fatal("unprefixed 64-hex must still use PBKDF2 (old --generate-key configs)")
}
if bytes.Equal(got, decoded) {
t.Fatal("unprefixed 64-hex must not be treated as a raw AES key")
}
}
func TestResolveKeyPassphrase(t *testing.T) {
pw := "test-passphrase-not-a-hex-key-value"
got, err := ResolveKey(pw)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, DeriveKey(pw)) {
t.Fatal("passphrase should use PBKDF2")
}
}
func TestDeriveKeyCached(t *testing.T) {
pw := "cache-me-please-this-is-long-enough"
start := time.Now()
k1 := DeriveKey(pw)
first := time.Since(start)
start = time.Now()
k2 := DeriveKey(pw)
second := time.Since(start)
if !bytes.Equal(k1, k2) {
t.Fatal("cached key mismatch")
}
if first < 10*time.Millisecond {
t.Logf("first PBKDF2 unexpectedly fast: %v", first)
}
if second > 5*time.Millisecond {
t.Fatalf("cached DeriveKey too slow: first=%v second=%v", first, second)
}
}
func TestValidateRawEncryptionKey(t *testing.T) {
raw, err := GenerateRawKey()
if err != nil {
t.Fatal(err)
}
if err := ValidateEncryptionKey(raw); err != nil {
t.Fatalf("valid raw key rejected: %v", err)
}
if err := ValidateEncryptionKey("raw:not-hex"); err == nil {
t.Fatal("invalid raw hex should fail")
}
if err := ValidateEncryptionKey("raw:abcd"); err == nil {
t.Fatal("short raw key should fail")
}
}