- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Hardening a Linux Server for a Laravel App: A Practical Checklist
About Post
Spin up a fresh server with a public IP address, then look at its SSH log an hour later. You'll find login attempts for root, admin, test and ubuntu from addresses all over the world. Nobody is targeting you personally. Bots simply scan the whole internet, all the time.
That's why hardening isn't something you do "later, once the app is live". It's the first hour of a server's life. The good news is that the basics are well understood, mostly boring, and take less time than setting up your deploy script.
Here's the checklist I follow for a Laravel app on Ubuntu 24.04 LTS with Nginx and PHP-FPM. Treat the commands as a simplified starting point and adapt them to your setup.
Step 1: patch everything, then keep patching
Most real-world compromises use known vulnerabilities that already have a fix. So update first, and make security updates automatic:
sudo apt update && sudo apt upgrade -y
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Unattended upgrades install security updates for you. Some updates (like kernel patches) need a reboot to take effect, so check for /var/run/reboot-required now and then, or schedule a maintenance window.
Step 2: a normal user and SSH keys only
Don't work as root. Create a user with sudo rights and copy your public key to it:
sudo adduser deploy
sudo usermod -aG sudo deploy
# from your own machine:
ssh-copy-id deploy@your-server-ip
Then turn off password logins and direct root logins. On Ubuntu 24.04, put your settings in a drop-in file rather than editing the main config:
# /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
Here's the gotcha that catches people: for most SSH settings, the first value found wins, and drop-in files are read in alphabetical order. Some cloud images ship a file like 50-cloud-init.conf that sets PasswordAuthentication yes. Name your file with a 00- prefix so it's read first, then confirm the result instead of assuming:
sudo sshd -t # check the config for errors
sudo systemctl restart ssh
sudo sshd -T | grep -Ei 'passwordauth|permitroot'
Don't lock yourself out: keep your current SSH session open, and test the new settings by logging in from a second terminal. Only close the first one when the second works.
Step 3: a firewall that says no by default
A web server needs exactly three ports open to the world: SSH, HTTP and HTTPS. Everything else (MySQL, Redis, your queue dashboard) should be unreachable from outside.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose
Two things worth knowing:
- Bind services to localhost too. MySQL and Redis should listen on
127.0.0.1, not0.0.0.0. Then even a firewall mistake doesn't expose them. Check what's listening withsudo ss -tulpn. - Docker can bypass ufw. Ports published with
-p 3306:3306are added to iptables by Docker directly, so they can be reachable even when ufw says otherwise. Publish to127.0.0.1:3306:3306if a container only needs local access.
On AWS, security groups give you a second firewall in front of the server. Use both: security groups to allow only what's needed, ufw as the safety net, and keep databases like RDS in private subnets.
Step 4: fail2ban for the noisy neighbours
With password logins off, brute-force attempts can't succeed, but they still fill your logs and waste resources. fail2ban watches the logs and temporarily bans IPs that keep failing:
sudo apt install fail2ban
sudo fail2ban-client status sshd
On Ubuntu the SSH jail is enabled out of the box. To tune ban times or add jails (for example, for repeated failed logins on your app's login page), create /etc/fail2ban/jail.local rather than editing the default files, which get overwritten on updates.
Step 5: least privilege for the app itself
This is the step most guides skip, and it matters a lot for PHP apps. If an attacker finds a hole in your application, the damage they can do depends on what the PHP process is allowed to touch.
- Code owned by the deploy user, not the web server. PHP-FPM runs as
www-dataand should be able to read the code, but only write tostorageandbootstrap/cache. - Only
public/is the web root. Your Nginxrootpoints to/var/www/app/public, never the project folder. Otherwise.envmay be one URL away. - Protect
.env. Readable by the app, not by every user on the box.
cd /var/www/app
sudo chown -R deploy:www-data .
sudo find . -type f -exec chmod 644 {} \;
sudo find . -type d -exec chmod 755 {} \;
sudo chmod -R g+w storage bootstrap/cache
sudo chmod 640 .env
And in .env, the two lines that should never be wrong in production: APP_ENV=production and APP_DEBUG=false. A debug error page in production can reveal file paths, queries and configuration to anyone who triggers an exception.
While you're there, hide version banners: server_tokens off; in Nginx and expose_php = Off in php.ini. It won't stop a determined attacker, but there's no reason to advertise.
Step 6: HTTPS everywhere
Let's Encrypt certificates are free, and Certbot sets them up and renews them automatically:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
Redirect all HTTP traffic to HTTPS, and once you're sure everything works over HTTPS, add a Strict-Transport-Security header so browsers stop trying plain HTTP at all. If the app is behind a load balancer, configure Laravel's trusted proxies so it knows the original request was HTTPS.
Step 7: backups you have actually restored
Hardening reduces the chance of a bad day. Backups decide how bad it gets. A backup plan for a typical Laravel app:
- Database dumps on a schedule (
mysqldump --single-transactionfor InnoDB, or managed snapshots if you use RDS). - Uploaded files, unless they already live in S3 with versioning on.
- Stored off the server, with credentials that can write new backups but can't delete old ones. If an attacker gets the server, they shouldn't get your backups too.
- Restored for real, regularly, into a test environment. A backup you've never restored is a hope, not a backup.
The checklist
- System updated, unattended security upgrades on.
- Non-root sudo user; SSH keys only; root login off; verified with
sshd -T. - Firewall denies by default; only SSH, HTTP and HTTPS open; databases on localhost or private subnets.
- fail2ban running.
- App code read-only to PHP except
storageandbootstrap/cache; web root ispublic/;.envlocked down; debug off. - HTTPS with automatic renewal.
- Off-server backups, tested by restoring.
For deeper reading, Laravel's deployment documentation covers the Nginx config and optimisation steps.
Which item on this list do you see skipped most often? My guess is the restore test, because it's the only one that feels like extra work until the day it isn't.

Be first to comment it...