PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Selbstbereitstellung von Benutzerkonten in PostgreSQL über unprivilegierten anonymen Zugriff

Anmerkung von Multiplenines:Dieser Blog wird posthum veröffentlicht, da Berend Tober am 16. Juli 2018 verstorben ist. Wir ehren seine Beiträge zur PostgreSQL-Community und wünschen unserem Freund und Gastautor Frieden.

Im vorherigen Artikel haben wir die Grundlagen von PostgreSQL-Triggern und gespeicherten Funktionen vorgestellt und sechs Beispielanwendungsfälle bereitgestellt, darunter Datenvalidierung, Änderungsprotokollierung, Ableitung von Werten aus eingefügten Daten, Datenverbergung mit einfachen aktualisierbaren Ansichten, Verwaltung von Zusammenfassungsdaten in separaten Tabellen und sicherer Aufruf von Code mit erhöhten Rechten. Dieser Artikel baut weiter auf dieser Grundlage auf und stellt eine Technik vor, die einen Auslöser und eine gespeicherte Funktion verwendet, um das Delegieren der Bereitstellung von Anmeldeinformationen an Rollen mit eingeschränkten Rechten (d. h. Nicht-Superuser) zu erleichtern. Diese Funktion kann verwendet werden, um die Verwaltungsarbeitslast für hochrangiges Systemverwaltungspersonal zu reduzieren. Auf die Spitze getrieben demonstrieren wir die anonyme Selbstbereitstellung von Anmeldeinformationen durch Endbenutzer, d. h. potenzielle Datenbankbenutzer können Anmeldeinformationen selbst bereitstellen, indem sie „dynamisches SQL“ innerhalb einer gespeicherten Funktion implementieren, die auf einer angemessenen Berechtigungsebene ausgeführt wird.Einführung

Hilfreiche Hintergrundlektüre

Der kürzlich erschienene Artikel von Sebastian Insausti zum Thema Sichern Ihrer PostgreSQL-Datenbank enthält einige äußerst relevante Tipps, mit denen Sie vertraut sein sollten, nämlich die Tipps Nr. 1 bis Nr. 5 zur Client-Authentifizierungskontrolle, Serverkonfiguration, Benutzer- und Rollenverwaltung, Super-User-Verwaltung und Datenverschlüsselung. Wir verwenden Teile jedes Tipps in diesem Artikel.

Ein weiterer kürzlich erschienener Artikel von Joshua Otwell über PostgreSQL-Berechtigungen und Benutzerverwaltung enthält ebenfalls eine gute Behandlung der Hostkonfiguration und der Benutzerberechtigungen, die etwas detaillierter auf diese beiden Themen eingeht.

Schutz des Netzwerkverkehrs

Die vorgeschlagene Funktion beinhaltet, dass Benutzer Zugangsdaten für die Datenbank bereitstellen können und dabei ihren neuen Anmeldenamen und ihr Passwort über das Netzwerk angeben. Der Schutz dieser Netzwerkkommunikation ist unerlässlich und kann erreicht werden, indem der PostgreSQL-Server so konfiguriert wird, dass er verschlüsselte Verbindungen unterstützt und erfordert. Die Sicherheit der Transportschicht wird in der Datei postgresql.conf durch die Einstellung „ssl“ aktiviert:

ssl = on

Hostbasierte Zugriffskontrolle

Für den vorliegenden Fall fügen wir eine Konfigurationszeile für den hostbasierten Zugriff in die Datei pg_hba.conf ein, die eine anonyme, d. h. vertrauenswürdige Anmeldung bei der Datenbank von einem geeigneten Subnetzwerk aus für die Population potenzieller Datenbankbenutzer ermöglicht, die buchstäblich den Benutzernamen verwendet „anonym“ und eine zweite Konfigurationszeile, die eine Passwortanmeldung für jeden anderen Anmeldenamen erfordert. Denken Sie daran, dass Hostkonfigurationen die erste Übereinstimmung aufrufen, sodass die erste Zeile immer dann gilt, wenn der „anonyme“ Benutzername angegeben wird, was eine vertrauenswürdige (d. h. kein Kennwort erforderlich) Verbindung zulässt, und anschließend immer dann, wenn ein anderer Benutzername angegeben wird, ist ein Kennwort erforderlich. Wenn beispielsweise die Beispieldatenbank „sampledb“ beispielsweise nur von Mitarbeitern und intern für Unternehmenseinrichtungen verwendet werden soll, können wir einen vertrauenswürdigen Zugriff für ein nicht routbares internes Subnetz konfigurieren mit:

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

