PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Auto-approvisionnement des comptes d'utilisateurs dans PostgreSQL via un accès anonyme non privilégié

Note de Manynines :Ce blog est publié à titre posthume car Berend Tober est décédé le 16 juillet 2018. Nous honorons ses contributions à la communauté PostgreSQL et souhaitons la paix à notre ami et écrivain invité.

Dans l'article précédent, nous avons présenté les bases des déclencheurs PostgreSQL et des fonctions stockées et fourni six exemples de cas d'utilisation, y compris la validation des données, la journalisation des modifications, la dérivation des valeurs à partir des données insérées, le masquage des données avec de simples vues pouvant être mises à jour, la maintenance des données récapitulatives dans des tables séparées et invocation sécurisée du code avec des privilèges élevés. Cet article s'appuie davantage sur cette base et présente une technique utilisant un déclencheur et une fonction stockée pour faciliter la délégation de la fourniture d'informations d'identification de connexion à des rôles à privilèges limités (c'est-à-dire non superutilisateur). Cette fonctionnalité peut être utilisée pour réduire la charge de travail administratif du personnel d'administration des systèmes de grande valeur. Poussés à l'extrême, nous démontrons l'auto-provisionnement anonyme des identifiants de connexion par l'utilisateur final, c'est-à-dire en laissant les utilisateurs potentiels de la base de données fournir eux-mêmes les identifiants de connexion en implémentant le "SQL dynamique" dans une fonction stockée exécutée au niveau de privilège approprié. /P>

Lecture de fond utile

L'article récent de Sebastian Insausti sur la sécurisation de votre base de données PostgreSQL comprend des conseils très pertinents que vous devriez connaître, à savoir les conseils n°1 à n°5 sur le contrôle de l'authentification client, la configuration du serveur, la gestion des utilisateurs et des rôles, la gestion des super utilisateurs et Cryptage des données. Nous utiliserons des parties de chaque astuce dans cet article.

Un autre article récent de Joshua Otwell sur les privilèges et la gestion des utilisateurs de PostgreSQL présente également un bon traitement de la configuration de l'hôte et des privilèges des utilisateurs qui entre un peu plus en détail sur ces deux sujets.

Protéger le trafic réseau

La fonctionnalité proposée implique de permettre aux utilisateurs de fournir des informations d'identification de connexion à la base de données et, ce faisant, ils spécifieront leur nouveau nom de connexion et leur nouveau mot de passe sur le réseau. La protection de cette communication réseau est essentielle et peut être obtenue en configurant le serveur PostgreSQL pour prendre en charge et exiger des connexions cryptées. La sécurité de la couche de transport est activée dans le fichier postgresql.conf par le paramètre "ssl" :

ssl = on

Contrôle d'accès basé sur l'hôte

