sql >> Database teknologi >  >> RDS >> PostgreSQL

Selvprovisionering af brugerkonti i PostgreSQL via uprivilegeret anonym adgang

Note fra Severalnines:Denne blog udgives posthumt, da Berend Tober døde den 16. juli 2018. Vi ærer hans bidrag til PostgreSQL-fællesskabet og ønsker fred for vores ven og gæsteskribent.

I tidligere artikel introducerede vi det grundlæggende i PostgreSQL-triggere og lagrede funktioner og leverede seks eksempler på brug, herunder datavalidering, ændringslogning, udledning af værdier fra indsatte data, dataskjul med simple opdaterbare visninger, vedligeholdelse af oversigtsdata i separate tabeller og sikker påkaldelse af kode ved forhøjet privilegium. Denne artikel bygger videre på dette grundlag og præsenterer en teknik, der anvender en trigger- og lagret funktion til at lette uddelegering af login-legitimationsoplysninger til roller med begrænset privilegium (dvs. ikke-superbruger). Denne funktion kan bruges til at reducere den administrative arbejdsbyrde for systemadministrationspersonale af høj værdi. Taget til det ekstreme, demonstrerer vi anonym slutbruger-selv-provisionering af login-legitimationsoplysninger, dvs. at lade potentielle databasebrugere levere login-legitimationsoplysninger på egen hånd ved at implementere "dynamisk SQL" inde i en lagret funktion, der udføres på passende omfang af privilegieniveau. Introduktion

Nyttig baggrundslæsning

Den nylige artikel af Sebastian Insausti om, hvordan du sikrer din PostgreSQL-database, indeholder nogle yderst relevante tips, du bør være bekendt med, nemlig tips #1 - #5 om klientgodkendelseskontrol, serverkonfiguration, bruger- og rollestyring, superbrugerstyring og Datakryptering. Vi bruger dele af hvert tip i denne artikel.

En anden nylig artikel af Joshua Otwell om PostgreSQL Privileges &User Management har også en god behandling af værtskonfiguration og brugerprivilegier, der går lidt mere i detaljer om disse to emner.

Beskyttelse af netværkstrafik

Den foreslåede funktion indebærer, at brugere kan levere database-login-legitimationsoplysninger, og mens de gør det, vil de angive deres nye login-navn og adgangskode over netværket. Beskyttelse af denne netværkskommunikation er afgørende og kan opnås ved at konfigurere PostgreSQL-serveren til at understøtte og kræve krypterede forbindelser. Transportlagssikkerhed er aktiveret i postgresql.conf-filen med "ssl"-indstillingen:

ssl = on

Værtsbaseret adgangskontrol

I det foreliggende tilfælde vil vi tilføje en værtsbaseret adgangskonfigurationslinje i filen pg_hba.conf, der tillader anonymt, dvs. betroet, login til databasen fra et passende undernetværk for populationen af ​​potentielle databasebrugere, der bogstaveligt talt bruger brugernavnet "anonym" og en anden konfigurationslinje, der kræver adgangskode-login for ethvert andet login-navn. Husk, at værtskonfigurationer påberåber sig det første match, så den første linje vil gælde, hver gang det "anonyme" brugernavn er angivet, hvilket tillader en pålidelig (dvs. ingen adgangskode påkrævet) forbindelse, og derefter, hver gang et andet brugernavn er angivet, kræves der en adgangskode. Hvis eksempeldatabasen "sampledb" f.eks. kun skal bruges af medarbejdere og internt til virksomhedsfaciliteter, kan vi konfigurere betroet adgang til et eller andet internt undernet, der ikke kan dirigeres med:

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

Hvis databasen skal gøres tilgængelig generelt for offentligheden, kan vi konfigurere "enhver adresse"-adgang:

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

Bemærk, at ovenstående er potentielt farligt uden yderligere forholdsregler, muligvis i applikationsdesignet eller på en firewall-enhed, for at hastighedsbegrænse brugen af ​​denne funktion, fordi du ved, at en eller anden script-kiddie vil automatisere endeløs kontooprettelse kun for lulz'en.

Bemærk også, at vi har specificeret forbindelsestypen som "hostssl", hvilket betyder, at forbindelser lavet ved hjælp af TCP/IP kun lykkes, når forbindelsen er oprettet med SSL-kryptering for at beskytte netværkstrafikken mod aflytning.

