database/securing-data/SKILL.md
Implement database security through encryption, Row-Level Security, connection hardening, backups, and audit logging.
npx skillsauth add 7a336e6e/skills Securing DataInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Protect data at rest and in transit by encrypting sensitive columns, isolating tenant data with Row-Level Security, enforcing secure connections, maintaining reliable backups, and logging all data changes for audit.
Encrypt PII at the application level before writing to the database. Use a symmetric encryption library (e.g., cryptography.fernet) with keys managed through environment configuration, never stored in the database or source code.
from cryptography.fernet import Fernet
# Key loaded from environment config
fernet = Fernet(settings.ENCRYPTION_KEY)
def encrypt_value(plaintext: str) -> str:
return fernet.encrypt(plaintext.encode()).decode()
def decrypt_value(ciphertext: str) -> str:
return fernet.decrypt(ciphertext.encode()).decode()
Store encrypted values in TEXT or BYTEA columns. Mark these columns clearly in the schema:
CREATE TABLE user_profiles (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users (id),
ssn_encrypted TEXT NOT NULL, -- AES-encrypted, app-level
phone_encrypted TEXT, -- AES-encrypted, app-level
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Use PostgreSQL RLS to enforce tenant isolation at the database level. This provides a safety net even if application code has bugs.
-- Enable RLS on the table
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Create a policy that restricts access to the current tenant
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant_id')::BIGINT);
-- Force RLS even for table owners (important for testing)
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
Set the tenant context at the beginning of each request in your application:
def set_tenant_context(session: Session, tenant_id: int) -> None:
session.execute(
text("SET LOCAL app.current_tenant_id = :tenant_id"),
{"tenant_id": str(tenant_id)},
)
The SET LOCAL command scopes the setting to the current transaction, ensuring it does not leak across requests.
Require SSL for all connections and use least-privilege database users.
-- Create a limited application user
CREATE ROLE app_service WITH LOGIN PASSWORD 'strong-random-password';
GRANT CONNECT ON DATABASE mydb TO app_service;
GRANT USAGE ON SCHEMA public TO app_service;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_service;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO app_service;
-- Do NOT grant: CREATE, DROP, ALTER, TRUNCATE, SUPERUSER
Enforce SSL in the connection string:
engine = create_engine(
"postgresql://app_service:password@db-host/mydb",
connect_args={"sslmode": "verify-full", "sslrootcert": "/path/to/ca.crt"},
)
Create separate database users for different services and for migrations:
| User | Permissions |
|------------------|------------------------------------------|
| app_service | SELECT, INSERT, UPDATE, DELETE |
| migration_user | ALL on schema (for running migrations) |
| readonly_user | SELECT only (for reporting and analytics)|
Use multiple backup approaches for defense in depth.
# Logical backup with pg_dump (full database)
pg_dump -Fc --no-owner --no-acl mydb > backup_$(date +%Y%m%d_%H%M%S).dump
# Restore from logical backup
pg_restore -d mydb --no-owner --no-acl backup_20250115_143000.dump
# Physical backup with pg_basebackup (for point-in-time recovery)
pg_basebackup -D /backups/base -Ft -z -P
# Enable WAL archiving in postgresql.conf for continuous archiving
# archive_mode = on
# archive_command = 'cp %p /archive/%f'
Test backup restoration on a regular schedule. A backup that has not been tested is not a backup.
Track who changed what and when. Use a dedicated audit table and a trigger function.
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
table_name TEXT NOT NULL,
record_id BIGINT NOT NULL,
action TEXT NOT NULL, -- INSERT, UPDATE, DELETE
old_values JSONB,
new_values JSONB,
changed_by TEXT NOT NULL,
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (table_name, record_id, action, old_values, new_values, changed_by)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) END,
current_setting('app.current_user', true)
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- Attach to any table that needs auditing
CREATE TRIGGER audit_users
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
Produce SQL scripts for RLS policies, role definitions, and audit triggers. Produce Python modules for encryption utilities. Include configuration snippets for connection security. Document the backup schedule and restoration procedure.
development
Implement features using the Red-Green-Refactor cycle to ensure testability and correctness from the start.
data-ai
Manage the `tasks.md` ledger with strict locking and collision avoidance protocols to allow multiple agents to work in parallel safely.
development
The git-workflow skill defines branching conventions, commit message formats, and pull request standards that all agents must follow for consistent version control.
development
The environment-config skill standardizes how agents manage environment variables, secrets, and application configuration across local development and deployed environments.