Pour le cas présent, nous allons ajouter une ligne de configuration d'accès basée sur l'hôte dans le fichier pg_hba.conf qui permet une connexion anonyme, c'est-à-dire de confiance, à la base de données à partir d'un sous-réseau approprié pour la population d'utilisateurs potentiels de la base de données en utilisant littéralement le nom d'utilisateur « anonyme » et une deuxième ligne de configuration nécessitant une connexion par mot de passe pour tout autre nom de connexion. N'oubliez pas que les configurations d'hôte invoquent la première correspondance, donc la première ligne s'appliquera chaque fois que le nom d'utilisateur "anonyme" est spécifié, permettant une connexion de confiance (c'est-à-dire, aucun mot de passe requis), puis chaque fois qu'un autre nom d'utilisateur est spécifié, un mot de passe sera requis. Par exemple, si l'exemple de base de données "sampledb" doit être utilisé, par exemple, par les employés uniquement et en interne dans les installations de l'entreprise, nous pouvons alors configurer un accès sécurisé pour un sous-réseau interne non routable avec :

# TYPE  DATABASE USER      ADDRESS        METHOD
hostssl sampledb anonymous 192.168.1.0/24 trust
hostssl sampledb all       192.168.1.0/24 md5

Si la base de données doit être mise à la disposition du public, nous pouvons alors configurer un accès "toute adresse" :

# TYPE  DATABASE USER       ADDRESS  METHOD
hostssl sampledb anonymous  all      trust
hostssl sampledb all        all      md5

Notez que ce qui précède est potentiellement dangereux sans précautions supplémentaires, éventuellement dans la conception de l'application ou au niveau d'un pare-feu, pour limiter l'utilisation de cette fonctionnalité, car vous savez qu'un script kiddie automatisera la création de compte sans fin juste pour le lulz.

Notez également que nous avons spécifié le type de connexion comme "hostssl", ce qui signifie que les connexions établies à l'aide de TCP/IP ne réussissent que lorsque la connexion est établie avec le cryptage SSL afin de protéger le trafic réseau contre les écoutes clandestines.

Verrouiller le schéma public

Étant donné que nous autorisons des personnes potentiellement inconnues (c'est-à-dire non fiables) à accéder à la base de données, nous voudrons nous assurer que les accès par défaut sont limités en capacité. Une mesure importante consiste à révoquer le privilège de création d'objet de schéma public par défaut afin d'atténuer une vulnérabilité PostgreSQL récemment publiée liée aux privilèges de schéma par défaut (cf. Verrouiller le schéma public par votre serviteur).

Un exemple de base de données

Nous allons commencer avec un exemple de base de données vide à des fins d'illustration :

create database sampledb;
\connect sampledb

revoke create on schema public from public;
alter default privileges revoke all privileges on tables from public;

Nous créons également le rôle de connexion anonyme correspondant au paramètre pg_hba.conf précédent.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

Et puis nous faisons quelque chose de nouveau en définissant une vue non conventionnelle :

create or replace view person as 
 select 
    null::name as login_name,
    null::name as login_pass;

Cette vue ne fait référence à aucune table et donc une requête de sélection renvoie toujours une ligne vide :

select * from person;
 login_name | login_pass 
------------+-------------
            | 
(1 row)

Cela nous permet en quelque sorte de fournir une documentation ou un indice aux utilisateurs finaux quant aux données requises pour créer un compte. Autrement dit, en interrogeant la table, même si le résultat est une ligne vide, le résultat révèle les noms des deux éléments de données.

Mais mieux encore, l'existence de cette vue permet de déterminer les types de données nécessaires :

\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 

Nous allons implémenter la fonctionnalité de fourniture d'informations d'identification avec une fonction et un déclencheur stockés. Déclarons donc un modèle de fonction vide et le déclencheur associé :

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as '
  begin
  end;
  ';

create trigger person_iit
  instead of insert
  on person
  for each row execute procedure person_iit();

Notez que nous suivons la convention de dénomination proposée dans l'article précédent, en utilisant le nom de table associé suffixé par une abréviation abrégée indiquant les attributs de la relation de déclenchement entre la table et la fonction stockée pour un déclencheur INSTEAD OF INSERT (c'est-à-dire le suffixe " ii »). Nous avons également ajouté à la fonction stockée les attributs SCHEMA et SECURITY DEFINER :le premier car il est recommandé de définir le chemin de recherche qui s'applique à la durée d'exécution de la fonction, et le second pour faciliter la création de rôle, qui est normalement une autorité de superutilisateur de base de données. seulement mais dans ce cas sera déléguée à des utilisateurs anonymes.

Et enfin, nous ajoutons des autorisations minimalement suffisantes sur la vue pour interroger et insérer :

grant select, insert on table person to anonymous;
Téléchargez le livre blanc aujourd'hui PostgreSQL Management &Automation with ClusterControlDécouvrez ce que vous devez savoir pour déployer, surveiller, gérer et faire évoluer PostgreSQLTélécharger le livre blanc

Passons en revue

Avant d'implémenter le code de fonction stocké, passons en revue ce que nous avons. Il y a d'abord l'exemple de base de données appartenant à l'utilisateur postgres :

\l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 sampledb  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
And there’s the user roles, including the database superuser and the newly-created anonymous login roles:
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Et il y a la vue que nous avons créée et une liste des privilèges d'accès de création et de lecture accordés à l'utilisateur anonyme par l'utilisateur postgres :

\d
         List of relations
 Schema |  Name  | Type |  Owner   
--------+--------+------+----------
 public | person | view | postgres
(1 row)


\dp
                                Access privileges
 Schema |  Name  | Type |     Access privileges     | Column privileges | Policies 
--------+--------+------+---------------------------+-------------------+----------
 public | person | view | postgres=arwdDxt/postgres+|                   | 
        |        |      | anonymous=ar/postgres     |                   | 
(1 row)

Enfin, le détail du tableau affiche les noms et types de données des colonnes ainsi que le déclencheur associé :

\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 
Triggers:
    person_iit INSTEAD OF INSERT ON person FOR EACH ROW EXECUTE PROCEDURE person_iit()

SQL dynamique

Nous allons utiliser SQL dynamique, c'est-à-dire construire la forme finale d'une instruction DDL au moment de l'exécution en partie à partir de données saisies par l'utilisateur, pour remplir le corps de la fonction de déclenchement. Plus précisément, nous codons en dur le contour de la déclaration pour créer un nouveau rôle de connexion et remplissons les paramètres spécifiques en tant que variables.

La forme générale de cette commande est

create role name [ [ with ] option [ ... ] ]

option peut être l'une des seize propriétés spécifiques. Généralement, les valeurs par défaut sont appropriées, mais nous allons être explicites sur plusieurs options de limitation et utiliser le formulaire

create role name 
  with 
    login 
    inherit 
    nosuperuser 
    nocreatedb 
    nocreaterole 
    password ‘password’;

où nous insérerons le nom de rôle et le mot de passe spécifiés par l'utilisateur au moment de l'exécution.

Les instructions construites dynamiquement sont appelées avec la commande d'exécution :

execute command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

qui, pour nos besoins spécifiques, ressemblerait

  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

où la fonction quote_literal renvoie l'argument de chaîne convenablement cité pour être utilisé comme littéral de chaîne afin de se conformer à l'exigence syntaxique selon laquelle le mot de passe doit en fait être cité..

Une fois que nous avons construit la chaîne de commande, nous la fournissons comme argument à la commande d'exécution pl/pgsql dans la fonction de déclenchement.

Mettre tout cela ensemble ressemble à :

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- note this is for demonstration only. it is vulnerable to sql injection.

  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Essayons !

Tout est en place, alors allons-y ! Nous passons d'abord l'autorisation de session à l'utilisateur anonyme, puis nous effectuons une insertion dans la vue de la personne :

set session authorization anonymous;
insert into person values ('alice', '1234');

Le résultat est que le nouvel utilisateur alice a été ajouté à la table système :

\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Il fonctionne même directement depuis la ligne de commande du système d'exploitation en redirigeant une chaîne de commande SQL vers l'utilitaire client psql pour ajouter l'utilisateur bob :

$ psql sampledb anonymous <<< "insert into person values ('bob', '4321');"
INSERT 0 1

$ psql sampledb anonymous <<< "\du"
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 bob       |                                                            | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Appliquer une armure

L'exemple initial de la fonction de déclenchement est vulnérable à l'attaque par injection SQL, c'est-à-dire qu'un acteur malveillant malveillant pourrait créer une entrée qui entraînerait un accès non autorisé. Par exemple, lorsque vous êtes connecté en tant qu'utilisateur anonyme, une tentative de faire quelque chose hors de portée échoue de manière appropriée :

set session authorization anonymous;
drop user alice;
ERROR:  permission denied to drop role

Mais l'entrée malveillante suivante crée un rôle de superutilisateur nommé "eve" (ainsi qu'un compte leurre nommé "cathy") :

insert into person 
  values ('eve with superuser login password ''666''; create role cathy', '777');
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 cathy     |                                                            | {}
 eve       | Superuser                                                  | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Ensuite, le rôle de superutilisateur subreptice peut être utilisé pour faire des ravages dans la base de données, par exemple en supprimant des comptes d'utilisateurs (ou pire !) :

\c - eve
drop user alice;
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 anonymous | No inheritance                                             | {}
 cathy     |                                                            | {}
 eve       | Superuser                                                  | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Pour atténuer cette vulnérabilité, nous devons prendre des mesures pour assainir l'entrée. Par exemple, l'application de la fonction quote_ident, qui renvoie une chaîne convenablement citée pour être utilisée comme identifiant dans une instruction SQL avec des guillemets ajoutés si nécessaire, par exemple si la chaîne contient des caractères non identificateurs ou serait pliée en casse, et correctement doublé incorporé guillemets :

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Maintenant, si le même exploit d'injection SQL est tenté de créer un autre super-utilisateur nommé "frank", il échoue et le résultat est un nom d'utilisateur très peu orthodoxe :

set session authorization anonymous;
insert into person 
  values ('frank with superuser login password ''666''; create role dave', '777');
\du
                                 List of roles
    Role name          |                         Attributes                         | Member of 
-----------------------+------------------------------------------------------------+----------
 anonymous             | No inheritance                                             | {}
 eve                   | Superuser                                                  | {}
 frank with superuser  |                                                            |
  login password '666';|                                                            |
  create role dave     |                                                            |
 postgres              | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Nous pouvons appliquer une validation supplémentaire des données sensibles dans la fonction de déclenchement, par exemple en n'exigeant que des noms d'utilisateur alphanumériques et en rejetant les espaces blancs et autres caractères :

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- Basic input sanitization

  if new.login_name is null then
    raise exception 'null login_name disallowed';
  elsif position(' ' in new.login_name) > 0 then
    raise exception 'login_name whitespace disallowed';
  elsif length(new.login_name) = 0 then
    raise exception 'login_name must be non-empty';
  elsif not (select new.login_name similar to '[A-Za-z]%') then
    raise exception 'login_name must begin with a letter.';
  end if;

  if new.login_pass is null then
    raise exception 'null login_pass disallowed';
  elsif position(' ' in new.login_pass) > 0 then
    raise exception 'login_pass whitespace disallowed';
  elsif length(new.login_pass) = 0 then
    raise exception 'login_pass must be non-empty';
  end if;

  -- Provision login credentials

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

puis confirmez que les différents contrôles de désinfection fonctionnent :

set session authorization anonymous;
insert into person values (NULL, NULL);
ERROR:  null login_name disallowed
insert into person values ('gina', NULL);
ERROR:  null login_pass disallowed
insert into person values ('gina', '');
ERROR:  login_pass must be non-empty
insert into person values ('', '1234');
ERROR:  login_name must be non-empty
insert into person values ('gi na', '1234');
ERROR:  login_name whitespace disallowed
insert into person values ('1gina', '1234');
ERROR:  login_name must begin with a letter.

Passons à la vitesse supérieure

Supposons que nous souhaitions stocker des métadonnées ou des données d'application supplémentaires liées au rôle d'utilisateur créé, par exemple, peut-être un horodatage et une adresse IP source associés à la création du rôle. La vue ne peut bien sûr pas satisfaire cette nouvelle exigence car il n'y a pas de stockage sous-jacent, donc une table réelle est requise. Supposons également que nous voulions restreindre la visibilité de cette table aux utilisateurs se connectant avec le rôle de connexion anonyme. Nous pouvons cacher la table dans un espace de noms séparé (c'est-à-dire un schéma PostgreSQL) qui reste inaccessible aux utilisateurs anonymes. Appelons cet espace de noms l'espace de noms "privé" et créons la table dans l'espace de noms :

