---
title: "Hardening bases de données — MySQL & PostgreSQL"
domain: security
subdomain: hardening
type: snippet
tags: [hardening, mysql, postgresql, database, security, encryption, audit, sql]
difficulty: intermediate
status: stable
updated: "2025-05-14"
---
## MySQL / MariaDB

### Installation initiale

```bash
# Premier lancement obligatoire — supprime les défauts dangereux
mysql_secure_installation
# Répond OUI à tout :
# - Set root password
# - Remove anonymous users
# - Disallow root login remotely
# - Remove test database
# - Reload privilege tables
```

```sql
-- Nettoyage manuel si mysql_secure_installation n'a pas tout couvert
-- Supprimer les comptes anonymes
DELETE FROM mysql.user WHERE User='';

-- Supprimer la base de test
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';

-- Restreindre root à localhost uniquement
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');

-- Vérifier les comptes existants
SELECT User, Host, plugin, authentication_string FROM mysql.user;

-- Appliquer les changements
FLUSH PRIVILEGES;
```

### Comptes applicatifs dédiés

```sql
-- Principe : un compte par application, privilèges minimaux
-- Ne jamais utiliser root pour les connexions applicatives

-- Créer un compte applicatif
CREATE USER 'app'@'localhost' IDENTIFIED BY '{{STRONG_PASSWORD}}';

-- Accorder uniquement les privilèges nécessaires (pas GRANT OPTION, pas SUPER)
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'localhost';

-- Pour une app read-only (reporting)
CREATE USER 'app_ro'@'{{APP_SERVER_IP}}' IDENTIFIED BY '{{STRONG_PASSWORD}}';
GRANT SELECT ON appdb.* TO 'app_ro'@'{{APP_SERVER_IP}}';

-- Vérifier les privilèges accordés
SHOW GRANTS FOR 'app'@'localhost';

FLUSH PRIVILEGES;
```

### Configuration /etc/mysql/mysql.conf.d/mysqld.cnf

```bash
[mysqld]

# Réseau — restreindre l'écoute
bind-address            = 127.0.0.1   # ou IP interne uniquement
# bind-address          = {{INTERNAL_IP}}

# Désactiver la résolution DNS (performance + évite rebinding attacks)
skip-name-resolve       = 1

# Désactiver LOCAL INFILE (lecture de fichiers locaux via SQL)
local_infile            = 0
secure_file_priv        = /var/lib/mysql-files   # restreindre le répertoire d'import/export

# Logs
log_error               = /var/log/mysql/error.log
general_log             = 0                       # désactivé en prod (verbeux)
slow_query_log          = 1
slow_query_log_file     = /var/log/mysql/slow.log
long_query_time         = 2

# TLS — forcer les connexions chiffrées
require_secure_transport = ON
ssl-ca                  = /etc/mysql/ssl/ca.pem
ssl-cert                = /etc/mysql/ssl/server-cert.pem
ssl-key                 = /etc/mysql/ssl/server-key.pem

# Chiffrement at-rest (InnoDB)
innodb_encrypt_tables   = ON
innodb_encrypt_log      = ON
early-plugin-load       = keyring_file.so
keyring_file_data       = /var/lib/mysql-keyring/keyring
```

```sql
-- Vérifier que TLS est actif
SHOW VARIABLES LIKE '%ssl%';
SHOW STATUS LIKE 'Ssl_cipher';

-- Forcer TLS pour un compte spécifique
ALTER USER 'app'@'{{APP_SERVER_IP}}' REQUIRE SSL;

-- Forcer TLS + certificat client
ALTER USER 'admin'@'{{MGMT_IP}}' REQUIRE X509;
```

### Audit Plugin (MySQL Enterprise / MariaDB)

```sql
-- MariaDB Audit Plugin
INSTALL PLUGIN server_audit SONAME 'server_audit.so';

-- Configuration (my.cnf)
-- server_audit_logging     = ON
-- server_audit_events      = CONNECT,QUERY,TABLE
-- server_audit_file_path   = /var/log/mysql/audit.log
-- server_audit_file_rotate_size = 1073741824

-- MySQL Enterprise Audit
INSTALL PLUGIN audit_log SONAME 'audit_log.so';
SET GLOBAL audit_log_policy = 'ALL';
```

