Anteckning från Severalnines:Den här bloggen publiceras postumt när Berend Tober gick bort den 16 juli 2018. Vi hedrar hans bidrag till PostgreSQL-communityt och önskar fred för vår vän och gästskribent.
I tidigare artikel introducerade vi grunderna för PostgreSQL-utlösare och lagrade funktioner och gav sex exempel på användningsfall inklusive datavalidering, ändringsloggning, härledning av värden från infogade data, datadöljning med enkla uppdateringsbara vyer, underhåll av sammanfattningsdata i separata tabeller och säker anrop av kod med förhöjd behörighet. Den här artikeln bygger vidare på den grunden och presenterar en teknik som använder en utlösare och lagrad funktion för att underlätta delegering av inloggningsuppgifter till roller med begränsade privilegier (dvs. icke-superanvändare). Den här funktionen kan användas för att minska den administrativa arbetsbelastningen för högt värdefulla systemadministratörer. Till det yttersta demonstrerar vi anonym slutanvändares självtillhandahållande av inloggningsuppgifter, d.v.s. låter potentiella databasanvändare tillhandahålla inloggningsuppgifter på egen hand genom att implementera "dynamisk SQL" i en lagrad funktion som körs på behörighetsnivå med lämplig omfattning. Inledning
Användbar bakgrundsläsning
Den senaste artikeln av Sebastian Insausti om hur du säkrar din PostgreSQL-databas innehåller några mycket relevanta tips som du bör känna till, nämligen tips #1 - #5 om klientautentiseringskontroll, serverkonfiguration, användar- och rollhantering, superanvändarhantering och Datakryptering. Vi kommer att använda delar av varje tips i den här artikeln.
En annan artikel nyligen av Joshua Otwell om PostgreSQL Privileges &User Management har också en bra behandling av värdkonfiguration och användarprivilegier som går in lite mer i detalj på dessa två ämnen.
Skydda nätverkstrafik
Den föreslagna funktionen innebär att användarna kan tillhandahålla inloggningsuppgifter för databasen och medan de gör det kommer de att ange sitt nya inloggningsnamn och lösenord över nätverket. Skydd av denna nätverkskommunikation är väsentligt och kan uppnås genom att konfigurera PostgreSQL-servern för att stödja och kräva krypterade anslutningar. Transportlagersäkerheten är aktiverad i postgresql.conf-filen med inställningen "ssl":
ssl = on
Värdbaserad åtkomstkontroll
I det aktuella fallet kommer vi att lägga till en värdbaserad åtkomstkonfigurationsrad i filen pg_hba.conf som tillåter anonym, d.v.s. pålitlig, inloggning till databasen från något lämpligt undernätverk för populationen av potentiella databasanvändare som bokstavligen använder användarnamnet "anonym" och en andra konfigurationsrad som kräver lösenordsinloggning för alla andra inloggningsnamn. Kom ihåg att värdkonfigurationer anropar den första matchningen, så den första raden kommer att gälla närhelst det "anonyma" användarnamnet anges, vilket tillåter en pålitlig (dvs inget lösenord krävs) anslutning, och sedan, när något annat användarnamn anges, kommer ett lösenord att krävas. Om exempeldatabasen "sampledb" till exempel endast ska användas av anställda och internt på företagsanläggningar, kan vi konfigurera betrodd åtkomst för något icke-routbart internt subnät med:
# TYPE DATABASE USER ADDRESS METHOD
hostssl sampledb anonymous 192.168.1.0/24 trust
hostssl sampledb all 192.168.1.0/24 md5
Om databasen ska göras allmänt tillgänglig för allmänheten, kan vi konfigurera "valfri adress"-åtkomst:
# TYPE DATABASE USER ADDRESS METHOD
hostssl sampledb anonymous all trust
hostssl sampledb all all md5
Notera att ovanstående är potentiellt farligt utan ytterligare försiktighetsåtgärder, möjligen i applikationsdesignen eller på en brandväggsenhet, för att hastighetsbegränsa användningen av den här funktionen, eftersom du vet att någon scriptkiddie kommer att automatisera oändligt kontoskapande bara för lulz.
Observera också att vi har angett anslutningstypen som "hostssl", vilket betyder att anslutningar som görs med TCP/IP bara lyckas när anslutningen görs med SSL-kryptering för att skydda nätverkstrafiken från avlyssning.
Låsa ner det offentliga schemat
Eftersom vi tillåter möjligen okända (d.v.s. opålitliga) personer att komma åt databasen, vill vi vara säkra på att standardåtkomster är begränsade. En viktig åtgärd är att återkalla privilegiet för att skapa standardobjekt för offentliga scheman för att mildra en nyligen publicerad PostgreSQL-sårbarhet relaterad till standardschemaprivilegier (jfr. Locking Down the Public Schema by yours truly).
En exempeldatabas
Vi börjar med en tom exempeldatabas i illustrationssyfte:
create database sampledb;
\connect sampledb
revoke create on schema public from public;
alter default privileges revoke all privileges on tables from public;
Vi skapar också den anonyma inloggningsrollen som motsvarar den tidigare inställningen pg_hba.conf.
create role anonymous login
nosuperuser
noinherit
nocreatedb
nocreaterole
Noreplication;
Och så gör vi något nytt genom att definiera en okonventionell syn:
create or replace view person as
select
null::name as login_name,
null::name as login_pass;
Den här vyn refererar till ingen tabell och därför returnerar en urvalsfråga alltid en tom rad:
select * from person;
login_name | login_pass
------------+-------------
|
(1 row)
En sak detta gör för oss är på sätt och vis att tillhandahålla dokumentation eller en ledtråd till slutanvändare om vilken data som krävs för att upprätta ett konto. Det vill säga, genom att fråga tabellen, även om resultatet är en tom rad, avslöjar resultatet namnen på de två dataelementen.
Men ännu bättre, förekomsten av denna vy tillåter bestämning av de datatyper som krävs:
\d person
View "public.person"
Column | Type | Modifiers
--------------+------+-----------
login_name | name |
login_pass | name |
Vi kommer att implementera funktionen för tillhandahållande av autentiseringsuppgifter med en lagrad funktion och utlösare, så låt oss deklarera en tom funktionsmall och den tillhörande utlösaren:
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();
Observera att vi följer den föreslagna namnkonventionen från föregående artikel och använder det associerade tabellnamnet suffixat med en förkortning som anger attribut för triggerrelationen mellan tabellen och den lagrade funktionen för en INSTEAD OF INSERT-utlösare (d.v.s. suffixet " jag det"). Vi har också lagt till attributen SCHEMA och SECURITY DEFINER till den lagrade funktionen:det förra eftersom det är god praxis att ställa in sökvägen som gäller för varaktigheten av funktionskörning, och det senare för att underlätta rollskapandet, vilket normalt är en databas superanvändarbehörighet endast men i detta fall kommer att delegeras till anonyma användare.
Och slutligen lägger vi till minimalt tillräckliga behörigheter för vyn för att fråga och infoga:
grant select, insert on table person to anonymous;
Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda Whitepaper Låt oss granska
Innan vi implementerar den lagrade funktionskoden, låt oss se över vad vi har. Först finns det exempeldatabasen som ägs av postgres-användaren:
\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 | {}
Och det är vyn vi skapade och en lista över skapande och läsrättigheter som beviljats den anonyma användaren av postgres-användaren:
\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)
Slutligen visar tabelldetaljen kolumnnamnen och datatyperna samt tillhörande utlösare:
\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 kommer att använda dynamisk SQL, d.v.s. konstruera den slutliga formen av en DDL-sats vid körning, delvis från användarinmatade data, för att fylla i triggerfunktionskroppen. Specifikt hårdkodar vi konturerna av uttalandet för att skapa en ny inloggningsroll och fylla i de specifika parametrarna som variabler.
Den allmänna formen för detta kommando är
create role name [ [ with ] option [ ... ] ]
där alternativ kan vara vilken som helst av sexton specifika egenskaper. I allmänhet är standardinställningarna lämpliga men vi kommer att vara tydliga om flera begränsningsalternativ och använda formuläret
create role name
with
login
inherit
nosuperuser
nocreatedb
nocreaterole
password ‘password’;
där vi kommer att infoga det användarspecificerade rollnamnet och lösenordet vid körning.
Dynamiskt konstruerade satser anropas med kommandot execute:
execute command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];
som för våra specifika behov skulle se ut
execute 'create role '
|| new.login_name
|| ' with login inherit nosuperuser nocreatedb nocreaterole password '
|| quote_literal(new.login_pass);
där funktionen quote_literal returnerar strängargumentet som är lämpligt citerat för att användas som en strängliteral för att uppfylla det syntaktiska kravet att lösenordet faktiskt ska citeras.
När vi har byggt kommandosträngen, tillhandahåller vi den som argument till pl/pgsql exekveringskommandot i triggerfunktionen.
Att sätta ihop det hela ser ut så här:
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;
$$;
Låt oss prova!
Allt är på plats, så låt oss snurra! Först byter vi sessionsauktorisering till den anonyma användaren och gör sedan en infogning mot personvyn:
set session authorization anonymous;
insert into person values ('alice', '1234');
Resultatet är att ny användare alice har lagts till i systemtabellen:
\du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
alice | | {}
anonymous | No inheritance | {}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
Det fungerar till och med direkt från operativsystemets kommandorad genom att skicka en SQL-kommandosträng till psql-klientverktyget för att lägga till användaren 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 | {}
Applicera lite rustning
Det initiala exemplet på triggerfunktionen är sårbart för SQL-injektionsattacker, det vill säga en skadlig hotaktör kan skapa indata som resulterar i obehörig åtkomst. Till exempel, när ett försök att göra något utanför räckvidden misslyckas som den anonyma användarrollen:
set session authorization anonymous;
drop user alice;
ERROR: permission denied to drop role
Men följande skadliga indata skapar en superanvändarroll som heter 'eve' (liksom ett lockbetekonto som heter '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 | {}
Sedan kan den smygande superanvändarrollen användas för att skapa kaos i databasen, till exempel att ta bort användarkonton (eller ännu 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 | {}
För att mildra denna sårbarhet måste vi vidta åtgärder för att sanera indata. Till exempel att tillämpa funktionen quote_ident, som returnerar en sträng som är lämpligt citerad för användning som en identifierare i en SQL-sats med citattecken som läggs till när det behövs, till exempel om strängen innehåller icke-identifierande tecken eller skulle vara skiftlägesvikt och korrekt dubblering inbäddad citat:
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;
$$;
Om nu samma SQL-injektion utnyttjas för att skapa en annan superanvändare som heter "frank", misslyckas det, och resultatet är ett mycket oortodoxt användarnamn:
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 tillämpa ytterligare förnuftig datavalidering inom triggerfunktionen, som att endast kräva alfanumeriska användarnamn och avvisa blanksteg och andra tecken:
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;
$$;
och bekräfta sedan att de olika saneringskontrollerna fungerar:
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.
Låt oss öka ett snäpp
Anta att vi vill lagra ytterligare metadata eller applikationsdata relaterat till den skapade användarrollen, t.ex. kanske en tidsstämpel och källans IP-adress som är kopplad till rollskapandet. Utsikten kan naturligtvis inte tillfredsställa detta nya krav eftersom det inte finns någon underliggande lagring, så en faktisk tabell krävs. Låt oss också anta att vi vill begränsa synligheten för den tabellen från användare som loggar in med den anonyma inloggningsrollen. Vi kan gömma tabellen i ett separat namnutrymme (dvs ett PostgreSQL-schema) som förblir otillgängligt för anonyma användare. Låt oss kalla detta namnutrymme för det "privata" namnområdet och skapa tabellen i namnområdet:
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()
);
Ett enkelt ytterligare infogningskommando inuti triggerfunktionen registrerar denna associerade 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;
$$;
Och vi kan ge det ett enkelt test. Först bekräftar vi att medan den är ansluten som den anonyma rollen är endast public.person-vyn synlig och inte 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
Och sedan efter en ny roll infoga:
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 visar metadatainsamlingen för IP-adressen och tiden för radinsättning.
Slutsats
I den här artikeln har vi visat en teknik för att delegera postgreSQL-rolluppgifter till roller som inte är superanvändare. Även om exemplet helt delegerade legitimationsfunktionen till anonyma användare, kunde ett liknande tillvägagångssätt användas för att delvis delegera funktionen till endast betrodd personal samtidigt som man behåller fördelen med att avlasta detta arbete från värdefull databas- eller systemadministratörspersonal. Vi demonstrerade också en teknik för skiktad dataåtkomst med hjälp av PostgreSQL-scheman, selektivt exponera eller dölja databasobjekt. I nästa artikel i den här serien kommer vi att utvidga tekniken för åtkomst av data i lager för att föreslå en ny databasarkitekturdesign för tillämpningar.