create schema private;

create table private.person (
  login_name   name not null primary key,
  inet_client_addr inet default inet_client_addr(),
  create_time timestamptz default now()  
);

Une simple commande d'insertion supplémentaire à l'intérieur de la fonction de déclenchement enregistre ces métadonnées associées :

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- Basic input sanitization
  if new.login_name is null then
    raise exception 'null login_name disallowed';
  elsif position(' ' in new.login_name) > 0 then
    raise exception 'login_name whitespace disallowed';
  elsif length(new.login_name) = 0 then
    raise exception 'login_name must be non-empty';
  elsif not (select new.login_name similar to '[A-Za-z]%') then
    raise exception 'login_name must begin with a letter.';
  end if;

  if new.login_pass is null then
    raise exception 'null login_pass disallowed';
  elsif length(new.login_pass) = 0 then
    raise exception 'login_pass must be non-empty';
  end if;

  -- Record associated metadata
  insert into private.person values (new.login_name);

  -- Provision login credentials

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Et nous pouvons lui donner un test facile. Tout d'abord, nous confirmons que lorsque vous êtes connecté en tant que rôle anonyme, seule la vue public.person est visible et non la table private.person :

set session authorization anonymous;

\d
         List of relations
 Schema |  Name  | Type |  Owner   
--------+--------+------+----------
 public | person | view | postgres
