Security Hardening: Complete Checklist for Production Applications

•13 min read•Subid Das
securitydevopsbest-practicesinfrastructurecompliance
Security Hardening: Complete Checklist for Production Applications

Security breaches cost millions. Your job is preventing them. This is a complete hardening checklist.

Layer 1: Authentication & Authorization#

Passwords#

# ❌ Bad: Storing plaintext
user.password = request.json['password']

# ✅ Good: Hash with bcrypt
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
user.password = hashed

# Verify
if bcrypt.checkpw(password.encode(), user.password):
    print("Correct password")

Multi-Factor Authentication (MFA)#

# Require TOTP (Time-based One-Time Password)
import pyotp

# Generate secret
secret = pyotp.random_base32()
user.mfa_secret = secret

# Client generates code
totp = pyotp.TOTP(secret)
code = totp.now()  # "123456"

# Verify
if totp.verify(code):
    user.mfa_enabled = True

Session Management#

# ❌ Bad: Long-lived tokens
token_lifetime = 365 * 24 * 60  # 1 year

# ✅ Good: Short-lived tokens + refresh tokens
access_token_lifetime = 15 * 60  # 15 minutes
refresh_token_lifetime = 7 * 24 * 60 * 60  # 7 days

# Rotate tokens
@app.route('/api/refresh', methods=['POST'])
def refresh_token():
    refresh = request.json['refresh_token']
    user = User.verify_refresh_token(refresh)
    
    access = create_access_token(user)
    new_refresh = create_refresh_token(user)
    
    return {
        "access_token": access,
        "refresh_token": new_refresh,
        "expires_in": 900
    }

OAuth 2.0 / OpenID Connect#

# Use OAuth for third-party integrations (don't store passwords)
from authlib.integrations.flask_client import OAuth

oauth = OAuth()
oauth.register(
    name='github',
    client_id='...',
    client_secret='...',
    authorize_url='https://github.com/login/oauth/authorize',
    access_token_url='https://github.com/login/oauth/access_token',
    server_metadata_url='https://github.com/.well-known/openid-configuration'
)

@app.route('/login/github')
def login_github():
    return oauth.github.authorize_redirect(redirect_uri=url_for('callback', _external=True))

Layer 2: Data Protection#

Encryption at Rest#

# AWS: Enable S3 encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "secrets" {
  bucket = aws_s3_bucket.secrets.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.secrets.arn
    }
  }
}

# Database: Enable encryption
resource "aws_db_instance" "postgres" {
  storage_encrypted = true
  kms_key_id        = aws_kms_key.rds.arn
}

Encryption in Transit#

# ✅ Always use HTTPS (TLS 1.3)
@app.route('/api/login', methods=['POST'])
def login():
    # Only accept over HTTPS
    if not request.is_secure:
        raise APIException("HTTPS_REQUIRED", "Use HTTPS", 400)
    
    # Harden headers
    response = make_response(...)
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
    return response

Secrets Management#

# ❌ Bad: Secrets in code
DB_PASSWORD = "super_secret_123"

# ✅ Good: Use secrets vault (AWS Secrets Manager, HashiCorp Vault)
import boto3

secrets_client = boto3.client('secretsmanager', region_name='us-east-1')
secret = secrets_client.get_secret_value(SecretId='prod/db-password')
DB_PASSWORD = json.loads(secret['SecretString'])['password']

# Rotate secrets regularly (90 days)

Database#

-- Encrypt sensitive columns
ALTER TABLE users
ADD COLUMN ssn_encrypted BYTEA;

-- Use parameterized queries (prevent SQL injection)
-- Python:
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
-- NOT: "SELECT * FROM users WHERE id = " + str(user_id)

-- Hash PII
UPDATE users SET email_hash = MD5(email);

Layer 3: Application Security#

Input Validation#

from marshmallow import Schema, fields, validate

class UserSchema(Schema):
    email = fields.Email(required=True)
    password = fields.Str(required=True, validate=validate.Length(min=12))
    age = fields.Int(validate=validate.Range(min=18, max=120))

@app.route('/api/register', methods=['POST'])
def register():
    schema = UserSchema()
    try:
        user_data = schema.load(request.json)
    except ValidationError as err:
        raise APIException("INVALID_INPUT", "Validation failed", 400, err.messages)
    
    user = User.create(**user_data)
    return user.to_dict(), 201

CSRF Protection#

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)

# Form includes CSRF token
@app.route('/api/orders', methods=['POST'])
@csrf.protect
def create_order():
    # CSRF token validated automatically
    order = Order.create(request.json)
    return order.to_dict(), 201

XSS Protection#

<!-- ❌ Bad: Unescaped user input -->
<div>Welcome {{ user.name }}</div>
<!-- If user.name = "<script>alert('xss')</script>", it executes! -->

<!-- ✅ Good: Escaped output -->
<div>Welcome {{ user.name | escape }}</div>
<!-- Rendered as: Welcome &lt;script&gt;...&lt;/script&gt; -->

SQL Injection Prevention#

# ❌ Bad: String concatenation
query = f"SELECT * FROM users WHERE id = {user_id}"

# ✅ Good: Parameterized queries
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

Layer 4: Infrastructure Security#

Network Security#

