Database security is a critical component of any organization's security posture. Databases store the most valuable data: customer records, financial data, intellectual property, and credentials. This guide covers the key security practices including encryption, access control, network isolation, and secret management.
Encryption at Rest
Encryption at rest protects data stored on disk. If an attacker gains access to the underlying storage, encrypted data remains unreadable without the encryption key.
Transparent Data Encryption (TDE)
TDE encrypts database files automatically. The database engine handles encryption and decryption transparently.
-- PostgreSQL: Enable TDE with pg_tde extension
CREATE EXTENSION pg_tde;
-- Create an encrypted table
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT,
email TEXT,
ssn TEXT
) USING tde;
-- MySQL: Enable InnoDB tablespace encryption
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
amount DECIMAL(10,2)
) ENCRYPTION='Y';
Application-Level Encryption
For maximum protection, encrypt sensitive columns at the application level. The database never sees the plaintext.
# Application-level encryption with AWS KMS
import boto3
from cryptography.fernet import Fernet
def encrypt_column(plaintext, kms_key_id):
# Generate a data key from KMS
kms = boto3.client('kms')
response = kms.generate_data_key(
KeyId=kms_key_id,
KeySpec='AES_256'
)
data_key = response['Plaintext']
encrypted_key = response['CiphertextBlob']
# Encrypt the data with the data key
f = Fernet(base64.urlsafe_b64encode(data_key))
ciphertext = f.encrypt(plaintext.encode())
return ciphertext, encrypted_key
def decrypt_column(ciphertext, encrypted_key):
kms = boto3.client('kms')
response = kms.decrypt(CiphertextBlob=encrypted_key)
data_key = response['Plaintext']
f = Fernet(base64.urlsafe_b64encode(data_key))
return f.decrypt(ciphertext).decode()
Key Management for Encryption at Rest
Encryption in Transit
Encryption in transit protects data as it travels between the database and clients.
TLS Configuration
-- PostgreSQL: Require TLS for all connections
-- In postgresql.conf
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'
ssl_ca_file = '/etc/ssl/certs/ca.crt'
-- In pg_hba.conf
# Require TLS for all connections
hostssl all all 0.0.0.0/0 md5
# MySQL: Require TLS
[mysqld]
require_secure_transport = ON
ssl_ca = /etc/ssl/certs/ca.pem
ssl_cert = /etc/ssl/certs/server-cert.pem
ssl_key = /etc/ssl/private/server-key.pem
**Best practices**:
Row-Level Security (RLS)
RLS restricts which rows a user can access based on a policy. It implements multi-tenancy and data isolation at the database level.
PostgreSQL Row-Level Security
-- Create a table with RLS
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
customer_name TEXT,
amount DECIMAL(10,2)
);
-- Enable RLS on the table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Create a policy: users can only see their tenant's orders
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::INT);
-- Grant access to the table
GRANT SELECT, INSERT ON orders TO app_user;
Row-Level Security in Other Databases
-- SQL Server: Row-level security with security predicates
CREATE FUNCTION dbo.tenantAccessPredicate(@tenant_id INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS access_result
WHERE @tenant_id = CAST(SESSION_CONTEXT(N'tenant_id') AS INT);
CREATE SECURITY POLICY dbo.tenantSecurityPolicy
ADD FILTER PREDICATE dbo.tenantAccessPredicate(tenant_id)
ON dbo.orders;
RLS ensures that even if a query accesses rows it should not, the database engine filters them out automatically.
Audit Logging
Audit logging records database activity for security analysis and compliance.
What to Log
PostgreSQL Audit with pgaudit
-- Install pgaudit extension
CREATE EXTENSION pgaudit;
-- Configure audit logging in postgresql.conf
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'write,ddl,role,function'
pgaudit.log_level = 'notice'
pgaudit.log_relation = on
pgaudit.log_statement_once = off
MySQL Audit Log
-- MySQL Enterprise Audit
INSTALL PLUGIN audit_log SONAME 'audit_log.so';
SET GLOBAL audit_log_policy = 'ALL';
-- Query audit logs
SELECT * FROM mysql.audit_log_table
WHERE timestamp >= NOW() - INTERVAL 1 HOUR
AND status = 0 -- Failed operations
ORDER BY timestamp DESC;
Centralized Audit Log Collection
Forward database audit logs to a SIEM for analysis and alerting.
# Filebeat configuration for shipping database audit logs
filebeat.inputs:
- type: log
paths:
- /var/log/postgresql/pgaudit.log
fields:
service: postgresql
environment: production
output.elasticsearch:
hosts: ["https://elasticsearch.example.com:9200"]
Network Isolation
Network isolation limits which systems can reach the database.
VPC and Subnet Design
Place databases in private subnets with no direct internet access. Only application servers in the same VPC should connect.
# Terraform: Database in private subnet
resource "aws_subnet" "db_private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.3.0/24"
tags = {
Name = "db-private"
Tier = "database"
}
}
resource "aws_security_group" "db" {
name_prefix = "db-sg-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app.id]
description = "PostgreSQL from app servers"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
Database Firewall Rules
-- PostgreSQL: Restrict listen address
-- In postgresql.conf
listen_addresses = '10.0.1.0,10.0.2.0'
Least Privilege Access
Apply the principle of least privilege to database access.
Separate Roles by Function
-- Create separate roles for different access patterns
CREATE ROLE read_only;
CREATE ROLE read_write;
CREATE ROLE admin;
-- Grant minimal permissions
GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO read_write;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin;
-- Grant roles to users
GRANT read_only TO reporting_user;
GRANT read_write TO app_user;
GRANT admin TO dba_team;
Application Users vs Admin Users
Secret Rotation
Database credentials need regular rotation. Compromised credentials that are never rotated become permanent backdoors.
Automated Rotation with HashiCorp Vault
# Vault database secrets engine configuration
path "database/creds/my-app" {
capabilities = ["read"]
}
# Dynamic database credentials with Vault
import hvac
client = hvac.Client(url='https://vault.example.com')
client.auth.approle('my-role-id', 'my-secret-id')
# Get short-lived database credentials
creds = client.secrets.database.generate_credentials(
name='my-app'
)
db_user = creds['data']['username'] # v-token-myapp-a1b2c3...
db_pass = creds['data']['password'] # random password
# These credentials expire in 1 hour
**Rotation frequency**:
Security Hardening Checklist
Conclusion
Database security requires defense in depth. Encrypt data at rest and in transit. Isolate databases on private networks. Implement row-level security and audit logging. Follow least privilege for database roles. Automate secret rotation. A well-secured database makes it significantly harder for attackers to exfiltrate sensitive data, even if they breach other parts of the infrastructure.