Here's something that keeps me up at night: I've investigated breaches where the attacker never touched the OS. They didn't crack SSH. They didn't exploit the firewall. Instead, they walked straight through the application layer like it had a welcome mat.
The database contained 50,000 customer records. The credentials were stored in a plaintext config file inside the web root. The SQL injection vulnerability had been public for six months.
It took the attacker approximately four minutes to get everything.
Here's the uncomfortable truth:
Securing your VPS infrastructure means nothing if your database and applications are wide open.
The Data Layer is the Real Target
When I do penetration testing, I almost never attack the server directly anymore. Why? Because applications are easier targets.
Think about it from an attacker's perspective:
- Exploiting a web application gets you direct access to data without needing root privileges
- SQL injection returns database records immediately
- Authentication bypass lets you impersonate users
- Directory traversal reveals sensitive files
- Insecure deserialization can execute arbitrary code
None of these require kernel exploits or SSH brute force.
The OWASP Top 10 exists for a reason. These vulnerabilities are everywhere because they're trivial to exploit and effective.
From my incident response work, here's what I've actually found in compromised systems:
Exposed API keys and database credentials in source code repositories, environment files, and logs. Attackers search GitHub for these constantly using tools like TruffleHog automatically.
SQL injection vulnerabilities in login forms, search features, and filter parameters. Even frameworks with built-in protection fail when developers write raw SQL queries.
Default credentials on databases and admin panels. I can't tell you how many times I've logged into a system using admin/admin
or the database's default password that was never changed.
Missing authentication on critical endpoints. I've accessed entire admin dashboards by simply knowing the URL path.
Unencrypted sensitive data in databases. Passwords stored as plaintext. API keys visible in logs. Customer data readable by anyone with database access.
Step 1: Secure Your Database — The Crown Jewels Inside the Crown Jewels
Your database is the most valuable target on your server. Everything else is infrastructure — but the database is where your actual business data lives.
Change Default Credentials Immediately
This isn't optional. The moment your database is installed, change the root/admin password.
# PostgreSQL
sudo -u postgres psql
ALTER USER postgres WITH PASSWORD 'generate_a_truly_random_password_here';
# MySQL/MariaDB
sudo mysql -u root
ALTER USER 'root'@'localhost' IDENTIFIED BY 'generate_a_truly_random_password_here';
FLUSH PRIVILEGES;
Store these passwords in a secrets manager — I'll cover that in a second. Never, ever put database passwords in your application code or
.env
files that get committed to Git.
Create Limited-Privilege Database Users
This is the principle of least privilege. Your web application should never connect as the database root user. Create specific users with minimal permissions.
# PostgreSQL example
sudo -u postgres psql
CREATE USER app_user WITH PASSWORD 'app_password_here';
CREATE DATABASE app_database;
GRANT CONNECT ON DATABASE app_database TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
That user can only:
- Connect to their specific database
- Read and modify their own data
- Never drop tables, create users, or access system tables
If your application is compromised, the attacker is limited to that user's permissions. This is your safety net.

Bind Database to Localhost Only
Your database should never listen on your public IP address. Ever.
# PostgreSQL
sudo nano /etc/postgresql/*/main/postgresql.conf
# Find this line and change it to:
listen_addresses = 'localhost'
# Restart
sudo systemctl restart postgresql
# MySQL/MariaDB
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
# Change:
bind-address = 127.0.0.1
sudo systemctl restart mysql
If your application and database run on the same server (which they should for a VPS), there's zero reason for your database to accept remote connections.
If you need remote connections for administration or replication, use SSH tunneling:
ssh -L 5432:localhost:5432 sysop@your_server_ip -p 7822
This securely forwards the database connection through your SSH tunnel. No database port exposed to the internet.
Enable Database-Level Encryption
Store sensitive data encrypted inside the database, not just in transit.
For PostgreSQL with pgcrypto:
CREATE EXTENSION pgcrypto;
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
ssn TEXT NOT NULL,
ssn_encrypted bytea NOT NULL
);
-- Insert encrypted data
INSERT INTO users (email, ssn_encrypted) VALUES
('user@example.com', pgp_sym_encrypt('123-45-6789', 'encryption_key_here'));
-- Query decrypted data
SELECT pgp_sym_decrypt(ssn_encrypted, 'encryption_key_here') FROM users;
Even if an attacker gets database access, encrypted columns are unreadable without the encryption key.
Enable Query Logging for Forensics
This is crucial for investigating breaches.
# PostgreSQL
sudo nano /etc/postgresql/*/main/postgresql.conf
log_statement = 'all'
log_duration = on
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
sudo systemctl restart postgresql
Query logs go to /var/log/postgresql/
and you can analyze them during an incident.

