Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions internal/k8s/cluster/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,76 @@ type ErrInvalidConfiguration struct {
func (e *ErrInvalidConfiguration) Error() string {
return "invalid cluster configuration: " + e.Message
}

// Security Best Practices
//
// These guidelines should be followed when provisioning clusters and resources.

// SecurityWarnings contains common security warnings to include in plans
var SecurityWarnings = map[string]string{
"public-endpoint": "Warning: Enabling public endpoints exposes the cluster to the internet. Consider using private endpoints with VPN/bastion access for production workloads.",
"public-service": "Warning: Services of type LoadBalancer expose your application to the internet. Consider using internal load balancers or Ingress with authentication.",
"no-network-policy": "Note: No network policy specified. Consider adding network policies to restrict pod-to-pod traffic for defense in depth.",
"secrets-plain": "Warning: Secrets should be managed using a secrets management solution (e.g., HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) rather than plain Kubernetes secrets.",
"root-container": "Warning: Running containers as root is a security risk. Consider using non-root users in your container images.",
"privileged": "Warning: Privileged containers have full access to the host. Only use when absolutely necessary.",
"host-network": "Warning: Using host network bypasses network policies. Consider using standard pod networking.",
"no-resource-limits": "Note: No resource limits specified. Consider adding CPU/memory limits to prevent resource exhaustion.",
}

// SecurityRecommendations contains security recommendations for different scenarios
var SecurityRecommendations = map[string][]string{
"new-cluster": {
"Enable private endpoint access when possible",
"Configure network policies to restrict pod communication",
"Enable pod security standards (restricted or baseline)",
"Set up audit logging for security monitoring",
"Use managed node groups with automatic updates",
},
"new-deployment": {
"Use non-root containers where possible",
"Set resource requests and limits",
"Configure readiness and liveness probes",
"Use read-only root filesystem when possible",
"Drop unnecessary Linux capabilities",
},
"new-service": {
"Use ClusterIP for internal-only services",
"Use internal load balancers for private traffic",
"Consider Ingress with TLS termination for HTTP services",
"Add network policies to control service access",
},
"secrets": {
"Use external secrets management (Vault, AWS Secrets Manager, etc.)",
"Enable encryption at rest for etcd",
"Rotate secrets regularly",
"Limit secret access using RBAC",
},
}

// GetSecurityWarning returns a security warning for the given key
func GetSecurityWarning(key string) string {
if warning, ok := SecurityWarnings[key]; ok {
return warning
}
return ""
}

// GetSecurityRecommendations returns security recommendations for the given scenario
func GetSecurityRecommendations(scenario string) []string {
if recs, ok := SecurityRecommendations[scenario]; ok {
return recs
}
return nil
}

// IsPublicEndpoint checks if cluster configuration has public endpoint enabled
func IsPublicEndpoint(opts CreateOptions) bool {
return opts.EnablePublicEndpoint && !opts.EnablePrivateEndpoint
}

// ShouldWarnPublicAccess checks if a public access warning should be shown
func ShouldWarnPublicAccess(opts CreateOptions) bool {
// Warn if only public endpoint is enabled without private
return opts.EnablePublicEndpoint && !opts.EnablePrivateEndpoint
}
253 changes: 253 additions & 0 deletions internal/k8s/cluster/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package cluster

import (
"testing"
)

func TestGetSecurityWarning(t *testing.T) {
tests := []struct {
name string
key string
wantNone bool
}{
{
name: "public endpoint warning",
key: "public-endpoint",
},
{
name: "public service warning",
key: "public-service",
},
{
name: "no network policy warning",
key: "no-network-policy",
},
{
name: "secrets warning",
key: "secrets-plain",
},
{
name: "root container warning",
key: "root-container",
},
{
name: "privileged warning",
key: "privileged",
},
{
name: "host network warning",
key: "host-network",
},
{
name: "no resource limits warning",
key: "no-resource-limits",
},
{
name: "unknown key returns empty",
key: "unknown-key",
wantNone: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warning := GetSecurityWarning(tt.key)
if tt.wantNone {
if warning != "" {
t.Errorf("GetSecurityWarning(%q) = %q, want empty", tt.key, warning)
}
} else {
if warning == "" {
t.Errorf("GetSecurityWarning(%q) returned empty, want warning", tt.key)
}
}
})
}
}

