Il y a quelques temps j'ai décrit comment « pirater » un système en utilisant PostgreSQL. Aujourd'hui, je vais décrire comment sécuriser au maximum une installation PostgreSQL1.

Je passerai par différentes étapes, en décrivant ce qui est possible, ce qui est simple et ce qui n'est pas si simple :)

Commençons donc le tutoriel...

Tout d'abord, je suppose que vous voulez protéger la base des mauvais agissements d'un utilisateur PostgreSQL. Je n'évoquerai pas la protection contre les utilisateurs système ou les administreurs.

La première chose à faire consiste à interdire les connexions distantes pour les super-utilisateurs. C'est une modification basique qui se fait simplement.

Cherchons tout d'abord le fichier pg_hba.conf. Sur mon système, il se trouve dans /home/pgdba/data/pg_hba.conf. Sur le vôtre, il est certainement ailleurs. Vous pouvez vérifier son emplacement en exécutant cette requête :

# show hba_file ; hba_file ------------------------------ /home/pgdba/data/pg_hba.conf (1 row)

Bien. Maintenant, voici le contenu de ce fichier sur ma machine :

local all all trust host all all 127.0.0.1/32 trust host all all ::1/128 trust host all all 0.0.0.0/0 md5

Si vous ne comprenez pas ce que cela signifie, consultez le manuel.

Maintenant, pour certaines raisons, j'ai trois comptes super-utilisateurs sur la machine : pgdba, postgres et depesz. Nous allons effectuer les modifications afin que ces comptes ne puissent être utilisés que par connexion locale (socket unix) et en fournissant un mot de passe. Toute autre tentative de connexion à ces comptes sera interdite.

Voici le nouveau fichier pg_hba.conf :

local all @admins md5 local all all trust host all @admins 0.0.0.0/0 reject host all all 127.0.0.1/32 trust host all all ::1/128 trust host all all 0.0.0.0/0 md5

Je viens également de créer un nouveau fichier, /home/pgdba/data/admins

depesz pgdba postgres

Remarque : N'oubliez pas qu'après chaque modification de votre fichier pg_hba.conf, vous devez redémarrer le serveur.

Comme nous le voyons clairement, le fichier admins contient les noms des trois utilisateurs auxquels je souhaite limiter l'accès.

Lorsque PostgreSQL vérifie le fichier pg_hab.conf, il s'arrête à la première ligne pertinente. Ainsi il est nécessaire de placer la ligne local all all trust après la ligne concernant le groupe d'administrateur ( local all @admins md5 ). Cependant, si l'on changeait l'ordre de la manière suivante :

local all all trust local all @admins md5 host all @admins 0.0.0.0/0 reject ...

alors tous les utilisateurs seraient autorisés à se connecter sur n'importe quel compte PostgreSQL. Ceci serait regrettable donc soyez prudent en configurant le fichier pg_hba.conf

Certaines personnes (notamment celles qui utilisent debian) sont fans de l'authentification ident sameuser. Les mots me manquent pour dire à quel point je déteste cela. Je ne vais pas rentrer dans les détails mais ce que je peux vous dire, c'est que j'utilise trust sur ma machine de développement (ordinateur portable) et uniquement md5 sur les serveurs de production. Je pense que l'authentification ident peut être utile dans certains cas, mais je n'ai pas trouvé lesquels. Pour le moment.

Bref, la première étape était relatiment simple : nous avons protéger la base contre des utilisateurs inopportuns qui voudraient se connecter en tant que super-utilisateurs (à distance, tout du moins).

Avant de poursuivre, évoquons les outils nommés dblink et dbilink. Souvenez-vous qu'utiliser ces modules abaissent la sécurité de votre système car ils modifient l'adresse IP depuis laquelle vous vous connectez. Je ne dis pas que ces modules sont mauvais : ils sont parfaitement sains, mais leur utilisation impose de réfléchir en préalable aux implications en terme de sécurité.

Revenons maintenant à notre tutoriel.

Nous avons notre base super top-secrète qui contient les orientations sexuelles de tout le monde. Nous ne voulons pas que n'importe quel utilisateur Postgres se connecte à la base à l'exception des utilisateurs dédiés (via une application web).

Comment faire ?

Revenons simplement au fichier pg_hba.conf et modifions-le :