Step 2: Implement Secrets Management — Keep Your Keys Safe
This is where I see organizations completely mess up. Database passwords, API keys, encryption keys — all scattered across environment files, config files, and developers' laptops.
I responded to a breach where the attacker found production credentials in an old developer's GitHub fork. A fork that had been deleted from the main account but still existed on GitHub because the developer had forked it to their personal account years ago.
Never put secrets in code. Never.
Use a Proper Secrets Manager
For a small VPS setup, here's what I recommend:
Option 1: HashiCorp Vault (Production Standard)
If you're handling sensitive data, Vault is the gold standard. It's what major companies use.
# Install Vault
wget https://releases.hashicorp.com/vault/1.15.0/vault_1.15.0_linux_amd64.zip
unzip vault_1.15.0_linux_amd64.zip
sudo mv vault /usr/local/bin/
vault --version
Initialize and unseal Vault:
vault server -dev
# In another terminal:
vault status
Store secrets:
vault kv put secret/myapp/database \
username="app_user" \
password="secure_password_here"
Retrieve secrets in your application:
vault kv get secret/myapp/database
Option 2: Simple Environment Approach (Small Projects)
For simpler deployments, use environment variables stored securely:
# Create a secrets file outside your web root
sudo nano /var/secrets/app.env
DATABASE_URL="postgresql://app_user:password@localhost/app_database"
API_KEY="your_api_key_here"
ENCRYPTION_KEY="your_encryption_key_here"
# Restrict permissions
sudo chmod 600 /var/secrets/app.env
sudo chown app_user:app_user /var/secrets/app.env
Rotate Secrets Regularly
This is non-negotiable. Quarterly minimum, monthly if possible.
- Generate new API key
- Rotate database user password
- Update encryption keys
- Revoke old credentials
Every time you rotate a secret, document it. You'll need those records during an audit or incident investigation.

Step 3: Prevent SQL Injection — The #1 Application Vulnerability
SQL injection is so common it shouldn't still be happening. Yet here we are.
I found a SQL injection vulnerability in a system handling credit card data.
Always Use Parameterized Queries
This is the rule. No exceptions. No "I'm just concatenating this one string."
❌ NEVER do this:
query = f"SELECT * FROM users WHERE email = '{email}'"
cursor.execute(query)
An attacker submits: ' OR '1'='1
The query becomes: SELECT * FROM users WHERE email = '' OR '1'='1'
They get all user records.
✅ DO THIS:
query = "SELECT * FROM users WHERE email = %s"
cursor.execute(query, (email,))
The database driver handles escaping. The attacker's input is treated as data, not code.
Language-specific examples:
# Python with psycopg2
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# Python with SQLAlchemy
user = session.query(User).filter(User.email == email).first()
# JavaScript with node-postgres
const result = await client.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
# PHP with prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
Validate and Sanitize Input
Parameterized queries are your first defense. Input validation is your second.
import re
from email_validator import validate_email
# Validate email format
try:
valid_email = validate_email(user_email)
except:
return "Invalid email format"
# Validate input length
if len(user_comment) > 500:
return "Comment too long"
# Validate expected format
if not re.match(r'^\d{3}-\d{2}-\d{4}$', ssn):
return "Invalid SSN format"
This catches bad data before it even reaches the database.
Use an ORM
Object-Relational Mapping frameworks like SQLAlchemy, Sequelize, or Django ORM build parameterized queries for you automatically.
# Django ORM - automatically parameterized
user = User.objects.filter(email=user_email).first()
# SQLAlchemy - automatically parameterized
user = session.query(User).filter(User.email == user_email).first()
If you must write raw SQL, parameterize it. If you can't parameterize it, you're doing something wrong.
Step 4: Implement Proper Authentication & Authorization
I've accessed entire admin dashboards by simply knowing the URL path. No credentials required.
Hash Passwords Correctly
Never store passwords in plaintext. Never use weak hashing like MD5 or SHA1.
Use bcrypt, scrypt, or Argon2:
import bcrypt
# Hashing during registration
password = "user_password_here"
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
# Verifying during login
if bcrypt.checkpw(password.encode(), stored_hash):
login_successful()
The rounds=12
parameter makes password cracking exponentially harder. It takes about 100ms to hash each password, which is fast enough for login but makes brute force attacks impractical.
Implement Session Management Correctly
Sessions are where authentication state lives.
# Set secure session cookie
session_cookie_settings = {
'httponly': True, # JavaScript can't access it
'secure': True, # HTTPS only
'samesite': 'Strict', # CSRF protection
'max_age': 3600 # 1 hour expiration
}
Use industry-standard session libraries. Don't roll your own authentication. Trust me — I've reviewed plenty of custom auth systems and they're almost always broken.
Enforce Least Privilege Authorization
Different users need different permissions.
# Define roles
ROLES = {
'admin': ['read', 'write', 'delete', 'manage_users'],
'editor': ['read', 'write'],
'viewer': ['read']
}
# Check permissions before any action
def require_permission(permission):
def decorator(f):
def wrapper(*args, **kwargs):
if permission not in user_roles:
return "Access denied", 403
return f(*args, **kwargs)
return wrapper
return decorator
@require_permission('delete')
def delete_resource(resource_id):
# Only editors with 'delete' permission can reach here
pass

