Essential Security Practices to Protect Your Web Applications
Practical, easy-to-apply security improvements for any online project — from security headers, rate limiting, login protection, to safe file uploads and more.
Essential Security Practices to Protect Your Web Applications
Security is not a one-time setup — it's a continuous habit. Many developers assume security only matters for large systems, but even small personal projects can be attacked within minutes of going live.
This guide provides practical, actionable security practices you can implement immediately, regardless of your tech stack. These steps are framework-agnostic and apply to:
- Next.js, React, Vue, Angular
- Express, Fastify, NestJS
- Nginx, Apache, Caddy
- Cloudflare, Vercel, AWS, Azure
- Any Linux-based deployment
Let's build a strong security baseline that protects against the most common attacks.
1) Add Essential Security Headers
Security headers are your first line of defense against browser-based attacks. Without them, your application is vulnerable to:
| Attack Type | What It Does | Impact |
|---|---|---|
| XSS (Cross-Site Scripting) | Injects malicious scripts into your pages | Steals cookies, session tokens, user data |
| Clickjacking | Tricks users into clicking hidden elements | Unauthorized actions, data theft |
| MIME Sniffing | Browser guesses file type incorrectly | Executes malicious files as scripts |
| Referrer Leakage | Exposes sensitive URLs in referrer headers | Information disclosure |
| Feature Abuse | Unauthorized access to camera, microphone, etc. | Privacy violations |
Security Headers Reference Table
| Header | Purpose | Recommended Value | When to Use |
|---|---|---|---|
| Strict-Transport-Security (HSTS) | Forces HTTPS connections | max-age=31536000; includeSubDomains; preload | All HTTPS sites |
| X-Frame-Options | Prevents clickjacking | DENY (or SAMEORIGIN if you need iframes) | All pages |
| X-Content-Type-Options | Prevents MIME sniffing | nosniff | All pages |
| Referrer-Policy | Controls referrer information | strict-origin-when-cross-origin | All pages |
| Permissions-Policy | Restricts browser features | camera=(), microphone=(), geolocation=() | Customize per app |
| Content-Security-Policy (CSP) | Prevents XSS attacks | default-src 'self'; script-src 'self' | Must customize per project |
Implementation Examples
Next.js (next.config.js)
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
},
],
},
];
},
};
module.exports = nextConfig;
Nginx Configuration
server {
listen 443 ssl;
server_name example.com;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self';" always;
# Your proxy_pass or root configuration
location / {
proxy_pass http://localhost:3000;
}
}
Express.js Middleware
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self';");
next();
});
Cookie Security Best Practices
| Attribute | Purpose | Recommended Value | Example |
|---|---|---|---|
| Secure | Only sent over HTTPS | Always set | Secure |
| HttpOnly | Not accessible via JavaScript | Always set (except for client-side tokens) | HttpOnly |
| SameSite | Prevents CSRF attacks | Strict (or Lax for cross-site) | SameSite=Strict |
| Domain | Restricts cookie scope | Set only if needed | Domain=.example.com |
| Path | Restricts cookie path | Set to specific path | Path=/api |
Example Cookie Implementation:
// Express.js example
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
});
2) Apply Rate Limiting for All Requests
Rate limiting prevents abuse and protects your server from being overwhelmed. Here's a comprehensive approach:
Rate Limiting Strategy Table
| Endpoint Type | Rate Limit | Window | Action on Exceed |
|---|---|---|---|
| Static Assets | 100 req/min | Per IP | Return 429 (Too Many Requests) |
| API Endpoints | 30 req/min | Per IP | Return 429 with retry-after header |
| Login/Auth | 5 req/min | Per IP | Lock account after 5 failures |
| Password Reset | 3 req/hour | Per IP | Block for 1 hour |
| File Upload | 10 req/hour | Per IP | Block for 1 hour |
Implementation Examples
Express.js with express-rate-limit
const rateLimit = require('express-rate-limit');
// General API rate limiter
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 30, // 30 requests per minute
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
// Strict login limiter
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per 15 minutes
message: 'Too many login attempts, please try again later.',
skipSuccessfulRequests: true,
});
app.use('/api/', apiLimiter);
app.use('/api/auth/login', loginLimiter);
Next.js API Route with Rate Limiting
// lib/rateLimit.ts
import { LRUCache } from 'lru-cache';
type Options = {
uniqueTokenPerInterval?: number;
interval?: number;
};
export function rateLimit(options?: Options) {
const tokenCache = new LRUCache({
max: options?.uniqueTokenPerInterval || 500,
ttl: options?.interval || 60000,
});
return {
check: (limit: number, token: string) =>
new Promise<void>((resolve, reject) => {
const tokenCount = (tokenCache.get(token) as number[]) || [0];
if (tokenCount[0] === 0) {
tokenCache.set(token, tokenCount);
}
tokenCount[0] += 1;
const currentUsage = tokenCount[0];
const isRateLimited = currentUsage >= limit;
return isRateLimited ? reject() : resolve();
}),
};
}
// app/api/login/route.ts
import { rateLimit } from '@/lib/rateLimit';
import { NextResponse } from 'next/server';
const limiter = rateLimit({
interval: 60 * 1000, // 60 seconds
uniqueTokenPerInterval: 500, // Max 500 users per interval
});
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
try {
await limiter.check(5, ip); // 5 requests per minute
// Your login logic here
return NextResponse.json({ success: true });
} catch {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
}
Nginx Rate Limiting
http {
# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
server {
location /api/ {
limit_req zone=api_limit burst=5 nodelay;
proxy_pass http://backend;
}
location /api/auth/login {
limit_req zone=login_limit burst=2 nodelay;
proxy_pass http://backend;
}
}
}
Login Protection Best Practices
❌ Bad: Reveals Account Existence
// DON'T DO THIS
if (!user) {
return res.status(404).json({ error: 'Email not found' });
}
if (!isValidPassword) {
return res.status(401).json({ error: 'Incorrect password' });
}
✅ Good: Generic Error Messages
// DO THIS INSTEAD
async function login(req, res) {
const { email, password } = req.body;
// Always perform both checks, even if user doesn't exist
const user = await User.findOne({ email });
const isValidPassword = user
? await bcrypt.compare(password, user.password)
: false;
// Generic error message regardless of which check failed
if (!user || !isValidPassword) {
// Log the attempt for monitoring
await logFailedLoginAttempt(req.ip, email);
// Return generic error
return res.status(401).json({
error: 'Invalid email or password'
});
}
// Success logic
const token = generateToken(user);
res.json({ token });
}
CAPTCHA Integration
| CAPTCHA Type | Best For | Implementation Difficulty | User Experience |
|---|---|---|---|
| reCAPTCHA v2 | Login forms | Easy | Checkbox (visible) |
| reCAPTCHA v3 | Background protection | Medium | Invisible (score-based) |
| hCaptcha | Privacy-focused | Easy | Checkbox (visible) |
| Cloudflare Turnstile | Modern alternative | Easy | Invisible or checkbox |
Example: reCAPTCHA v3 in Next.js
// app/api/login/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email, password, recaptchaToken } = await request.json();
// Verify reCAPTCHA
const recaptchaResponse = await fetch(
`https://www.google.com/recaptcha/api/siteverify`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `secret=${process.env.RECAPTCHA_SECRET}&response=${recaptchaToken}`,
}
);
const recaptchaData = await recaptchaResponse.json();
if (recaptchaData.score < 0.5) {
return NextResponse.json(
{ error: 'reCAPTCHA verification failed' },
{ status: 400 }
);
}
// Continue with login logic
// ...
}
3) Protect File Upload Endpoints
File uploads are one of the most dangerous features if not properly secured. Follow this multi-layered approach:
File Upload Security Checklist
| Security Layer | What It Prevents | Implementation |
|---|---|---|
| Extension Whitelist | Executable files | Check file extension against allowed list |
| Magic Byte Validation | Disguised files | Verify actual file signature |
| Size Limits | DoS attacks | Enforce maximum file size |
| Virus Scanning | Malware | Scan with ClamAV or cloud service |
| Isolated Storage | Direct execution | Store outside web root |
| Content-Type Validation | MIME spoofing | Verify Content-Type header |
Allowed File Types Reference
| File Category | Allowed Extensions | Max Size | Magic Bytes (Hex) |
|---|---|---|---|
| Images | jpg, jpeg, png, gif, webp | 5 MB | JPG: FF D8 FF<br>PNG: 89 50 4E 47<br>GIF: 47 49 46 38 |
| Documents | pdf, docx, xlsx | 20 MB | PDF: 25 50 44 46 |
| Archives | zip (if needed) | 50 MB | ZIP: 50 4B 03 04 |
Implementation Example
// lib/fileUploadValidator.js
const fs = require('fs');
const path = require('path');
// Magic bytes for common file types
const MAGIC_BYTES = {
'image/jpeg': [0xFF, 0xD8, 0xFF],
'image/png': [0x89, 0x50, 0x4E, 0x47],
'image/gif': [0x47, 0x49, 0x46, 0x38],
'application/pdf': [0x25, 0x50, 0x44, 0x46],
};
const ALLOWED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'application/pdf': ['.pdf'],
};
const MAX_SIZES = {
'image/jpeg': 5 * 1024 * 1024, // 5MB
'image/png': 5 * 1024 * 1024,
'image/gif': 5 * 1024 * 1024,
'application/pdf': 20 * 1024 * 1024, // 20MB
};
async function validateFile(file) {
const errors = [];
// 1. Check extension
const ext = path.extname(file.originalname).toLowerCase();
const allowedExts = Object.values(ALLOWED_TYPES).flat();
if (!allowedExts.includes(ext)) {
errors.push(`File type ${ext} is not allowed`);
}
// 2. Check size
const mimeType = file.mimetype;
if (file.size > MAX_SIZES[mimeType]) {
errors.push(`File size exceeds ${MAX_SIZES[mimeType] / 1024 / 1024}MB limit`);
}
// 3. Verify magic bytes
const buffer = fs.readFileSync(file.path);
const expectedBytes = MAGIC_BYTES[mimeType];
if (expectedBytes) {
const matches = expectedBytes.every((byte, index) => buffer[index] === byte);
if (!matches) {
errors.push('File signature does not match declared type');
}
}
// 4. Sanitize filename
const sanitizedFilename = file.originalname
.replace(/[^a-zA-Z0-9.-]/g, '_')
.substring(0, 255);
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true, sanitizedFilename };
}
// Express.js upload handler
const multer = require('multer');
const upload = multer({
dest: '/var/uploads/private/', // Outside web root
limits: { fileSize: 20 * 1024 * 1024 }, // 20MB max
fileFilter: (req, file, cb) => {
const allowedMimes = Object.keys(ALLOWED_TYPES);
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
}
},
});
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
const validation = await validateFile(req.file);
if (!validation.valid) {
fs.unlinkSync(req.file.path); // Delete invalid file
return res.status(400).json({ errors: validation.errors });
}
// Move to final location with sanitized name
const finalPath = `/var/uploads/private/${Date.now()}-${validation.sanitizedFilename}`;
fs.renameSync(req.file.path, finalPath);
// Store file metadata in database
// Serve through secure API endpoint, not direct file access
res.json({ success: true, fileId: finalPath });
} catch (error) {
res.status(500).json({ error: 'Upload failed' });
}
});
Storage Best Practices
| Approach | Security Level | Implementation |
|---|---|---|
| Public Directory | ❌ Very Low | Files directly accessible via URL |
| Private Directory + API | ✅ High | Files served through authenticated endpoint |
| Cloud Storage (S3) | ✅✅ Very High | Pre-signed URLs, access control |
| Database Storage | ✅ Medium | Base64 or binary in database (not recommended for large files) |
4) Always Sanitize User Input
Input sanitization prevents XSS attacks and injection vulnerabilities. Here's a comprehensive approach:
Input Types and Risks
| Input Type | Risk | Attack Example | Protection |
|---|---|---|---|
| Form Fields | XSS, SQL Injection | <script>alert('XSS')</script> | Sanitize + Validate |
| URL Parameters | Path Traversal, Injection | ../../../etc/passwd | Validate + Escape |
| JSON Body | NoSQL Injection | {"$ne": null} | Schema validation |
| Rich Text | XSS via HTML | <img src=x onerror=alert(1)> | HTML sanitization |
| File Names | Path Traversal | ../../malicious.exe | Sanitize filename |
Sanitization Libraries Comparison
| Library | Use Case | Framework | Example |
|---|---|---|---|
| DOMPurify | HTML sanitization | Client + Server | DOMPurify.sanitize(html) |
| validator.js | String validation | Node.js | validator.isEmail(email) |
| sanitize-html | HTML cleaning | Node.js | sanitizeHtml(dirty) |
| Zod | Schema validation | TypeScript/JS | z.string().email() |
| express-validator | Express validation | Express.js | body('email').isEmail() |
Implementation Examples
Next.js with Zod
// lib/validations.ts
import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
// app/api/login/route.ts
import { loginSchema } from '@/lib/validations';
export async function POST(request: Request) {
const body = await request.json();
// Validate and sanitize
const result = loginSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ errors: result.error.errors },
{ status: 400 }
);
}
// result.data is now sanitized and validated
const { email, password } = result.data;
// Continue with login logic
}
Express.js with express-validator
const { body, validationResult } = require('express-validator');
const DOMPurify = require('isomorphic-dompurify');
// Validation middleware
const validateInput = [
body('email')
.isEmail()
.normalizeEmail()
.trim(),
body('password')
.isLength({ min: 8 })
.matches(/[A-Z]/).withMessage('Password must contain uppercase')
.matches(/[a-z]/).withMessage('Password must contain lowercase')
.matches(/[0-9]/).withMessage('Password must contain number'),
body('bio')
.optional()
.customSanitizer(value => DOMPurify.sanitize(value)), // Sanitize HTML
];
app.post('/api/register', validateInput, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// req.body is now sanitized
// Continue with registration
});
HTML Sanitization Example
const DOMPurify = require('isomorphic-dompurify');
// Rich text editor content
function sanitizeRichText(html) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href'],
ALLOW_DATA_ATTR: false,
});
}
// Usage
const userInput = '<p>Hello <script>alert("XSS")</script> world</p>';
const safe = sanitizeRichText(userInput);
// Result: '<p>Hello world</p>' (script removed)
5) Keep Dependencies Updated
Outdated dependencies are a leading cause of security breaches. Here's a systematic approach:
Dependency Management Strategy
| Task | Frequency | Command | Action |
|---|---|---|---|
| Check for updates | Weekly | npm outdated | Review and update |
| Security audit | Weekly | npm audit | Fix vulnerabilities |
| Update packages | Monthly | npm update | Update minor/patch versions |
| Major version review | Quarterly | Manual review | Test and upgrade major versions |
| Remove unused | Quarterly | depcheck | Remove unused dependencies |
npm Audit Severity Levels
| Severity | Description | Action Required | Timeline |
|---|---|---|---|
| Critical | Remote code execution | Fix immediately | Within 24 hours |
| High | Significant security risk | Fix soon | Within 1 week |
| Medium | Moderate risk | Plan to fix | Within 1 month |
| Low | Minor risk | Consider fixing | Next update cycle |
Automated Security Checks
// package.json
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"check-updates": "npm-check-updates",
"update-patch": "npm-check-updates -u --target patch",
"security-check": "npm audit && npm outdated"
}
}
# Weekly security check script
#!/bin/bash
echo "Running security audit..."
npm audit
echo "Checking for outdated packages..."
npm outdated
echo "Checking for known vulnerabilities..."
npm audit --audit-level=moderate
Dependency Update Workflow
- Test in development - Update and test thoroughly
- Review changelogs - Check for breaking changes
- Run tests - Ensure all tests pass
- Deploy to staging - Test in production-like environment
- Monitor production - Watch for issues after deployment
6) Run Your Application with Least Privilege
Running applications with minimal permissions limits the damage if an attacker gains access.
User Permission Strategy
| User Type | Permissions | Use Case |
|---|---|---|
| Root | Full system access | ❌ Never run apps as root |
| System User | Limited, app-specific | ✅ Recommended for services |
| Regular User | Home directory access | ✅ Development only |
Implementation Guide
Create Application User (Ubuntu/Debian)
# Create a dedicated user for your application
sudo adduser --system --group --no-create-home appuser
# Set ownership of application directory
sudo chown -R appuser:appuser /var/www/myapp
# Set proper permissions
sudo chmod -R 755 /var/www/myapp
sudo chmod -R 600 /var/www/myapp/.env # Protect sensitive files
PM2 with Non-Root User
# Start PM2 as the application user
sudo -u appuser pm2 start ecosystem.config.js
sudo -u appuser pm2 save
sudo -u appuser pm2 startup systemd
File Permissions Reference
| File Type | Recommended Permissions | Command |
|---|---|---|
| Application files | 755 (rwxr-xr-x) | chmod 755 |
| Configuration files | 644 (rw-r--r--) | chmod 644 |
| Environment files (.env) | 600 (rw-------) | chmod 600 |
| Log files | 640 (rw-r-----) | chmod 640 |
| Upload directories | 750 (rwxr-x---) | chmod 750 |
Docker Least Privilege Example
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Switch to non-root user
USER appuser
# Application runs as appuser, not root
CMD ["node", "server.js"]
7) Monitor Logs & Set Alerts
Effective monitoring helps you detect attacks and issues before they cause damage.
Log Types and What to Monitor
| Log Type | What It Tracks | Key Metrics | Tools |
|---|---|---|---|
| Access Logs | HTTP requests | Failed requests, suspicious IPs | Nginx, Apache |
| Error Logs | Application errors | Error rates, stack traces | PM2, Winston |
| Authentication Logs | Login attempts | Failed logins, brute force | Custom middleware |
| Security Logs | Security events | Rate limit hits, blocked IPs | Fail2ban, custom |
Monitoring Checklist
| Pattern | What It Indicates | Action |
|---|---|---|
| Multiple 401/403 errors | Brute force attack | Block IP, increase rate limits |
| High request rate from single IP | DDoS or bot attack | Rate limit, block IP |
| Unusual file access patterns | Path traversal attempt | Block IP, review logs |
| Repeated failed logins | Account enumeration | Lock account, alert admin |
| SQL error messages | Injection attempt | Block IP, review code |
Implementation Examples
Express.js Logging Middleware
const winston = require('winston');
const expressWinston = require('express-winston');
// Create logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
// Request logging middleware
app.use(expressWinston.logger({
winstonInstance: logger,
meta: true,
msg: "HTTP {{req.method}} {{req.url}}",
expressFormat: true,
colorize: false,
}));
// Security event logging
function logSecurityEvent(type, details) {
logger.warn('Security Event', {
type,
ip: details.ip,
userAgent: details.userAgent,
timestamp: new Date(),
...details,
});
}
// Usage in login endpoint
app.post('/api/login', async (req, res) => {
// ... login logic
if (loginFailed) {
logSecurityEvent('FAILED_LOGIN', {
ip: req.ip,
email: req.body.email,
userAgent: req.headers['user-agent'],
});
}
});
Simple Alert Script (Cron Job)
#!/bin/bash
# check-security.sh - Run via cron every 5 minutes
LOG_FILE="/var/log/app/combined.log"
ALERT_EMAIL="admin@example.com"
# Check for multiple failed logins
FAILED_LOGINS=$(grep "FAILED_LOGIN" $LOG_FILE | tail -20 | wc -l)
if [ $FAILED_LOGINS -gt 10 ]; then
echo "Alert: $FAILED_LOGINS failed login attempts in last 5 minutes" | \
mail -s "Security Alert: Brute Force Detected" $ALERT_EMAIL
fi
# Check for high error rate
ERROR_RATE=$(grep "ERROR" $LOG_FILE | tail -100 | wc -l)
if [ $ERROR_RATE -gt 50 ]; then
echo "Alert: High error rate detected ($ERROR_RATE errors)" | \
mail -s "Security Alert: High Error Rate" $ALERT_EMAIL
fi
PM2 Logging Configuration
// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: './server.js',
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
max_memory_restart: '1G',
}]
};
Quick Reference: Security Checklist
Use this checklist when deploying any new application:
Pre-Deployment Checklist
- Security headers configured (HSTS, CSP, X-Frame-Options, etc.)
- Rate limiting enabled for all endpoints
- Login endpoints protected with CAPTCHA and account lockout
- File uploads validated (extension, magic bytes, size)
- All user input sanitized and validated
- Dependencies updated (
npm auditshows no critical issues) - Application runs as non-root user
- Environment variables secured (
.envnot in git, proper permissions) - HTTPS enabled (no HTTP endpoints)
- Logging and monitoring configured
- Error messages don't reveal sensitive information
- Database credentials secured
- CORS properly configured (not
*) - API keys and secrets in environment variables
Post-Deployment Monitoring
- Set up alerts for failed login attempts
- Monitor error rates
- Review access logs weekly
- Check for dependency updates monthly
- Review and rotate secrets quarterly
Final Thoughts
Security is not a one-time configuration — it's a combination of many small, consistent practices that work together to create a strong defense. By implementing these seven essential practices:
- ✅ Security headers — Browser-level protection
- ✅ Rate limiting — Prevent abuse and attacks
- ✅ Login protection — Secure authentication
- ✅ File upload security — Multi-layered validation
- ✅ Input sanitization — Prevent injection attacks
- ✅ Updated dependencies — Patch known vulnerabilities
- ✅ Least privilege — Limit potential damage
- ✅ Monitoring — Detect and respond to threats
You'll protect your application against the vast majority of common attacks. Start with the highest-impact items (security headers and rate limiting), then gradually implement the rest based on your application's specific needs.
Remember: security is a process, not a destination. Regular reviews and updates are essential to maintaining a secure application.
Post Details
Navigation
Related posts
Deploy Next.js on Ubuntu with Git, PM2, Nginx, and Certbot
Production-ready guide to deploy a Next.js app on Ubuntu using Git for code, PM2 for process management, Nginx as reverse proxy, and Certbot for HTTPS.
Read more →Generate a Self‑Signed SSL Certificate on Ubuntu (Nginx)
Step-by-step guide to creating a self-signed certificate with OpenSSL and configuring Nginx with secure defaults.
Read more →Ubuntu Server Best Practices: Create 'ubuntu' User, SSH Keys, Disable Root Login
Harden an Ubuntu server by creating a non-root user with SSH key-based access, disabling root login, and configuring sudo privileges.
Read more →