Lesson 14
Security & hardening
Threat model basics and safer defaults.
Course Navigation
Back to courseLearning Objectives
-
Implement TLS/SSL encryption for secure communication
-
Build authentication systems with passwords and tokens
-
Design authorization and access control mechanisms
-
Implement rate limiting and DDoS protection
-
Apply security hardening best practices
-
Handle graceful shutdowns and rolling upgrades
Lesson 14.1: TLS/SSL Encryption
Generate TLS Certificates
# Generate private key
openssl genrsa -out server.key 2048
# Generate certificate signing request
openssl req -new -key server.key -out server.csr
# Self-signed certificate (for testing)
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# For production: use CA-signed certificate
TLS Server Implementation
import "crypto/tls"
// Create TLS listener
func (s *Server) StartTLS(certFile, keyFile string) error {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
CipherSuites: []uint16{
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_AES_128_GCM_SHA256,
},
}
listener, err := tls.Listen("tcp", s.addr, tlsConfig)
if err != nil {
return err
}
return s.serve(listener)
}
// TLS client connection
func (c *Client) ConnectTLS(addr, certFile string) error {
cert, err := tls.LoadX509KeyPair(certFile, "")
if err != nil {
return err
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: false, // Set to true for self-signed certs
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return err
}
c.conn = conn
return nil
}
Lesson 14.2: Authentication
Password-based Authentication
import "golang.org/x/crypto/bcrypt"
// User represents a database user
type User struct {
Username string
PasswordHash []byte
Permissions []string
CreatedAt time.Time
LastLogin time.Time
}
// Authenticate user with password
func (s *Server) Authenticate(username, password string) (*User, error) {
// Lookup user
user, err := s.getUserByUsername(username)
if err != nil {
return nil, err
}
// Verify password hash
if err := bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)); err != nil {
return nil, fmt.Errorf("invalid credentials")
}
// Update last login
user.LastLogin = time.Now()
s.updateUser(user)
return user, nil
}
// Hash password for storage
func hashPassword(password string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
}
// CreateUser creates a new user
func (s *Server) CreateUser(username, password string, permissions []string) error {
hash, err := hashPassword(password)
if err != nil {
return err
}
user := &User{
Username: username,
PasswordHash: hash,
Permissions: permissions,
CreatedAt: time.Now(),
}
return s.saveUser(user)
}
Token-based Authentication
import "github.com/golang-jwt/jwt/v5"
// JWTClaims represents JWT token claims
type JWTClaims struct {
Username string `json:"username"`
Permissions []string `json:"permissions"`
jwt.RegisteredClaims
}
// GenerateToken creates JWT token
func (s *Server) GenerateToken(user *User) (string, error) {
claims := JWTClaims{
Username: user.Username,
Permissions: user.Permissions,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.jwtSecret))
}
// ValidateToken checks JWT token
func (s *Server) ValidateToken(tokenString string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.jwtSecret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
Lesson 14.3: Authorization
Role-Based Access Control (RBAC)
// Permission represents a specific permission
type Permission struct {
Resource string // e.g., "database", "backup", "config"
Action string // e.g., "read", "write", "delete", "admin"
}
// Role represents a user role
type Role struct {
Name string
Permissions []Permission
Description string
}
// Predefined roles
var (
AdminRole = Role{
Name: "admin",
Permissions: []Permission{
{Resource: "database", Action: "read"},
{Resource: "database", Action: "write"},
{Resource: "database", Action: "delete"},
{Resource: "backup", Action: "read"},
{Resource: "backup", Action: "write"},
{Resource: "config", Action: "read"},
{Resource: "config", Action: "write"},
},
Description: "Full access to all resources",
}
ReadOnlyRole = Role{
Name: "readonly",
Permissions: []Permission{
{Resource: "database", Action: "read"},
},
Description: "Read-only access to database",
}
BackupRole = Role{
Name: "backup",
Permissions: []Permission{
{Resource: "database", Action: "read"},
{Resource: "backup", Action: "read"},
{Resource: "backup", Action: "write"},
},
Description: "Database read and backup access",
}
)
// CheckPermission checks if user has permission
func (s *Server) CheckPermission(user *User, resource, action string) bool {
for _, roleName := range user.Roles {
role, exists := s.roles[roleName]
if !exists {
continue
}
for _, perm := range role.Permissions {
if perm.Resource == resource && perm.Action == action {
return true
}
}
}
return false
}
// AuthorizeRequest checks if request is authorized
func (s *Server) AuthorizeRequest(user *User, operation string) error {
var resource, action string
switch operation {
case "GET", "SCAN":
resource, action = "database", "read"
case "PUT", "POST":
resource, action = "database", "write"
case "DELETE":
resource, action = "database", "delete"
case "BACKUP":
resource, action = "backup", "write"
case "RESTORE":
resource, action = "backup", "read"
case "CONFIG":
resource, action = "config", "read"
default:
return fmt.Errorf("unknown operation: %s", operation)
}
if !s.CheckPermission(user, resource, action) {
return fmt.Errorf("access denied: insufficient permissions for %s on %s", action, resource)
}
return nil
}
Lesson 14.4: Rate Limiting
Token Bucket Rate Limiter
import "golang.org/x/time/rate"
// RateLimiter controls request rate
type RateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.RWMutex
burst int
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter() *RateLimiter {
return &RateLimiter{
limiters: make(map[string]*rate.Limiter),
burst: 10,
}
}
// AllowRequest checks if request allowed
func (rl *RateLimiter) AllowRequest(clientIP string, rps float64) bool {
rl.mu.RLock()
limiter, exists := rl.limiters[clientIP]
rl.mu.RUnlock()
if !exists {
limiter = rate.NewLimiter(rate.Limit(rps), rl.burst)
rl.mu.Lock()
rl.limiters[clientIP] = limiter
rl.mu.Unlock()
}
return limiter.Allow()
}
// WaitForToken waits for token to become available
func (rl *RateLimiter) WaitForToken(clientIP string, rps float64) error {
rl.mu.RLock()
limiter, exists := rl.limiters[clientIP]
rl.mu.RUnlock()
if !exists {
limiter = rate.NewLimiter(rate.Limit(rps), rl.burst)
rl.mu.Lock()
rl.limiters[clientIP] = limiter
rl.mu.Unlock()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return limiter.Wait(ctx)
}
Sliding Window Rate Limiter
// SlidingWindowLimiter implements sliding window rate limiting
type SlidingWindowLimiter struct {
windows map[string]*SlidingWindow
mu sync.RWMutex
limit int
window time.Duration
}
// SlidingWindow represents a sliding window for a client
type SlidingWindow struct {
requests []time.Time
limit int
window time.Duration
mu sync.Mutex
}
// NewSlidingWindowLimiter creates a new sliding window limiter
func NewSlidingWindowLimiter(limit int, window time.Duration) *SlidingWindowLimiter {
return &SlidingWindowLimiter{
windows: make(map[string]*SlidingWindow),
limit: limit,
window: window,
}
}
// AllowRequest checks if request allowed in sliding window
func (swl *SlidingWindowLimiter) AllowRequest(clientIP string) bool {
swl.mu.RLock()
window, exists := swl.windows[clientIP]
swl.mu.RUnlock()
if !exists {
window = &SlidingWindow{
limit: swl.limit,
window: swl.window,
}
swl.mu.Lock()
swl.windows[clientIP] = window
swl.mu.Unlock()
}
return window.AllowRequest()
}
// AllowRequest checks if request allowed in this window
func (sw *SlidingWindow) AllowRequest() bool {
sw.mu.Lock()
defer sw.mu.Unlock()
now := time.Now()
cutoff := now.Add(-sw.window)
// Remove old requests
var validRequests []time.Time
for _, reqTime := range sw.requests {
if reqTime.After(cutoff) {
validRequests = append(validRequests, reqTime)
}
}
sw.requests = validRequests
// Check if under limit
if len(sw.requests) < sw.limit {
sw.requests = append(sw.requests, now)
return true
}
return false
}
Lesson 14.5: Graceful Shutdown and Upgrades
Graceful Shutdown
// GracefulShutdown waits for connections to finish
func (s *Server) GracefulShutdown(timeout time.Duration) error {
s.logger.Info("starting graceful shutdown")
// Stop accepting new connections
if err := s.listener.Close(); err != nil {
s.logger.Error("error closing listener", zap.Error(err))
}
// Wait for active connections to finish
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
activeCount := s.getActiveConnectionCount()
if activeCount == 0 {
s.logger.Info("all connections closed")
return nil
}
s.logger.Info("waiting for connections to close",
zap.Int("active", activeCount),
zap.Duration("remaining", deadline.Sub(time.Now())),
)
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("timeout waiting for connections to close")
}
// ShutdownWithSignal handles shutdown signals
func (s *Server) ShutdownWithSignal() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
s.logger.Info("received shutdown signal")
if err := s.GracefulShutdown(30 * time.Second); err != nil {
s.logger.Error("graceful shutdown failed", zap.Error(err))
os.Exit(1)
}
s.logger.Info("server shutdown complete")
os.Exit(0)
}
Rolling Upgrades
// RollingUpgrade upgrades nodes one at a time
func (cluster *Cluster) RollingUpgrade(newImage string) error {
cluster.logger.Info("starting rolling upgrade", zap.String("image", newImage))
for i, node := range cluster.nodes {
cluster.logger.Info("upgrading node",
zap.Int("index", i),
zap.String("node", node.Address()),
)
// Remove node from load balancer
if err := cluster.deregisterNode(node); err != nil {
return fmt.Errorf("failed to deregister node %s: %w", node.Address(), err)
}
// Wait for in-flight requests to complete
cluster.logger.Info("waiting for in-flight requests", zap.String("node", node.Address()))
time.Sleep(10 * time.Second)
// Perform upgrade
if err := node.Upgrade(newImage); err != nil {
// Try to re-register node on failure
cluster.registerNode(node)
return fmt.Errorf("failed to upgrade node %s: %w", node.Address(), err)
}
// Wait for health check
cluster.logger.Info("waiting for node to become healthy", zap.String("node", node.Address()))
if err := node.WaitHealthy(30 * time.Second); err != nil {
// Try to re-register node on failure
cluster.registerNode(node)
return fmt.Errorf("node %s failed health check: %w", node.Address(), err)
}
// Add back to load balancer
if err := cluster.registerNode(node); err != nil {
return fmt.Errorf("failed to re-register node %s: %w", node.Address(), err)
}
cluster.logger.Info("node upgrade complete",
zap.Int("index", i),
zap.String("node", node.Address()),
)
}
cluster.logger.Info("rolling upgrade complete")
return nil
}
// CanaryUpgrade performs canary deployment
func (cluster *Cluster) CanaryUpgrade(newImage string, percentage int) error {
if percentage < 1 || percentage > 100 {
return fmt.Errorf("invalid percentage: %d", percentage)
}
canaryCount := len(cluster.nodes) * percentage / 100
if canaryCount == 0 {
canaryCount = 1
}
cluster.logger.Info("starting canary upgrade",
zap.String("image", newImage),
zap.Int("canary_count", canaryCount),
)
// Upgrade canary nodes
for i := 0; i < canaryCount; i++ {
node := cluster.nodes[i]
if err := cluster.upgradeNode(node, newImage); err != nil {
return fmt.Errorf("canary upgrade failed for node %s: %w", node.Address(), err)
}
}
// Monitor canary nodes
cluster.logger.Info("monitoring canary nodes", zap.Duration("duration", 5*time.Minute))
time.Sleep(5 * time.Minute)
// Check canary health
for i := 0; i < canaryCount; i++ {
node := cluster.nodes[i]
if !node.IsHealthy() {
return fmt.Errorf("canary node %s is unhealthy", node.Address())
}
}
cluster.logger.Info("canary upgrade successful, proceeding with full rollout")
return cluster.RollingUpgrade(newImage)
}
Lab 14.1: Security Implementation
Objective
Build a comprehensive security system with TLS encryption, authentication, authorization, rate limiting, and security hardening.
Requirements
-
TLS/SSL Encryption: End-to-end encrypted communication
-
Authentication: Password and JWT token authentication
-
Authorization: Role-based access control (RBAC)
-
Rate Limiting: Token bucket and sliding window limiters
-
Security Hardening: Input validation, secure defaults
-
Audit Logging: Security event logging and monitoring
Starter Code
// TODO: Implement TLS server
func (s *Server) StartTLS(certFile, keyFile string) error {
// Load TLS certificates
// Configure TLS settings
// Start TLS listener
return nil
}
// TODO: Implement authentication
func (s *Server) Authenticate(username, password string) (*User, error) {
// Validate credentials
// Return user if valid
return nil, nil
}
// TODO: Implement authorization
func (s *Server) AuthorizeRequest(user *User, operation string) error {
// Check user permissions
// Return error if not authorized
return nil
}
// TODO: Implement rate limiting
func (s *Server) CheckRateLimit(clientIP string) bool {
// Check if request is within rate limit
// Return true if allowed
return true
}
// TODO: Implement security middleware
func (s *Server) SecurityMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Rate limiting
// Authentication
// Authorization
// Input validation
next.ServeHTTP(w, r)
})
}
Test Template
func TestTLSConnection(t *testing.T) {
server := NewServer()
err := server.StartTLS("server.crt", "server.key")
assert.NoError(t, err)
// Test TLS client connection
client := NewClient()
err = client.ConnectTLS("localhost:6379", "server.crt")
assert.NoError(t, err)
}
func TestAuthentication(t *testing.T) {
server := NewServer()
// Create test user
err := server.CreateUser("testuser", "password123", []string{"read"})
assert.NoError(t, err)
// Test valid authentication
user, err := server.Authenticate("testuser", "password123")
assert.NoError(t, err)
assert.Equal(t, "testuser", user.Username)
// Test invalid authentication
_, err = server.Authenticate("testuser", "wrongpassword")
assert.Error(t, err)
}
func TestAuthorization(t *testing.T) {
server := NewServer()
// Create user with read-only permissions
user := &User{Username: "readonly", Roles: []string{"readonly"}}
// Test authorized operation
err := server.AuthorizeRequest(user, "GET")
assert.NoError(t, err)
// Test unauthorized operation
err = server.AuthorizeRequest(user, "PUT")
assert.Error(t, err)
}
func TestRateLimiting(t *testing.T) {
limiter := NewRateLimiter()
// Test within rate limit
for i := 0; i < 5; i++ {
allowed := limiter.AllowRequest("127.0.0.1", 10.0)
assert.True(t, allowed)
}
// Test rate limit exceeded
time.Sleep(100 * time.Millisecond)
allowed := limiter.AllowRequest("127.0.0.1", 0.1) // Very low rate
assert.False(t, allowed)
}
Acceptance Criteria
-
✅ TLS encryption works for all connections
-
✅ Password authentication with bcrypt hashing
-
✅ JWT token authentication and validation
-
✅ Role-based access control (RBAC)
-
✅ Rate limiting prevents abuse
-
✅ Input validation prevents injection attacks
-
✅ Security audit logging implemented
-
✅ Graceful shutdown handles connections
-
✅ > 90% code coverage
-
✅ All tests pass
Summary: Week 14 Complete
By completing Week 14, you’ve learned and implemented:
1. TLS/SSL Encryption
-
Certificate generation and management
-
TLS server and client implementation
-
Secure cipher suite configuration
-
Certificate validation
2. Authentication
-
Password-based authentication
-
JWT token authentication
-
Password hashing with bcrypt
-
User session management
3. Authorization
-
Role-based access control (RBAC)
-
Permission-based authorization
-
Resource and action mapping
-
Access control enforcement
4. Rate Limiting
-
Token bucket rate limiting
-
Sliding window rate limiting
-
Per-client rate limiting
-
DDoS protection
Key Skills Mastered:
-
✅ Implement end-to-end encryption with TLS
-
✅ Build secure authentication systems
-
✅ Design authorization and access control
-
✅ Implement rate limiting and DDoS protection
-
✅ Apply security hardening best practices
-
✅ Handle graceful shutdowns and upgrades
Ready for the Capstone Project?
Next week we’ll build a complete production-ready key-value database from scratch, incorporating all the concepts we’ve learned throughout the course.
Continue to Capstone Project →