Step 5: Secure Your API Endpoints
APIs are frequently the weakest link. I've exploited APIs to access data that should have been restricted.
Implement Rate Limiting
Prevent brute force and DoS attacks at the application level.
# Using Flask-Limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route("/api/login", methods=['POST'])
@limiter.limit("5 per minute")
def login():
# Max 5 login attempts per minute per IP
pass
Validate All Input
Even if your database queries are parameterized, validate everything:
from marshmallow import Schema, fields, ValidationError
class UserSchema(Schema):
email = fields.Email(required=True)
age = fields.Int(validate=lambda x: 18 <= x <= 120)
username = fields.Str(required=True, validate=lambda x: 3 <= len(x) <= 20)
schema = UserSchema()
try:
valid_data = schema.load(request.json)
except ValidationError as err:
return err.messages, 400
Use API Keys & OAuth Properly
Never authenticate APIs with basic auth over HTTP. Use API keys or OAuth tokens.
# API key authentication
@app.route('/api/data')
def get_data():
api_key = request.headers.get('X-API-Key')
if not api_key or not validate_api_key(api_key):
return "Unauthorized", 401
# Return data
Store API keys hashed in your database, just like passwords. Rotate them quarterly.
Step 6: Monitor Database Activity
Visibility into what's happening in your database is critical.
Enable Query Audit Logging
# PostgreSQL
sudo nano /etc/postgresql/*/main/postgresql.conf
log_connections = on
log_disconnections = on
log_statement = 'all'
log_duration = on
sudo systemctl restart postgresql
Review these logs regularly. Look for:
- Unusual query patterns that don't match normal application behavior
- Queries from unexpected users accessing data they shouldn't
- Exceptionally slow queries that might indicate attacks
- Administrative commands from unexpected sources
Set Up Database Alerts
# Create a monitoring script
#!/bin/bash
# /usr/local/bin/monitor_database.sh
# Check for failed authentication attempts
grep "authentication failed" /var/log/postgresql/postgresql.log | tail -5
# Check for SQL errors
grep "ERROR" /var/log/postgresql/postgresql.log | tail -5
# Check connection count
psql -U postgres -c "SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;"
Run this via cron and alert on anomalies.
Ready to Secure Your Data Layer?
Building a secure VPS requires more than just server hardening. You need reliable infrastructure that lets you implement application-level security without hitting artificial limitations.
If you need a VPS provider that gives you complete control over your application stack and database configurations, Hostinger VPS at $5.99/month provides genuine KVM virtualization with root access. You can implement every security measure in this guide without restrictions.
Let me know your questions in the comments. What's keeping you up at night about your database security?
What's Your Biggest Security Gap?
I want to hear from you. Of these six areas — database security, secrets management, SQL injection prevention, authentication, API security, and monitoring — which one do you struggle with the most?
Drop a comment below. I'm genuinely curious what developers are wrestling with on their own systems.
Disclaimer: I'm a cybersecurity engineer sharing professional best practices. Every system is unique. For regulated data or compliance requirements (HIPAA, PCI-DSS, GDPR), consult a qualified security professional for your specific situation.