Låsning af det offentlige skema

Da vi tillader muligvis ukendte (dvs. ikke-pålidelige) personer at få adgang til databasen, vil vi gerne være sikre på, at standardadgange er begrænset. En vigtig foranstaltning er at tilbagekalde standardrettighederne til oprettelse af offentlige skemaobjekter for at afbøde en nyligt udgivet PostgreSQL-sårbarhed relateret til standardskemaprivilegier (jf. Locking Down the Public Schema by yours truly).

En prøvedatabase

Vi starter med en tom eksempeldatabase til illustrationsformål:

create database sampledb;
\connect sampledb

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

Vi opretter også den anonyme login-rolle svarende til den tidligere pg_hba.conf-indstilling.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

Og så gør vi noget nyt ved at definere en ukonventionel opfattelse:

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

Denne visning refererer til ingen tabel, og derfor returnerer en udvalgt forespørgsel altid en tom række:

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

En ting, dette gør for os, er på en måde at levere dokumentation eller et hint til slutbrugere om, hvilke data der kræves for at oprette en konto. Det vil sige, at ved at forespørge i tabellen, selvom resultatet er en tom række, afslører resultatet navnene på de to dataelementer.

Men endnu bedre, eksistensen af ​​denne visning tillader bestemmelse af de nødvendige datatyper:

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

Vi implementerer legitimationsleveringsfunktionaliteten med en gemt funktion og trigger, så lad os erklære en tom funktionsskabelon og den tilhørende 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();

Bemærk, at vi følger den foreslåede navngivningskonvention fra den foregående artikel, ved at bruge det tilhørende tabelnavn suffikset med en kort forkortelse, der angiver attributter for triggerforholdet mellem tabellen og den lagrede funktion for en ISTEAD FOR INSERT-trigger (dvs. suffikset " iit"). Vi har også tilføjet attributterne SCHEMA og SECURITY DEFINER til den lagrede funktion:førstnævnte, fordi det er god praksis at indstille søgestien, der gælder for varigheden af ​​funktionsudførelse, og sidstnævnte for at lette oprettelse af rolle, som normalt er en database-superbrugerautoritet kun, men vil i dette tilfælde blive uddelegeret til anonyme brugere.

Og til sidst tilføjer vi minimalt tilstrækkelige tilladelser til visningen for at forespørge og indsætte:

grant select, insert on table person to anonymous;
Download Whitepaper Today PostgreSQL Management &Automation med ClusterControlFå flere oplysninger om, hvad du skal vide for at implementere, overvåge, administrere og skalere PostgreSQLDownload Whitepaper

Lad os gennemgå

Før du implementerer den lagrede funktionskode, lad os gennemgå, hvad vi har. Først er der prøvedatabasen, der ejes af postgres-brugeren:

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

Og der er den visning, vi oprettede, og en liste over oprettelses- og læseadgangsprivilegier, der er givet til den anonyme bruger af postgres-brugeren:

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

Til sidst viser tabeldetaljen kolonnenavnene og datatyperne samt den tilhørende 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()

Dynamisk SQL

Vi vil anvende dynamisk SQL, dvs. konstruere den endelige form af en DDL-sætning ved kørsel delvist ud fra brugerindtastede data, for at udfylde triggerfunktionskroppen. Specifikt koder vi omridset af erklæringen for at skabe en ny login-rolle og udfylde de specifikke parametre som variabler.

Den generelle form for denne kommando er

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

hvor mulighed kan være en hvilken som helst af seksten specifikke egenskaber. Generelt er standardindstillingerne passende, men vi vil være eksplicitte omkring flere begrænsende muligheder og bruge formularen

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

hvor vi vil indsætte det brugerspecificerede rollenavn og adgangskode under kørsel.

Dynamisk konstruerede sætninger fremkaldes med execute-kommandoen:

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

som for vores specifikke behov ville se ud

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

hvor funktionen quote_literal returnerer streng-argumentet passende citeret til brug som streng-literal for at overholde det syntaktiske krav om, at adgangskoden faktisk skal citeres.

Når vi har bygget kommandostrengen, leverer vi den som argumentet til pl/pgsql execute-kommandoen i triggerfunktionen.

At sætte det hele sammen ser sådan ud:

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

