1. go
  2. /best practices
  3. /security

Security Best Practices in Go

Security is a critical aspect of any application. This guide covers best practices for implementing security in Go applications.

Authentication

1. Password Handling

Secure password storage and validation:

import (
    "golang.org/x/crypto/bcrypt"
)

// Hash password before storage
func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return "", fmt.Errorf("failed to hash password: %w", err)
    }
    return string(bytes), nil
}

// Verify password
func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

// Password validation
func ValidatePassword(password string) error {
    if len(password) < 8 {
        return errors.New("password must be at least 8 characters")
    }
    
    var (
        hasUpper   bool
        hasLower   bool
        hasNumber  bool
        hasSpecial bool
    )
    
    for _, char := range password {
        switch {
        case unicode.IsUpper(char):
            hasUpper = true
        case unicode.IsLower(char):
            hasLower = true
        case unicode.IsNumber(char):
            hasNumber = true
        case unicode.IsPunct(char) || unicode.IsSymbol(char):
            hasSpecial = true
        }
    }
    
    if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
        return errors.New("password must contain upper, lower, number, and special characters")
    }
    
    return nil
}

2. JWT Authentication

Implement JWT-based authentication:

import (
    "github.com/golang-jwt/jwt/v4"
)

type Claims struct {
    UserID string `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func GenerateToken(userID, role string, secret []byte) (string, error) {
    claims := Claims{
        UserID: userID,
        Role:   role,
        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(secret)
}

func ValidateToken(tokenString string, secret []byte) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, 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 secret, nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, errors.New("invalid token")
}

Authorization

1. Role-Based Access Control (RBAC)

Implement RBAC:

type Permission string

const (
    PermissionRead   Permission = "read"
    PermissionWrite  Permission = "write"
    PermissionDelete Permission = "delete"
)

type Role struct {
    Name        string
    Permissions []Permission
}

type RBAC struct {
    roles map[string]Role
}

func NewRBAC() *RBAC {
    return &RBAC{
        roles: make(map[string]Role),
    }
}

func (r *RBAC) AddRole(role Role) {
    r.roles[role.Name] = role
}

func (r *RBAC) HasPermission(role string, permission Permission) bool {
    if r, ok := r.roles[role]; ok {
        for _, p := range r.Permissions {
            if p == permission {
                return true
            }
        }
    }
    return false
}

// Middleware for RBAC
func RBACMiddleware(rbac *RBAC, requiredPermission Permission) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims, ok := r.Context().Value("claims").(*Claims)
            if !ok {
                http.Error(w, "unauthorized", http.StatusUnauthorized)
                return
            }
            
            if !rbac.HasPermission(claims.Role, requiredPermission) {
                http.Error(w, "forbidden", http.StatusForbidden)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

2. Context-Based Authorization

Implement context-based authorization:

type AuthorizationContext struct {
    UserID string
    Role   string
    Scopes []string
}

type Resource struct {
    ID      string
    OwnerID string
    Type    string
}

type AuthorizationService struct {
    rbac *RBAC
}

func (s *AuthorizationService) CanAccess(ctx AuthorizationContext, resource Resource, action string) bool {
    // Check ownership
    if resource.OwnerID == ctx.UserID {
        return true
    }
    
    // Check role-based permissions
    if s.rbac.HasPermission(ctx.Role, Permission(action)) {
        return true
    }
    
    // Check scopes
    for _, scope := range ctx.Scopes {
        if scope == fmt.Sprintf("%s:%s", resource.Type, action) {
            return true
        }
    }
    
    return false
}

Data Protection

1. Encryption

Implement data encryption:

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
)

type Encryption struct {
    key []byte
}

func NewEncryption(key []byte) *Encryption {
    return &Encryption{key: key}
}

func (e *Encryption) Encrypt(data []byte) ([]byte, error) {
    block, err := aes.NewCipher(e.key)
    if err != nil {
        return nil, err
    }
    
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }
    
    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, err
    }
    
    return gcm.Seal(nonce, nonce, data, nil), nil
}

func (e *Encryption) Decrypt(data []byte) ([]byte, error) {
    block, err := aes.NewCipher(e.key)
    if err != nil {
        return nil, err
    }
    
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }
    
    nonceSize := gcm.NonceSize()
    if len(data) < nonceSize {
        return nil, errors.New("ciphertext too short")
    }
    
    nonce, ciphertext := data[:nonceSize], data[nonceSize:]
    return gcm.Open(nil, nonce, ciphertext, nil)
}

2. Input Validation

Implement input validation:

import (
    "github.com/go-playground/validator/v10"
)

type UserInput struct {
    Username string `validate:"required,min=3,max=50,alphanum"`
    Email    string `validate:"required,email"`
    Age      int    `validate:"required,gte=18,lte=120"`
}

func ValidateInput(input interface{}) error {
    validate := validator.New()
    return validate.Struct(input)
}

// Custom validation
func ValidateSQL(fl validator.FieldLevel) bool {
    value := fl.Field().String()
    // Check for SQL injection patterns
    dangerousPatterns := []string{"DROP", "DELETE", "UPDATE", "INSERT", "--", "/*", "*/"}
    for _, pattern := range dangerousPatterns {
        if strings.Contains(strings.ToUpper(value), pattern) {
            return false
        }
    }
    return true
}

HTTP Security

1. Secure Headers

Implement secure HTTP headers:

func SecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Security headers
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        
        next.ServeHTTP(w, r)
    })
}

2. CSRF Protection

Implement CSRF protection:

import (
    "github.com/gorilla/csrf"
)

func main() {
    CSRF := csrf.Protect(
        []byte("32-byte-long-auth-key"),
        csrf.Secure(true),
        csrf.HttpOnly(true),
    )
    
    router := mux.NewRouter()
    router.Use(CSRF)
    
    http.ListenAndServe(":8000", router)
}

func formHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-CSRF-Token", csrf.Token(r))
    // Handle form
}

Logging and Monitoring

1. Security Logging

Implement security event logging:

type SecurityEvent struct {
    Timestamp   time.Time
    EventType   string
    UserID      string
    IP          string
    Action      string
    Status      string
    Description string
}

func LogSecurityEvent(event SecurityEvent) {
    log.Info().
        Time("timestamp", event.Timestamp).
        Str("event_type", event.EventType).
        Str("user_id", event.UserID).
        Str("ip", event.IP).
        Str("action", event.Action).
        Str("status", event.Status).
        Str("description", event.Description).
        Msg("security event")
}

// Usage
func LoginHandler(w http.ResponseWriter, r *http.Request) {
    success := authenticate(r)
    
    LogSecurityEvent(SecurityEvent{
        Timestamp:   time.Now(),
        EventType:   "authentication",
        UserID:      r.Form.Get("username"),
        IP:          r.RemoteAddr,
        Action:      "login",
        Status:      fmt.Sprintf("%v", success),
        Description: "User login attempt",
    })
}

2. Rate Limiting

Implement rate limiting:

import (
    "golang.org/x/time/rate"
)

type IPRateLimiter struct {
    ips map[string]*rate.Limiter
    mu  *sync.RWMutex
    r   rate.Limit
    b   int
}

func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
    return &IPRateLimiter{
        ips: make(map[string]*rate.Limiter),
        mu:  &sync.RWMutex{},
        r:   r,
        b:   b,
    }
}

func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter {
    i.mu.Lock()
    defer i.mu.Unlock()
    
    limiter := rate.NewLimiter(i.r, i.b)
    i.ips[ip] = limiter
    
    return limiter
}

func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
    i.mu.Lock()
    limiter, exists := i.ips[ip]
    
    if !exists {
        i.mu.Unlock()
        return i.AddIP(ip)
    }
    
    i.mu.Unlock()
    return limiter
}

func RateLimitMiddleware(limiter *IPRateLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.GetLimiter(r.RemoteAddr).Allow() {
                http.Error(w, "too many requests", http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Next Steps