Skip to content

Security Guide

Comprehensive security guidelines and best practices for the Go REST API Boilerplate (GRAB).


🔐 JWT Secret Management

JWT secrets are critical for token security. This section covers proper generation, storage, and validation.

Security Requirements

All Environments

  • Minimum Length: 32 characters
  • Cryptographically Random: Generated using secure methods (not predictable)
  • Never Committed: Must never be checked into version control
  • Unique per Environment: Different secrets for development, staging, and production

All Environments Requirements

  • Minimum Length: 32+ characters for all environments
  • Recommended Length: 64 characters (auto-generated by make generate-jwt-secret)
  • Rotation Policy: Rotate secrets periodically (recommended: every 90 days)

Production Additional Requirements

  • SSL Required: Database must use SSL (sslmode: require)
  • Strong Passwords: Database passwords required

🔑 Generating JWT Secrets

Quick Start with Auto-Generation

make quick-start

The quick-start command automatically generates a secure 32+ character JWT secret if missing or empty.

Generate or Check JWT Secret

make generate-jwt-secret

Behavior:

  • If JWT_SECRET exists in .env: Displays confirmation message (doesn't regenerate)
  • If JWT_SECRET is missing: Generates and automatically saves to .env
  • Secret length: 64 characters (base64 encoded from 48 random bytes)

Example Output (when JWT_SECRET missing):

🔐 Generating JWT secret...
✅ JWT_SECRET generated and saved to .env

⚠️  NEVER commit .env to git!

Example Output (when JWT_SECRET exists):

✅ JWT_SECRET already exists in .env
💡 Current value is set (not displayed for security)

To regenerate, remove the current JWT_SECRET line from .env first

Manual Generation Methods

Generate a secure 32+ character secret:

openssl rand -base64 48

Using /dev/urandom

head -c 48 /dev/urandom | base64 | tr -d '\n'

Using Python

python3 -c "import secrets; print(secrets.token_urlsafe(48))"

Using Node.js

node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"

🔐 RBAC Security Best Practices

Admin Account Management

Principle of Least Privilege

  • Minimize admin accounts: Only promote trusted users to admin role
  • Regular audits: Review admin accounts quarterly
  • Immediate revocation: Remove admin privileges when no longer needed
  • Separation of duties: Use specialized roles instead of blanket admin access where possible

Secure Admin Creation

# Always use the CLI - never hardcode credentials
make create-admin

# Promote existing users only after verification
make promote-admin ID=<user_id>

Never: - ❌ Hardcode admin credentials in code or environment files - ❌ Use default/shared admin passwords - ❌ Create admins through API endpoints (no admin creation API exists by design) - ❌ Store admin credentials in version control

Role Assignment Security

Token-Based Role Validation

Roles are embedded in JWT tokens and validated on every request:

  1. Stateless validation: No database lookup required for role checks
  2. Token expiration: Roles cached only until token expires (15 minutes default)
  3. Re-authentication required: Role changes require user to login again
  4. Atomic operations: Role assignments use database transactions

Protection Against Role Elevation

// ✅ Good: Middleware enforces role requirements
adminGroup.Use(middleware.RequireAdmin())

// ❌ Bad: Manual role checks can be bypassed
if user.Email == "admin@example.com" {
    // Fragile and insecure
}

Database-Level Security

Role Integrity

  • Foreign keys: Ensure referential integrity between users and roles
  • Composite primary keys: Prevent duplicate role assignments
  • CASCADE deletes: Automatically clean up role assignments when users/roles deleted
  • Seeded roles: Default roles (user, admin) inserted via migrations, not application code

Audit Logging Recommendations

While GRAB includes structured logging, consider adding admin action auditing:

// Log admin actions for security monitoring
log.Info().
    Uint("admin_id", contextutil.GetUserID(c)).
    Str("action", "delete_user").
    Uint("target_user_id", targetID).
    Msg("Admin action performed")

Production Deployment Checklist

Before deploying RBAC to production:

  • All admin accounts created through secure CLI process
  • Admin passwords meet strength requirements (8+ chars, mixed case, numbers, symbols)
  • JWT secrets rotated and unique per environment
  • Database connections use SSL (sslmode: require)
  • Admin actions monitored in application logs
  • Regular security audits scheduled (quarterly recommended)
  • Backup admin account created and credentials stored securely
  • Role assignment process documented for your team

Token Security with Roles

Refresh Token Rotation

When users receive new roles, they must obtain a new access token:

# After promoting user to admin
POST /api/v1/auth/refresh
# User's new access token will contain updated roles

Token Leakage Mitigation

  • Short-lived access tokens: 15 minutes default (configurable)
  • Refresh token rotation: New refresh token issued on each refresh
  • Token family tracking: Detects refresh token reuse attacks
  • Immediate invalidation: Logout revokes all tokens for user

See Authentication Guide for complete token security details.


📝 Environment Configuration

.env File Setup

1. Copy Template:

cp .env.example .env

2. Generate Secret:

make generate-jwt-secret

3. Add to .env:

JWT_SECRET=xKyLmNpQrStUvWzAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

Environment Validation

Check your environment configuration:

make check-env

Example Output:

✅ .env file exists
✅ JWT_SECRET is set
✅ JWT_SECRET length: 64 characters
✅ DATABASE_PASSWORD is set


⚠️ Common Security Mistakes

❌ DON'T: Use Weak Secrets

# BAD - Too short (< 32 characters)
JWT_SECRET=mysecret

# BAD - Predictable pattern
JWT_SECRET=my-secret-key-that-is-only-32-char

# BAD - Not cryptographically random
JWT_SECRET=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

✅ DO: Use Strong Secrets

# GOOD - Cryptographically random, 64 chars (auto-generated)
JWT_SECRET=xKyLmNpQrStUvWzAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ

# GOOD - Generated with openssl (48 bytes = 64 chars base64)
JWT_SECRET=$(openssl rand -base64 48)

# GOOD - Generated with make command (auto-saved to .env)
make generate-jwt-secret

❌ DON'T: Commit Secrets to Git

# config.yaml - BAD
jwt:
  secret: "my-hardcoded-secret"  # ❌ Never do this
# .env - BAD
JWT_SECRET=hardcoded-value
git add .env  # ❌ Never commit .env files

✅ DO: Use Environment Variables

# config.yaml - GOOD
jwt:
  # No secret field - must be set via JWT_SECRET env var ✅
  access_token_ttl: "15m"
  refresh_token_ttl: "168h"
# .env - GOOD (not committed to git)
JWT_SECRET=$(openssl rand -base64 96)

# .gitignore - GOOD
.env  # ✅ Always ignored

🔄 Secret Rotation

When to Rotate Secrets

  • Scheduled: Every 90 days (production)
  • Security Incident: Immediately if compromise suspected
  • Team Changes: When team members with access leave
  • Compliance: As required by security policies

Rotation Process

1. Generate New Secret:

make generate-jwt-secret

2. Update Environment:

# Update .env file with new secret
JWT_SECRET=<new-secret-value>

3. Restart Application:

make restart

4. Verify:

curl http://localhost:8080/health

Note: Existing JWT tokens will become invalid after rotation. Users will need to re-authenticate.


🏗️ Production Deployment

Environment Setup Checklist

  • Generate JWT secret (32+ characters, 64 recommended)
  • Set JWT_SECRET environment variable
  • Enable SSL for database (DATABASE_SSLMODE=require)
  • Set DATABASE_PASSWORD via environment variable
  • Verify APP_ENVIRONMENT=production
  • Disable debug mode (APP_DEBUG=false)
  • Run make check-env to validate configuration

Docker Deployment

docker-compose.prod.yml:

services:
  app:
    environment:
      - APP_ENVIRONMENT=production
      - JWT_SECRET=${JWT_SECRET}  # From host environment
      - DATABASE_PASSWORD=${DATABASE_PASSWORD}
      - DATABASE_SSLMODE=require

Start Production:

# Set secrets in shell
export JWT_SECRET=$(openssl rand -base64 96)
export DATABASE_PASSWORD=$(openssl rand -base64 32)

# Start services
docker-compose -f docker-compose.prod.yml up -d

Kubernetes Deployment

Create Secret:

kubectl create secret generic app-secrets \
  --from-literal=jwt-secret=$(openssl rand -base64 96) \
  --from-literal=database-password=$(openssl rand -base64 32)

Deployment YAML:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: grab-api
spec:
  template:
    spec:
      containers:
      - name: api
        env:
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: jwt-secret
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-password


🔍 Security Validation

Startup Validation

The application validates security configuration on startup:

Validation Checks: 1. JWT_SECRET is present 2. JWT_SECRET length ≥ 32 characters (64+ for production) 3. DATABASE_PASSWORD is set in production 4. SSL is enabled in production

Example Validation Error:

ERROR Failed to load configuration error="JWT_SECRET must be at least 32 characters (current: 16)
Generate secure secret: make generate-jwt-secret"

Runtime Security

Token Validation: - All protected endpoints validate JWT tokens - Expired tokens are automatically rejected - Invalid signatures are rejected - Token refresh requires valid refresh token

Rate Limiting: - Optional rate limiting per endpoint - Configurable via RATELIMIT_ENABLED, RATELIMIT_REQUESTS, RATELIMIT_WINDOW


📚 Additional Security Resources

OWASP Guidelines

12-Factor App Methodology

  • Config - Store config in environment

🆘 Support

If you have security concerns or questions:

  1. Security Issues: Report via GitHub Security Advisories
  2. General Questions: Open an issue
  3. Documentation: Check full documentation