func TestGetSecurityRecommendations(t *testing.T) {
tests := []struct {
name string
scenario string
wantNone bool
}{
{
name: "new cluster recommendations",
scenario: "new-cluster",
},
{
name: "new deployment recommendations",
scenario: "new-deployment",
},
{
name: "new service recommendations",
scenario: "new-service",
},
{
name: "secrets recommendations",
scenario: "secrets",
},
{
name: "unknown scenario returns nil",
scenario: "unknown-scenario",
wantNone: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
recs := GetSecurityRecommendations(tt.scenario)
if tt.wantNone {
if recs != nil {
t.Errorf("GetSecurityRecommendations(%q) = %v, want nil", tt.scenario, recs)
}
} else {
if recs == nil || len(recs) == 0 {
t.Errorf("GetSecurityRecommendations(%q) returned empty, want recommendations", tt.scenario)
}
}
})
}
}

func TestIsPublicEndpoint(t *testing.T) {
tests := []struct {
name string
opts CreateOptions
expected bool
}{
{
name: "only public endpoint enabled",
opts: CreateOptions{
EnablePublicEndpoint: true,
EnablePrivateEndpoint: false,
},
expected: true,
},
{
name: "both endpoints enabled",
opts: CreateOptions{
EnablePublicEndpoint: true,
EnablePrivateEndpoint: true,
},
expected: false,
},
{
name: "only private endpoint enabled",
opts: CreateOptions{
EnablePublicEndpoint: false,
EnablePrivateEndpoint: true,
},
expected: false,
},
{
name: "neither endpoint enabled",
opts: CreateOptions{
EnablePublicEndpoint: false,
EnablePrivateEndpoint: false,
},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsPublicEndpoint(tt.opts)
if result != tt.expected {
t.Errorf("IsPublicEndpoint() = %v, want %v", result, tt.expected)
}
})
}
}

func TestShouldWarnPublicAccess(t *testing.T) {
tests := []struct {
name string
opts CreateOptions
expected bool
}{
{
name: "warn when only public endpoint",
opts: CreateOptions{
EnablePublicEndpoint: true,
EnablePrivateEndpoint: false,
},
expected: true,
},
{
name: "no warn when both endpoints",
opts: CreateOptions{
EnablePublicEndpoint: true,
EnablePrivateEndpoint: true,
},
expected: false,
},
{
name: "no warn when only private",
opts: CreateOptions{
EnablePublicEndpoint: false,
EnablePrivateEndpoint: true,
},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ShouldWarnPublicAccess(tt.opts)
if result != tt.expected {
t.Errorf("ShouldWarnPublicAccess() = %v, want %v", result, tt.expected)
}
})
}
}

func TestSecurityWarningsContent(t *testing.T) {
// Verify that security warnings contain expected keywords
expectedKeywords := map[string]string{
"public-endpoint": "public",
"public-service": "LoadBalancer",
"no-network-policy": "network policy",
"secrets-plain": "secrets",
"root-container": "root",
"privileged": "Privileged",
"host-network": "host network",
"no-resource-limits": "resource limits",
}

for key, keyword := range expectedKeywords {
t.Run(key, func(t *testing.T) {
warning := GetSecurityWarning(key)
if warning == "" {
t.Fatalf("GetSecurityWarning(%q) returned empty", key)
}
if !containsIgnoreCase(warning, keyword) {
t.Errorf("GetSecurityWarning(%q) = %q, does not contain %q", key, warning, keyword)
}
})
}
}

func containsIgnoreCase(s, substr string) bool {
// Simple case-insensitive contains check
for i := 0; i <= len(s)-len(substr); i++ {
match := true
for j := 0; j < len(substr); j++ {
if toLower(s[i+j]) != toLower(substr[j]) {
match = false
break
}
}
if match {
return true
}
}
return false
}

func toLower(c byte) byte {
if c >= 'A' && c <= 'Z' {
return c + 32
}
return c
}