Wenn die Datenbank allgemein öffentlich zugänglich gemacht werden soll, können wir den Zugriff „beliebige Adresse“ konfigurieren:

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

Beachten Sie, dass das Obige ohne zusätzliche Vorkehrungen, möglicherweise im Anwendungsdesign oder bei einem Firewall-Gerät, potenziell gefährlich ist, um die Verwendung dieser Funktion zu begrenzen, da Sie wissen, dass einige Skript-Kiddies die endlose Kontoerstellung nur für den Lulz automatisieren.

Beachten Sie auch, dass wir den Verbindungstyp als „hostssl“ angegeben haben, was bedeutet, dass Verbindungen, die über TCP/IP hergestellt werden, nur erfolgreich sind, wenn die Verbindung mit SSL-Verschlüsselung hergestellt wird, um den Netzwerkverkehr vor Abhören zu schützen.

Das öffentliche Schema sperren

Da wir möglicherweise unbekannten (d. h. nicht vertrauenswürdigen) Personen den Zugriff auf die Datenbank gestatten, möchten wir sichergehen, dass die Standardzugriffe eingeschränkt sind. Eine wichtige Maßnahme besteht darin, das Standardprivileg zum Erstellen öffentlicher Schemaobjekte zu widerrufen, um eine kürzlich veröffentlichte PostgreSQL-Schwachstelle im Zusammenhang mit Standardschemaprivilegien zu entschärfen (vgl. Sperren des öffentlichen Schemas von Ihnen).

Eine Beispieldatenbank

Zur Veranschaulichung beginnen wir mit einer leeren Beispieldatenbank:

create database sampledb;
\connect sampledb

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

Wir erstellen auch die anonyme Anmelderolle, die der früheren pg_hba.conf-Einstellung entspricht.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

Und dann machen wir etwas Neues, indem wir eine unkonventionelle Sichtweise definieren:

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

Diese Ansicht referenziert keine Tabelle, daher gibt eine Auswahlabfrage immer eine leere Zeile zurück:

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

Eine Sache, die dies für uns bedeutet, ist gewissermaßen eine Dokumentation oder ein Hinweis für Endbenutzer, welche Daten für die Einrichtung eines Kontos erforderlich sind. Das heißt, dass beim Abfragen der Tabelle die Namen der beiden Datenelemente angezeigt werden, obwohl das Ergebnis eine leere Zeile ist.

Aber noch besser, die Existenz dieser Ansicht ermöglicht die Bestimmung der erforderlichen Datentypen:

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

Wir werden die Funktionalität zur Bereitstellung von Anmeldeinformationen mit einer gespeicherten Funktion und einem Trigger implementieren, also deklarieren wir eine leere Funktionsvorlage und den zugehörigen Trigger:

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();

