Introduction
Security isn’t a one‑time checklist; it’s a mindset that must be woven into every line of code, every CI/CD pipeline, and every deployment. In this post we’ll walk through practical techniques you can start using today to harden your applications-whether you’re building a simple Flask API or a full‑blown microservices ecosystem. We’ll cover threat modeling, secure defaults, runtime defenses, and automated testing, all backed by concrete code snippets you can drop into your projects.
TL;DR: Adopt a “shift‑left” approach, validate everything, use modern libraries, and let automation enforce the rules.
1. Threat Modeling Made Simple
Before you write a single line of code, ask yourself: what can go wrong? The classic OWASP Top 10 is a great starting point, but a lightweight threat model helps you prioritize.
graph LR
A[User Input] --> B[API Endpoint]
B --> C{Validation}
C -->|Pass| D[Business Logic]
C -->|Fail| E[Reject Request]
D --> F[Data Store]
F --> G[Response]
- Identify assets - e.g., user credentials, financial data.
- Identify entry points - public APIs, file uploads, third‑party callbacks.
- Identify threats - injection, broken authentication, data leakage.
Document this in a markdown table:
| Asset | Entry Point | Threat | Mitigation |
|---|---|---|---|
| JWT token |
Authorization header |
Token replay | Short expiry + rotating secret |
| User avatar | File upload | Malicious script | Content‑type whitelist, scanning |
| Payment info | POST /pay
|
SQL injection | Parameterized queries |
2. Secure Defaults in Code
2.1. Use Safe Libraries
Never roll your own cryptography. In Python, stick with cryptography or hashlib for hashing.
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import os, base64
def hash_password(password: str) -> str:
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100_000,
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
return f"{base64.b64encode(salt).decode()}${key.decode()}"
Why it matters: Using a well‑vetted KDF (Key Derivation Function) protects against brute‑force attacks and eliminates common pitfalls like low‑entropy salts.
2.2. Enforce HTTPS Everywhere
Never serve mixed content. In Flask, you can force HTTPS with a simple middleware:
from flask import Flask, request, redirect
app = Flask(__name__)
@app.before_request
def enforce_https():
if request.headers.get('X-Forwarded-Proto', 'http') != 'https':
url = request.url.replace('http://', 'https://')
return redirect(url, code=301)
Deploy behind a reverse proxy (NGINX, Traefik) that terminates TLS, and set Strict-Transport-Security headers:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
3. Input Validation & Output Encoding
3.1. Validate All Inputs
Never trust client data. Use schema validation libraries like pydantic:
from pydantic import BaseModel, EmailStr, constr
class RegisterPayload(BaseModel):
username: constr(min_length=3, max_length=30, regex=r'^\w+$')
email: EmailStr
password: constr(min_length=8)
FastAPI automatically validates the request body against this model and returns a 422 response if anything is off.
3.2. Encode on Output
Cross‑site scripting (XSS) is mitigated by escaping data when rendering HTML. In Jinja2, enable autoescaping:
from flask import Flask, render_template
app = Flask(__name__)
app.jinja_env.autoescape = True
If you must embed user‑generated data into JavaScript, JSON‑encode it:
<script>
const user = {{ user|tojson|safe }};
</script>
4. Authentication & Authorization
4.1. JWT Best Practices
- Use asymmetric keys (RS256) for signing.
- Keep the token lifetime short (5‑15 minutes) and rotate refresh tokens.
- Store tokens in httpOnly, secure cookies to mitigate XSS.
import jwt
from datetime import datetime, timedelta
def create_access_token(user_id: str, private_key: str) -> str:
payload = {
"sub": user_id,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(minutes=10),
"iss": "myapp"
}
return jwt.encode(payload, private_key, algorithm="RS256")
4.2. Role‑Based Access Control (RBAC)
Define a central permission map:
PERMISSIONS = {
"admin": {"read:any", "write:any", "delete:any"},
"editor": {"read:any", "write:own"},
"viewer": {"read:any"},
}
Middleware checks the claim against the required permission:
def require_permission(permission):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
token = get_jwt_from_request()
user_perms = PERMISSIONS.get(token["role"], set())
if permission not in user_perms:
abort(403, "Insufficient privileges")
return fn(*args, **kwargs)
return wrapper
return decorator
5. Runtime Defenses
5.1. Security Headers
Add the following headers to every response:
| Header | Value |
|---|---|
Content-Security-Policy |
default-src 'self'; script-src 'self' |
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Referrer-Policy |
strict-origin-when-cross-origin |
In Flask:
@app.after_request
def set_security_headers(response):
response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
5.2. Rate Limiting
Prevent brute‑force attacks with token bucket algorithms. Flask-Limiter makes it painless:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(key_func=get_remote_address, default_limits=["200 per day", "50 per hour"])
limiter.init_app(app)
@app.route("/login", methods=["POST"])
@limiter.limit("5 per minute")
def login():
# authentication logic
pass
6. Automated Security Testing
6.1. Static Analysis
Integrate tools like Bandit (Python) into your CI pipeline:
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Bandit
run: pip install bandit
- name: Run Bandit
run: bandit -r . -lll
6.2. Dependency Scanning
Leverage Dependabot (built‑in on GitHub) or Snyk for continuous vulnerability alerts.
6.3. Dynamic Scanning
Run a lightweight OWASP ZAP scan against a staging environment after deployment:
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://staging.myapp.com -r zap_report.html
Fail the pipeline if the report contains high severity findings.
7. Incident Response Basics
Even with best practices, breaches happen. Prepare a concise run‑book:
- Identify - Pull logs from CloudWatch / ELK, look for anomalies.
- Contain - Disable compromised credentials, rotate secrets.
- Eradicate - Patch vulnerable code, run a fresh scan.
- Recover - Redeploy from a known‑good image, verify integrity.
- Post‑mortem - Document lessons, adjust policies.
Automation can speed up steps 2‑3. For example, a Lambda function that revokes all JWT refresh tokens for a user when a suspicious login is detected.
Conclusion
Security is a continuous journey, not a destination. By embedding threat modeling, secure defaults, rigorous validation, strong auth, runtime hardening, and automated testing into your development lifecycle, you dramatically reduce risk while keeping velocity high. Remember: shift left, automate, and never assume the client is trustworthy.
Recommended Tools
If you’re looking for a hassle‑free way to keep your codebase secure, I’ve been using Snyk’s Developer Security Platform for automated dependency scanning and container hardening. Their free tier is generous, and the paid plans integrate seamlessly with GitHub Actions and GitLab CI.
For runtime protection, especially around API gateways, I recommend the Imperva Cloud WAF. It offers out‑of‑the‑box OWASP Top 10 rule sets, bot mitigation, and real‑time analytics-perfect for teams that want enterprise‑grade security without managing infrastructure.
Happy coding, and stay secure!
Top comments (0)