Lad os prøve det!

Alt er på plads, så lad os give det snurre! Først skifter vi sessionsautorisation til den anonyme bruger og laver derefter en indsættelse mod personvisningen:

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

Resultatet er, at ny bruger alice er blevet tilføjet til systemtabellen:

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

Det virker endda direkte fra operativsystemets kommandolinje ved at overføre en SQL-kommandostreng til psql-klientværktøjet for at tilføje bruger 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 | {}

Anvend noget rustning

Det indledende eksempel på triggerfunktionen er sårbart over for SQL-injektionsangreb, dvs. en ondsindet trusselsaktør kan lave input, der resulterer i uautoriseret adgang. For eksempel, mens du er tilsluttet som den anonyme brugerrolle, mislykkes et forsøg på at gøre noget uden for omfanget korrekt:

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

Men følgende ondsindede input skaber en superbrugerrolle ved navn 'eve' (såvel som en lokkekonto ved navn '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 | {}

Så kan den skjulte superbrugerrolle bruges til at skabe kaos i databasen, for eksempel at slette brugerkonti (eller endnu værre!):

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

For at afbøde denne sårbarhed skal vi tage skridt til at rense inputtet. For eksempel at anvende funktionen citat_ident, som returnerer en streng, der er passende citeret til brug som en identifikator i en SQL-sætning med anførselstegn tilføjet, når det er nødvendigt, såsom hvis strengen indeholder ikke-identifikatortegn eller ville være store og små bogstaver, og korrekt fordoblet indlejret citater:

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

Hvis nu den samme SQL-indsprøjtningsudnyttelse forsøges at oprette en anden superbruger ved navn 'frank', mislykkes det, og resultatet er et meget uortodoks brugernavn:

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

Vi kan anvende yderligere fornuftig datavalidering i triggerfunktionen, såsom at kræve kun alfanumeriske brugernavne og afvise mellemrum og andre tegn:

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

og bekræft derefter, at de forskellige desinficeringstjek virker:

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.

Lad os øge det et hak

Antag, at vi ønsker at gemme yderligere metadata eller applikationsdata relateret til den oprettede brugerrolle, f.eks. måske et tidsstempel og kilde-IP-adresse forbundet med rolleoprettelse. Visningen kan naturligvis ikke opfylde dette nye krav, da der ikke er nogen underliggende opbevaring, så en egentlig tabel er påkrævet. Lad os også antage, at vi ønsker at begrænse synligheden af ​​denne tabel fra brugere, der logger på med den anonyme login-rolle. Vi kan skjule tabellen i et separat navneområde (dvs. et PostgreSQL-skema), som forbliver utilgængeligt for anonyme brugere. Lad os kalde dette navneområde for det "private" navneområde og oprette tabellen i navnerummet:

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

En simpel ekstra indsæt-kommando inde i triggerfunktionen registrerer disse tilknyttede metadata:

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

Og vi kan give det en nem test. Først bekræfter vi, at mens den er forbundet som den anonyme rolle, er kun public.person-visningen synlig og ikke private.person-tabellen:

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

Og så efter en ny rolle indsæt:

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)

tabellen private.person viser metadatafangsten for IP-adressen og tidspunktet for rækkeindsættelse.

Konklusion

I denne artikel har vi demonstreret en teknik til at uddelegere PostgreSQL-rolleoplysninger til ikke-superbrugerroller. Mens eksemplet fuldt ud delegerede legitimationsfunktionaliteten til anonyme brugere, kunne en lignende tilgang bruges til delvist at delegere funktionaliteten til kun betroet personale, mens man stadig bevarer fordelen ved at aflaste dette arbejde fra database- eller systemadministratorpersonale af høj værdi. Vi demonstrerede også en teknik med lagdelt dataadgang ved at bruge PostgreSQL-skemaer, selektivt afsløre eller skjule databaseobjekter. I den næste artikel i denne serie vil vi udvide den lagdelte dataadgangsteknik for at foreslå et nyt databasearkitekturdesign til applikationsimplementeringer.


  1. Sådan beregnes alder i MariaDB

  2. Forstå PostgreSQL-datotyper og -funktioner (ved eksempler)

  3. Hvordan opretter man en fremmednøgle med ON UPDATE CASCADE på Oracle?

  4. MS Access:Fordele og ulemper