Beachten Sie, dass wir der vorgeschlagenen Namenskonvention aus dem vorherigen Artikel folgen und den zugehörigen Tabellennamen mit einem angehängten Kürzel verwenden, das Attribute der Triggerbeziehung zwischen der Tabelle und der gespeicherten Funktion für einen INSTEAD OF INSERT-Trigger angibt (d. h. Suffix „ ich s"). Wir haben der gespeicherten Funktion auch die Attribute SCHEMA und SECURITY DEFINER hinzugefügt:Ersteres, weil es sich bewährt hat, den Suchpfad festzulegen, der für die Dauer der Funktionsausführung gilt, und Letzteres, um die Rollenerstellung zu erleichtern, die normalerweise eine Datenbank-Superuser-Berechtigung ist nur, wird aber in diesem Fall an anonyme Benutzer delegiert.

Und schließlich fügen wir der Ansicht minimal ausreichende Berechtigungen zum Abfragen und Einfügen hinzu:

grant select, insert on table person to anonymous;
Laden Sie noch heute das Whitepaper PostgreSQL-Verwaltung und -Automatisierung mit ClusterControl herunterErfahren Sie, was Sie wissen müssen, um PostgreSQL bereitzustellen, zu überwachen, zu verwalten und zu skalierenLaden Sie das Whitepaper herunter

Lassen Sie uns überprüfen

Bevor wir den gespeicherten Funktionscode implementieren, sehen wir uns an, was wir haben. Zuerst gibt es die Beispieldatenbank, die dem postgres-Benutzer gehört:

\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 | {}

Und da ist die Ansicht, die wir erstellt haben, und eine Liste der Zugriffsrechte zum Erstellen und Lesen, die dem anonymen Benutzer vom Postgres-Benutzer gewährt wurden:

\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)

Zuletzt zeigt das Tabellendetail die Spaltennamen und Datentypen sowie den zugehörigen Trigger:

\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()

Dynamisches SQL

Wir werden dynamisches SQL verwenden, d. h. die endgültige Form einer DDL-Anweisung zur Laufzeit teilweise aus vom Benutzer eingegebenen Daten erstellen, um den Hauptteil der Triggerfunktion zu füllen. Insbesondere codieren wir den Umriss der Anweisung fest, um eine neue Anmelderolle zu erstellen, und füllen die spezifischen Parameter als Variablen aus.

Die allgemeine Form dieses Befehls ist

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

wobei Option kann eine von sechzehn spezifischen Eigenschaften sein. Im Allgemeinen sind die Standardwerte angemessen, aber wir werden einige einschränkende Optionen explizit ansprechen und das Formular verwenden

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

wo wir den vom Benutzer angegebenen Rollennamen und das Passwort zur Laufzeit einfügen.

Dynamisch konstruierte Anweisungen werden mit dem Ausführungsbefehl aufgerufen:

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

was für unsere spezifischen Bedürfnisse aussehen würde

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

wobei die Funktion quote_literal das String-Argument zurückgibt, das für die Verwendung als String-Literal geeignet in Anführungszeichen gesetzt ist, um die syntaktische Anforderung zu erfüllen, dass das Passwort tatsächlich in Anführungszeichen gesetzt werden muss.

Sobald wir die Befehlszeichenfolge erstellt haben, liefern wir sie als Argument für den pl/pgsql-Ausführungsbefehl innerhalb der Trigger-Funktion.

Alles zusammen sieht so aus:

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;
  $$;

Lass es uns versuchen!

Alles ist an Ort und Stelle, also lass es uns drehen! Zuerst schalten wir die Sitzungsautorisierung auf den anonymen Benutzer um und führen dann eine Einfügung gegen die Personenansicht durch:

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

Das Ergebnis ist, dass der neue Benutzer Alice zur Systemtabelle hinzugefügt wurde:

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

Es funktioniert sogar direkt über die Befehlszeile des Betriebssystems, indem eine SQL-Befehlszeichenfolge an das psql-Client-Dienstprogramm weitergeleitet wird, um den Benutzer bob:

hinzuzufügen
$ 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 | {}

Einige Rüstung anlegen

Das erste Beispiel der Trigger-Funktion ist anfällig für SQL-Injection-Angriffe, d. h. ein böswilliger Angreifer könnte Eingaben erstellen, die zu einem unbefugten Zugriff führen. Während Sie beispielsweise als anonyme Benutzerrolle verbunden sind, schlägt ein Versuch, etwas außerhalb des Geltungsbereichs zu tun, entsprechend fehl:

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