local all @admins md5 local secret webapp md5 local secret all reject local all all trust host all @admins 0.0.0.0/0 reject host all all 127.0.0.1/32 trust host all all ::1/128 trust host all all 0.0.0.0/0 md5

Avec les lignes 2 et 3 (avec le mot 'secret'), j'autorise l'utisateur webapp à se connecter à la base secret en mode md5 et je rejette toutes les autres connections.

Il est important de laisser local all @admins md5 en première ligne. En effet, si je plaçais cette ligne après la ligne local secret all reject, cela interdirait les connexions super-utilisateurs.

Théoriquement, cette configuration semble correcte, mais vous devez procéder à deux configurations :

  1. Un compte super-utilisateur est équivalent à un accès shell, donc quelqu'un avec ce niveau d'accès pourrait modifier le fichier pg_hba.conf et recharger la configuration du serveur. Donc il n'y a pas de protection contre les super-utilisateurs.
  2. Désactiver les accès super-utilisateurs implique que chaque fois que vous exécuterez des opérations de haut niveau (ajout d'un nouveau langage, ajout de fonctions C), vous devrez modifier le fichier pg_hba.conf, ce qui augmente le risque d'erreur.

Pour ma part, je pense que laisser un accès aux super-utilisateurs est une bonne chose.

Maintenant, lorsque je tente de me connecter avec un autre utilisateur (disons l'utilisateur test), la connexion est rejetée :

=> psql -U test -d secret psql: FATAL: no pg_hba.conf entry for host "[local]", user "test", database "secret", SSL off

Parfait. Personne ne peut se connecter à la base, à l'exception d'un utilisateur précis (webapp). Mais voici un problème : si quelqu'un obtenait l'accès à ce compte webapp, il/elle pourrait faire ce qu'il/elle veut. Supprimer des tables ? Pas de problème. Vider des tables ? Pas de problème. Mettre à jour ? insérer des données ? tout cela fonctionnera.

Nous ne pouvons retirer ses droits au proprétaires des tables. Donc nous allons avoir besoin d'un utilisateur supplémentaire. Créons un utilisateur admin (qui ne serait pas super-utilisateur mais simplement administrateur de la base). L'utilisateur webapp sera dédié à l'application web et aura des droits restreints.

Nous devons alors :

  • créer l'utilisateur admin
  • déclarer admin propriétaire de tous les objets de la base
  • donner à admin la possibilité de se connecter
  • accorder et révoquer certains droits à webapp

Cela semble simple. La commande create user est triviale. Le changement de propriétaire est fastidieux mais simple. Attribuer les droits d'administration requiert simplement d'ajouter la ligne suivant dans le fichier pg_hba.conf :

local secret admin md5

avant la ligne :

local secret all reject

(et bien sûr redémarrer le serveur).

Maintenant, passons à la partie revoke/grant :)

Notre base a une table (et une sequence) :

> \d List of relations Schema | Name | Type | Owner --------+--------------+----------+------- public | users | table | admin public | users_id_seq | sequence | admin (2 rows)   >\d users Table "public.users" Column | Type | Modifiers ---------------+---------+---------------------------------------------------- id | integer | not null default nextval('users_id_seq'::regclass) username | text | not null sex_preference | text | Indexes: "users_pkey" PRIMARY KEY, btree (id) "users_username_key" UNIQUE, btree (username)   > select * from users; id | username | sex_preference ----+----------+----------------   1 | aa | qqq   2 | bb | www   3 | cc | eee (3 rows)

Rien de bien passionant, juste quelques données de test.

Bien sûr, maintenant, webapp ne peut plus rien lire dans la table users :

(webapp@[local]:5830) 22:56:51 [secret] > select * from users; ERROR: permission denied for relation users

Maintenant, le problème est qu'un utilisateur malveillant peut (par exemple) créer une table et la remplir aléatoirement, juste pour planter PostgreSQL.

La solution est simple :

  • se connecter à la base secret en tant que super-utilisateur
  • révoquer les droits de création au groupe public (le groupe qui contient tout les utilisateurs.
  • accorder les droits de création à l'utilisateur admin

Ce qui nous donne :

(admin@[local]:5830) 23:12:06 [secret]   > \c - depesz Password for user "depesz": You are now connected to database "secret" as user "depesz". (depesz@[local]:5830) 23:12:10 [secret]   # revoke create on schema public from public; REVOKE (depesz@[local]:5830) 23:12:15 [secret]   # grant create on schema public to admin; GRANT (depesz@[local]:5830) 23:12:24 [secret]   # \c - admin Password for user "admin": You are now connected to database "secret" as user "admin".

Il est nécessaire de se connecter à un compte super-utilisateur car le schéma public appartient aux super-utilisateurs et non pas au propriétaire de la base.

OK. Résumons la situation :

  1. Les super-utilisateurs peuvent se connecter uniquement via sockets unix, c'est-à-dire uniquement en local.
  2. Seuls deux utilisateurs peuvent se connecter à la base secret : admin et webapp.
  3. L'utilisateur admin peut faire ce qu'il veut
  4. L'utilisateur webapp peut se connecter et c'est à peu près tout. Il peut voir les noms des tables mais ne peut ni créer des tables, ni sélectionner/insérer/modifier/supprimer des données dans les tables existantes.

Nous souhaitons maintenant que l'utilisateur webapp puisse lire les donnée. Nous pourrions faire quelque chose comme :

grant select on table users to webapp;

Mais ce n'est pas très cool. Les données de la table users sont très sensibles et nous ne voulons pas que l'utilisateur puisse éxecuter la commande : select * from users; et ainsi obtenir des informations sur la sexualité de millions de personnes. Nous voulons que l'utilisateur webapp soit capable de lire les donnée pour un utilisateur précis (les opérations INSERT, UPDATE et DELETE ne seront pas évoquées car la méthode est très similaire)

Comment pouvons-nous réaliser cela ? Avec les procédures stockées bien sûr. Connectez-vous sur le compte admin et créez une fonction :

CREATE OR REPLACE FUNCTION get_user_record(in_user TEXT) RETURNS setof users as $BODY$ DECLARE temprec users; BEGIN for temprec in SELECT * FROM users WHERE username = in_user LOOP RETURN next temprec; END loop; RETURN; END; $BODY$ language plpgsql SECURITY DEFINER;

Cependant, puisque la fonction est écrite en plpgsql, elle est disponible par défaut pour tout le monde. Améliorons la sécurité :

(admin@[local]:5830) 23:24:21 [secret] > revoke execute on function get_user_record (text) from public; REVOKE   (admin@[local]:5830) 23:26:00 [secret] > grant execute on function get_user_record(text) to webapp; GRANT

Désormais notre utilisateur webapp peut appeler la fonction en donnant le nom exact qu'il recherche :

(webapp@[local]:5830) 23:26:10 [secret] > select * from get_user_record('aa'); id | username | sex_preference ---+----------+---------------- 1 | aa | qqq (1 row)

mais il ne pourra pas obtenir tous les enregistrement de la table.

Ce qui est plus important : pour que la fonction soit fonctionnelle, nous n'avons pas besoin de d'autoriser la selection (grant select) sur la table users pour l'utilisateur webapp. Ce tour de magie est opéré par la déclaration du gestionnaire de sécurité ( SECURITY DEFINER ) dans la définition de la fonction.

Si vous n'êtes pas familier avec cette fonctionnalité, sachez qu'elle fonctionne plus ou moins comme suid sur les systèmes UNIX. Tous les utilisateurs appelant la fonction seront vus par PostgreSQL comme l'utilisateur qui a créé la fonction.

Ce qui nous amène aux recommandation suivantes :

  • Vous devez être très prudent en écrivant des functions "security definer". Cela a été longtemps discuté et la conclusion est que dans une base ouverte (où les simples utilisateurs peuvent créer des objets), les fonctions "security definer" peuvent être utilisées pour contourner les limitations.
  • Si vous utilisez current_user pour quoi que ce soit, cela ne fonctionnera pas. Vous obtiendrez invariablement le nom du créateur de la fonction.

Dans notre situation, tout est bon : webapp ne pourra pas créer ses propres objets, ainsi il ne sera pas capable d'utiliser la fonction pour pirater la base, de plus current_user n'est pas réellement utile dans une base qui ne possède que deux utilisateurs :)

Les fonctions "security definer" peuvent aussi définir des opérations INSERT/UPDATE/DELETE, qui feront les vérifications de paramètres nécessaires et exécuteront les actions requises, éventuellement acommpagnées par des traitements connexes, tel que le changement de login, la sauvegarde, la dénormalisation et ainsi de suite.

Une dernière point à aborder : l'utilisateur webapp peut voir les noms des tables. Personnellement, cela ne me dérange pas, mais certaines personne peuvent souhaiter cacher les noms d'objets. Je n'ai pas vraiment creuser ce point, mais cela parait simple à faire. Cette fois, nous allons modifier les droits du schema pg_catalog :

(admin@[local]:5830) 23:36:17 [secret] > \c - depesz Password for user "depesz": You are now connected to database "secret" as user "depesz".   (depesz@[local]:5830) 23:36:21 [secret] # revoke usage on schema pg_catalog from public; REVOKE   (depesz@[local]:5830) 23:36:27 [secret] # grant usage on schema pg_catalog to admin; GRANT   (depesz@[local]:5830) 23:36:38 [secret] # \c - admin Password for user "admin": You are now connected to database "secret" as user "admin".

(notez bien qu'il existe aussi un schéma d'informations, qui devra être modifié de la même façon, sinon il suffira d'interroger les tables et des vues du schéma information_schema pour obtenir les noms des objets !).

Après ceci, l'utilisateur admin peut toujours travailler commme avant tandis que l'utilisateur webapp est contraint à certaines limitations :

(webapp@[local]:5830) 23:34:49 [secret] > \d ERROR: permission denied for schema pg_catalog   (webapp@[local]:5830) 23:37:41 [secret] > select * from get_user_record('aa'); id | username | sex_preference ----+----------+----------------   1 | aa | qqq (1 row)   (webapp@[local]:5830) 23:37:46 [secret] > \df+ get_user_record ERROR: permission denied for schema pg_catalog (webapp@[local]:5830) 23:37:52 [secret] > \df get_user_record ERROR: permission denied for schema pg_catalog

Comme vous pouvez le voir, l'utilisateur webapp ne peut pas voir la liste des tables, mais il est capable d'appeler la fonction get_user_record(). Par ailleurs, ce qui est important, c'est qu'il n'est pas capable de voir le code source de la fonction, ni même les informations comme le nombre d'arguments.

Il est possible dans certains cas que révoquer les droits du schéma pg_catalog provoque de mauvais résultats, mais (comme vous le constatez dans l'exemple ci-dessus) cela fonctionne et c'est assez simple à mettre en place.

Récapitulons à nouveau ce que nous avons accompli :

  1. Les super-utilisateurs peuvent se connecter uniquement via sockets unix, c'est-à-dire uniquement en local.
  2. Seuls deux utilisateurs peuvent se connecter à la base secret : admin et webapp.
  3. L'utilisateur admin peut faire ce qu'il veut
  4. L'utilisateur webapp peut se connecter à la base
  5. L'utilisateur webapp ne peut pas voir les noms des objets (tables, vues, fonctions) ni leurs définitions.
  6. L'utilisateur webapp ne peut pas interroger les tables directement.
  7. L'utilisateur webapp a accès aux données via des fonctions qui impliquent des vérifications de données.

Pour finir, voici quelques remarques :

1. En dehors des fonctions, les vues permettent également de limiter les droits d'accès : l'utilisateur n'a pas accès à la table mais a une vue batie sur celle-ci, ce qui permet de proposer une vue qui ne montre qu'une seule colonne de la table. Je n'en ai pas discuté ici car les vues n'ont pas la même flexibilité que les fonctions. De plus, dans les nouvelles versions de PostgreSQL, les fonctions sont presque aussi rapides que les vues.

2. Si vous avez déjà tout construit avec des fonctions, vous êtes prêt à utiliser l'outil pl/proxy pour effectuer de la répartition de charge (load-balancing).

3. Théoriquement on peut supposer qu'un utilisateur malveillant pourrait appeler la fonction get_user_record avec tous les noms possibles. C'est vrai, mais puisque nous pouvons écrire ce que nous voulons dans la fonction, il est assez simple de programmer la fonction afin qu'elle se bloque après le 3ème appel infructueux ou avertir l'administrateur par e-mail ou exécuter la commande pg_ctl stop pour empêcher immédiatement tout perte de données :)

4. Quand une base est protégée et en sécurité, souvenez-vous des sauvegardes. Je ne parle pas de sauvegarder la base. je parle de sauvegarder la base en toute sécurrité. Ne faites pas un simple dump de la base dans un fichier que vous entreposerez ailleurs. Chiffrer le fichier dump : au moins en utilisant "zip -e" au mieux avec gpg/pgp.