(1 row)
                   
select * from private.person;
ERROR:  permission denied for schema private

Et puis après un nouveau rôle insérez :

insert into person values ('gina', '1234');

reset session authorization;

select * from private.person;
 login_name | inet_client_addr |          create_time          
------------+------------------+-------------------------------
 gina       | 192.168.2.106    | 2018-06-24 07:56:13.838679-07
(1 row)

la table private.person affiche la capture des métadonnées pour l'adresse IP et l'heure d'insertion de la ligne.

Conclusion

Dans cet article, nous avons démontré une technique pour déléguer le provisionnement des informations d'identification de rôle PostgreSQL à des rôles non superutilisateurs. Bien que l'exemple délègue entièrement la fonctionnalité d'identification à des utilisateurs anonymes, une approche similaire pourrait être utilisée pour déléguer partiellement la fonctionnalité uniquement au personnel de confiance tout en conservant l'avantage de décharger ce travail du personnel d'administration de base de données ou de système de grande valeur. Nous avons également démontré une technique d'accès aux données en couches utilisant des schémas PostgreSQL, exposant ou masquant sélectivement les objets de la base de données. Dans le prochain article de cette série, nous développerons la technique d'accès aux données en couches pour proposer une nouvelle conception d'architecture de base de données pour les implémentations d'applications.