<Warning>
`general_log = 1` logue absolument toutes les requêtes SQL, y compris les mots de passe en clair si passés dans des requêtes. Ne l'activer qu'en environnement de debug isolé, jamais en production. Utiliser le Audit Plugin pour un audit de production.
</Warning>

---

## PostgreSQL

### pg_hba.conf — Authentification

```bash
# /etc/postgresql/{{VERSION}}/main/pg_hba.conf
# TYPE  DATABASE  USER       ADDRESS         METHOD

# Connexions locales via socket
local   all       postgres                   peer          # OS user postgres uniquement
local   all       all                        scram-sha-256 # comptes applicatifs

# Connexions réseau
host    appdb     app_user   {{APP_IP}}/32   scram-sha-256
host    appdb     app_ro     {{REPORT_IP}}/32 scram-sha-256

# Refuser tout le reste
host    all       all        0.0.0.0/0       reject

# SUPPRIMER ces lignes dangereuses si présentes :
# host  all  all  0.0.0.0/0  trust    ← pas d'authentification
# host  all  all  0.0.0.0/0  md5      ← MD5 obsolète, vulnérable
```

### postgresql.conf — Configuration sécurisée

```bash
# /etc/postgresql/{{VERSION}}/main/postgresql.conf

# Réseau
listen_addresses        = 'localhost'        # ou '{{INTERNAL_IP}}'
port                    = 5432               # changer si possible

# SSL
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'
ssl_min_protocol_version = 'TLSv1.2'

# Logs d'audit
log_connections         = on
log_disconnections      = on
log_failed_auth         = on
log_statement           = 'ddl'              # DDL en prod (ALTER, DROP, CREATE)
# log_statement         = 'all'             # tout (debug uniquement)
log_duration            = on
log_min_duration_statement = 1000           # requêtes > 1 seconde

# Sécurité
password_encryption     = scram-sha-256
```

### Rôles et permissions PostgreSQL

```sql
-- Créer un rôle applicatif avec privilèges minimaux
CREATE ROLE app_user LOGIN PASSWORD '{{STRONG_PASSWORD}}';

-- Connexion à la base de données
GRANT CONNECT ON DATABASE appdb TO app_user;

-- Accès au schéma
GRANT USAGE ON SCHEMA public TO app_user;

-- Privilèges sur les tables
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;

-- Appliquer aux futures tables (DEFAULT PRIVILEGES)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO app_user;

-- Retirer le privilège CREATE du schéma public pour PUBLIC
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

-- Désactiver la connexion distante pour le superuser postgres
ALTER ROLE postgres NOLOGIN;
-- Pour se connecter : sudo -u postgres psql (via peer auth locale)
```

```sql
-- Row Level Security (RLS) — isolation des données par locataire
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY orders_tenant_isolation ON orders
  FOR ALL
  TO app_user
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- L'application doit positionner le contexte avant chaque requête :
-- SET app.tenant_id = '{{TENANT_UUID}}';
```

### pgaudit — Audit complet

```sql
-- Installation de pgaudit
-- Dans postgresql.conf :
-- shared_preload_libraries = 'pgaudit'

-- Configuration (postgresql.conf)
-- pgaudit.log = 'write, ddl, role, connection'
-- pgaudit.log_catalog = off
-- pgaudit.log_parameter = on
-- pgaudit.log_statement_once = on

-- Vérifier que pgaudit est chargé
SELECT * FROM pg_extension WHERE extname = 'pgaudit';

-- Activer l'audit sur une base spécifique
ALTER DATABASE appdb SET pgaudit.log = 'write, ddl, role';
```

---

## Principes communs MySQL & PostgreSQL