Aber die folgende böswillige Eingabe erstellt eine Superuser-Rolle namens „eve“ (sowie ein Köderkonto namens „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 | {}

Dann kann die heimliche Superuser-Rolle verwendet werden, um Chaos in der Datenbank anzurichten, zum Beispiel das Löschen von Benutzerkonten (oder Schlimmeres!):

\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 | {}

Um diese Schwachstelle zu mindern, müssen wir Maßnahmen ergreifen, um die Eingabe zu bereinigen. Zum Beispiel das Anwenden der Funktion quote_ident, die eine Zeichenfolge zurückgibt, die für die Verwendung als Bezeichner in einer SQL-Anweisung geeignet in Anführungszeichen gesetzt ist, wobei bei Bedarf Anführungszeichen hinzugefügt werden, z Anführungszeichen:

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;
  $$;

Wenn nun versucht wird, mit demselben SQL-Injection-Exploit einen weiteren Superuser namens „frank“ zu erstellen, schlägt dies fehl und das Ergebnis ist ein sehr unorthodoxer Benutzername:

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 | {}

Wir können weitere sinnvolle Datenvalidierungen innerhalb der Trigger-Funktion anwenden, z. B. nur alphanumerische Benutzernamen verlangen und Leerzeichen und andere Zeichen ablehnen:

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;
  $$;

und bestätigen Sie dann, dass die verschiedenen Bereinigungsprüfungen funktionieren:

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.

Lass uns eine Stufe höher gehen

Angenommen, wir möchten zusätzliche Metadaten oder Anwendungsdaten in Bezug auf die erstellte Benutzerrolle speichern, z. B. möglicherweise einen Zeitstempel und eine Quell-IP-Adresse, die mit der Rollenerstellung verbunden sind. Die Ansicht kann diese neue Anforderung natürlich nicht erfüllen, da es keinen zugrunde liegenden Speicher gibt, sodass eine tatsächliche Tabelle erforderlich ist. Nehmen wir außerdem an, wir möchten die Sichtbarkeit dieser Tabelle für Benutzer einschränken, die sich mit der anonymen Anmelderolle anmelden. Wir können die Tabelle in einem separaten Namensraum (d. h. einem PostgreSQL-Schema) verstecken, der für anonyme Benutzer unzugänglich bleibt. Nennen wir diesen Namensraum den „privaten“ Namensraum und erstellen die Tabelle im Namensraum:

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()  
);

Ein einfacher zusätzlicher Einfügebefehl innerhalb der Triggerfunktion zeichnet diese zugehörigen Metadaten auf:

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;
  $$;

Und wir können es einem einfachen Test unterziehen. Zuerst bestätigen wir, dass während der Verbindung als anonyme Rolle nur die public.person-Ansicht sichtbar ist und nicht die private.person-Tabelle:

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

Und dann nach einer neuen Rolle einfügen:

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)

Die private.person-Tabelle zeigt die Metadatenerfassung für die IP-Adresse und die Zeileneinfügezeit.

Schlussfolgerung

In diesem Artikel haben wir eine Technik demonstriert, um die Bereitstellung von PostgreSQL-Rollenanmeldeinformationen an Nicht-Superuser-Rollen zu delegieren. Während das Beispiel die Berechtigungsnachweisfunktion vollständig an anonyme Benutzer delegierte, könnte ein ähnlicher Ansatz verwendet werden, um die Funktionalität teilweise nur an vertrauenswürdiges Personal zu delegieren, während der Vorteil erhalten bleibt, diese Arbeit von wertvollem Datenbank- oder Systemadministratorpersonal zu entlasten. Wir demonstrierten auch eine Technik des mehrschichtigen Datenzugriffs unter Verwendung von PostgreSQL-Schemas, die Datenbankobjekte selektiv verfügbar machen oder verbergen. Im nächsten Artikel dieser Serie werden wir die mehrschichtige Datenzugriffstechnik erweitern, um ein neuartiges Datenbankarchitekturdesign für Anwendungsimplementierungen vorzuschlagen.