# Security Group: Principle of least privilege
resource "aws_security_group" "app" {
  # Only allow traffic from ALB
  ingress {
    from_port       = 3000
    to_port         = 3000
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  # Deny all by default
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# WAF: Block known attacks
resource "aws_wafv2_web_acl" "main" {
  name = "rate-limit"

  default_action {
    allow {}
  }

  rule {
    name     = "RateLimitRule"
    priority = 0

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 2000  # 2000 requests per 5 minutes
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "RateLimitMetric"
      sampled_requests_enabled   = true
    }
  }
}

DDoS Protection#

# AWS Shield Standard: Automatic protection
# AWS Shield Advanced: Enhanced protection + DDoS Response Team

resource "aws_shield_protection" "alb" {
  name          = "ALB-Shield"
  resource_arn  = aws_lb.main.arn
  protection_group_id = aws_shield_protection_group.main.id
}

Container Security#

# ❌ Bad: Run as root
FROM ubuntu:latest
RUN apt-get install -y app
CMD ["node", "app.js"]

# ✅ Good: Non-root user, minimal image
FROM node:18-alpine
RUN addgroup -g 1001 -S app
RUN adduser -S app -u 1001
COPY --chown=app:app . .
USER app
CMD ["node", "app.js"]

# Scan image for vulnerabilities
# docker scan myapp:latest
# trivy image myapp:latest

Layer 5: Dependency Management#

Vulnerability Scanning#

# npm audit (JavaScript)
npm audit

# pip audit (Python)
pip install pip-audit
pip-audit

# Dependabot (GitHub)
# Automated PRs for security updates

# Snyk
npm install -g snyk
snyk test

Lock Files#

# ✅ Commit lock files
git add package-lock.json
git add requirements.txt

# ❌ Don't allow automatic dependency upgrades in production

Dependency Pinning#

# ✅ Pin versions
requests==2.31.0
flask==2.3.2

# ❌ Avoid floating versions (security risk)
# requests>=2.0.0

Layer 6: Logging & Monitoring#

Security Logging#

import logging

security_logger = logging.getLogger('security')

# Log authentication events
@app.route('/api/login', methods=['POST'])
def login():
    user = User.authenticate(email, password)
    if user:
        security_logger.info(f"LOGIN_SUCCESS user={user.id} ip={request.remote_addr}")
    else:
        security_logger.warning(f"LOGIN_FAILED email={email} ip={request.remote_addr}")

Alert on Suspicious Activity#

# Alert if too many failed logins
@app.route('/api/login', methods=['POST'])
def login():
    failed_logins = cache.incr(f"failed_logins:{request.remote_addr}")
    
    if failed_logins > 10:
        # Alert and block
        security_logger.error(f"BRUTE_FORCE_DETECTED ip={request.remote_addr}")
        return {"error": "Too many attempts"}, 429

Security Checklist#

Authentication (✅ 100%)#

  • Password hashing (bcrypt, argon2)
  • Multi-factor authentication (TOTP)
  • Session management (short-lived tokens)
  • Rate limiting on login (prevent brute force)
  • Account lockout after failures

Data Protection (✅ 100%)#

  • HTTPS/TLS everywhere (TLS 1.3)
  • Encryption at rest (KMS, database encryption)
  • Secrets management (Secrets Manager, Vault)
  • PII hashing/masking
  • Secure password reset flow

Application Security (✅ 100%)#

  • Input validation (Marshmallow, Joi, Zod)
  • CSRF protection
  • XSS prevention (output escaping)
  • SQL injection prevention (parameterized queries)
  • Command injection prevention (no shell=True)

Infrastructure (✅ 100%)#

  • Network segmentation (Security Groups, VPCs)
  • WAF (Web Application Firewall)
  • DDoS protection (AWS Shield)
  • Non-root containers
  • File integrity monitoring

Dependency Management (✅ 100%)#

  • Automated vulnerability scanning (Dependabot, Snyk)
  • Lock files (package-lock.json, requirements.txt)
  • Regular updates (weekly)
  • Dependency pinning

Monitoring & Logging (✅ 100%)#

  • Security event logging
  • Centralized logging (ELK, Splunk)
  • Alerting on suspicious activity
  • Log retention (90+ days)
  • Audit trails

Compliance#

SOC 2 Type II#

  • Audit logging
  • Access controls
  • Encryption
  • Incident response

GDPR#

  • Data minimization (only collect needed data)
  • User consent
  • Right to deletion
  • Data breach notification (72 hours)

PCI-DSS (Payment Processing)#

  • No plaintext card data
  • Network segmentation
  • Vulnerability scanning
  • Encryption in transit & rest

Conclusion#

Security is a journey, not a destination. Start with authentication, move to data protection, then infrastructure hardening.

Use this checklist on every project. Automate what you can (scanning, updates). Audit regularly.

A breach costs millions. Prevention costs time.


Not confident your production stack would pass a real audit? Book a 15-minute call.

FAQ

Where should a security hardening effort start?

Authentication and authorization first — it's the layer attackers hit first and the one most likely to have a critical, exploitable gap (weak session handling, missing rate limits, broken access control).

How often should dependencies be scanned for vulnerabilities?

Continuously, via automated scanning in CI (Dependabot, Snyk, or equivalent) — not a quarterly manual review. New CVEs land daily; a scan that only runs occasionally leaves a known-exploitable window open.

Is a security checklist enough, or do I need a penetration test?

A checklist closes the common, known gaps (OWASP Top 10 class issues) cheaply. A penetration test finds what the checklist misses — the two are complementary, not substitutes for each other.

About the author

Subid Dasis a Forward Deployed Engineer & Systems Architect building high-reliability cloud platforms, AI automations, and custom business software. Explore more technical articles on the blog, or get in touch if you're working on something similar.

Recommended Reading

Continue exploring production engineering guides, systems architecture, and cloud patterns.

Let's
build.

Tell me what’s slowing your business down. We’ll work out what to build next.

Tell me about your project

A few sentences are enough to start. No technical brief needed.

Service, budget & timeline (optional) +
I need help with
Budget (optional)
Timeline (optional)
LET'S CONNECT