```bash
# Ne jamais exposer les ports DB sur Internet
# Port 3306 (MySQL) et 5432 (PostgreSQL) → firewall

# iptables — bloquer l'accès externe
iptables -A INPUT -p tcp --dport 3306 -s {{APP_SERVER_IP}} -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP

iptables -A INPUT -p tcp --dport 5432 -s {{APP_SERVER_IP}} -j ACCEPT
iptables -A INPUT -p tcp --dport 5432 -j DROP

# Vérifier les ports en écoute
ss -tulnp | grep -E "3306|5432"
# Doit afficher 127.0.0.1:PORT ou IP_INTERNE:PORT, jamais 0.0.0.0:PORT
```

```bash
# Backups chiffrés avec vérification d'intégrité

# MySQL — backup chiffré via GPG
mysqldump --single-transaction --routines --triggers appdb | \
  gzip | \
  gpg --batch --yes --passphrase-file /root/.backup-passphrase \
      --symmetric --cipher-algo AES256 \
  > /backup/appdb_$(date +%Y%m%d).sql.gz.gpg

# PostgreSQL — backup chiffré
pg_dump appdb | \
  gzip | \
  gpg --batch --yes --passphrase-file /root/.backup-passphrase \
      --symmetric --cipher-algo AES256 \
  > /backup/appdb_$(date +%Y%m%d).dump.gz.gpg

# Vérifier l'intégrité
sha256sum /backup/appdb_$(date +%Y%m%d).*.gpg > /backup/checksums.sha256
# Stocker les checksums hors du serveur de backup

# Test de restauration mensuel obligatoire
gpg --decrypt /backup/appdb_latest.dump.gz.gpg | gunzip | psql -U postgres test_restore_db
```

```bash
# Accès administrateur via bastion host uniquement
# Depuis le bastion :
ssh -L 3306:{{DB_SERVER}}:3306 {{BASTION_USER}}@{{BASTION_IP}}
mysql -h 127.0.0.1 -P 3306 -u admin -p

# Ou SSH ProxyJump
ssh -J {{BASTION_USER}}@{{BASTION_IP}} -L 5432:{{DB_SERVER}}:5432 {{DB_USER}}@{{DB_SERVER}}
psql -h 127.0.0.1 -p 5432 -U admin appdb
```

## Checklist hardening base de données

<Checklist
  storageKey="hardening-database"
  items={[
    { id: "secure-install", label: "mysql_secure_installation exécuté (MySQL) / pg_hba.conf revu (PostgreSQL)", critical: true },
    { id: "anonymous-removed", label: "Comptes anonymes supprimés, base 'test' supprimée (MySQL)" },
    { id: "root-local", label: "root/postgres : connexion locale uniquement, pas de connexion distante", critical: true },
    { id: "app-accounts", label: "Comptes applicatifs dédiés avec privilèges minimaux (pas de root/superuser)", critical: true },
    { id: "port-firewalled", label: "Port DB non exposé sur Internet (3306/5432 → firewall strict)" , critical: true },
    { id: "tls-enabled", label: "TLS activé pour toutes les connexions (require_secure_transport / ssl=on)" },
    { id: "audit-logs", label: "Logs d'audit activés : connexions, déconnexions, échecs, DDL" },
    { id: "encrypted-backups", label: "Backups chiffrés (GPG AES256), checksums vérifiés, restauration testée" },
    { id: "bastion-access", label: "Accès administrateur uniquement via bastion host (pas d'accès direct)" },
    { id: "schema-locked", label: "REVOKE CREATE ON SCHEMA public FROM PUBLIC (PostgreSQL)" },
    { id: "local-infile", label: "LOCAL INFILE désactivé (MySQL : local_infile=0)" },
    { id: "password-rotation", label: "Rotation régulière des mots de passe des comptes DB (tous les 90 jours)" }
  ]}
/>

<Tip>
Ne jamais utiliser `root` (MySQL) ou `postgres` (PostgreSQL) comme compte applicatif — si l'application est compromise via injection SQL ou RCE, l'attaquant obtient un accès complet à toutes les bases de données du serveur, peut lire les fichiers système (`LOAD DATA INFILE`), et créer des backdoors. Un compte applicatif limité à `SELECT, INSERT, UPDATE, DELETE` sur une seule base contient la compromission.
</Tip>
