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

Applikationsbrugere vs. Row Level Security

For et par dage siden har jeg blogget om de almindelige problemer med roller og privilegier, vi opdager under sikkerhedsgennemgange.

Selvfølgelig tilbyder PostgreSQL mange avancerede sikkerhedsrelaterede funktioner, en af ​​dem er Row Level Security (RLS), tilgængelig siden PostgreSQL 9.5.

Da 9.5 blev udgivet i januar 2016 (altså for bare et par måneder siden), er RLS en ret ny funktion, og vi har ikke rigtig at gøre med mange produktionsinstallationer endnu. I stedet er RLS et almindeligt emne for "hvordan man implementerer" diskussioner, og et af de mest almindelige spørgsmål er, hvordan man får det til at fungere med brugere på applikationsniveau. Så lad os se, hvilke mulige løsninger der er.

Introduktion til RLS

Lad os først se et meget simpelt eksempel, der forklarer, hvad RLS handler om. Lad os sige, at vi har en chat tabel, der gemmer beskeder sendt mellem brugere – brugerne kan indsætte rækker i den for at sende beskeder til andre brugere og forespørge på den for at se beskeder sendt til dem af andre brugere. Så tabellen kan se sådan ud:

CREATE TABLE chat (
    message_uuid    UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    message_time    TIMESTAMP NOT NULL DEFAULT now(),
    message_from    NAME      NOT NULL DEFAULT current_user,
    message_to      NAME      NOT NULL,
    message_subject VARCHAR(64) NOT NULL,
    message_body    TEXT
);

Den klassiske rollebaserede sikkerhed giver os kun mulighed for at begrænse adgangen til enten hele tabellen eller lodrette udsnit af den (kolonner). Så vi kan ikke bruge det til at forhindre brugere i at læse beskeder beregnet til andre brugere eller sende beskeder med en falsk message_from felt.

Og det er præcis, hvad RLS er til - det giver dig mulighed for at oprette regler (politikker), der begrænser adgangen til undersæt af rækker. Så du kan for eksempel gøre dette:

CREATE POLICY chat_policy ON chat
    USING ((message_to = current_user) OR (message_from = current_user))
    WITH CHECK (message_from = current_user)

Denne politik sikrer, at en bruger kun kan se beskeder sendt af ham eller beregnet til ham – det er betingelsen i USING klausul gør. Den anden del af politikken (WITH CHECK ) forsikrer, at en bruger kun kan indsætte beskeder med sit brugernavn i message_from kolonne, der forhindrer beskeder med forfalsket afsender.

Du kan også forestille dig RLS som en automatisk måde at tilføje yderligere WHERE-betingelser. Du kunne gøre det manuelt på applikationsniveau (og før RLS gjorde folk det ofte), men RLS gør det på en pålidelig og sikker måde (f.eks. blev der lagt en stor indsats i at forhindre forskellige informationslækager).

Bemærk :Før RLS var en populær måde at opnå noget lignende på at gøre tabellen utilgængelig direkte (tilbagekalde alle privilegier) og give et sæt sikkerhedsdefineringsfunktioner for at få adgang til det. Det opnåede stort set det samme mål, men funktioner har forskellige ulemper - de har en tendens til at forvirre optimeringsværktøjet og i høj grad begrænse fleksibiliteten (hvis brugeren skal gøre noget, og der ikke er nogen passende funktion til det, er han uheldig). Og selvfølgelig skal du skrive disse funktioner.

Applikationsbrugere

Hvis du læser den officielle dokumentation om RLS, vil du muligvis bemærke en detalje – alle eksemplerne bruger current_user , dvs. den aktuelle databasebruger. Men det er ikke sådan de fleste databaseapplikationer fungerer i disse dage. Webapplikationer med mange registrerede brugere opretholder ikke 1:1-tilknytning til databasebrugere, men bruger i stedet en enkelt databasebruger til at køre forespørgsler og administrere applikationsbrugere på egen hånd – måske i en users tabel.

Teknisk set er det ikke et problem at oprette mange databasebrugere i PostgreSQL. Databasen burde håndtere det uden problemer, men applikationer gør det ikke af en række praktiske årsager. For eksempel skal de spore yderligere oplysninger for hver bruger (f.eks. afdeling, stilling i organisationen, kontaktoplysninger, …), så applikationen skal bruge users bord alligevel.

En anden årsag kan være forbindelsespooling – brug af en enkelt delt brugerkonto, selvom vi ved, at det kan løses ved hjælp af arv og SET ROLE (se det forrige indlæg).

Men lad os antage, at du ikke vil oprette separate databasebrugere - du vil fortsætte med at bruge en enkelt delt databasekonto og bruge RLS med applikationsbrugere. Hvordan gør man det?

Sessionsvariable

Det, vi har brug for, er at videregive yderligere kontekst til databasesessionen, så vi senere kan bruge den fra sikkerhedspolitikken (i stedet for current_user variabel). Og den nemmeste måde at gøre det på i PostgreSQL er sessionsvariabler:

SET my.username = 'tomas'

Hvis dette minder om de sædvanlige konfigurationsparametre (f.eks. SET work_mem = '...' ), du har fuldstændig ret - det er for det meste det samme. Kommandoen definerer et nyt navneområde (my ), og tilføjer et username variabel ind i det. Det nye navneområde er påkrævet, da det globale er reserveret til serverkonfigurationen, og vi kan ikke tilføje nye variabler til det. Dette giver os mulighed for at ændre sikkerhedspolitikken på denne måde:

CREATE POLICY chat_policy ON chat
    USING (current_setting('my.username') IN (message_from, message_to))
    WITH CHECK (message_from = current_setting('my.username'))

Alt, hvad vi skal gøre, er at sikre, at forbindelsespuljen/applikationen angiver brugernavnet, hver gang den får en ny forbindelse og tildeler det til brugeropgaven.

Lad mig påpege, at denne tilgang kollapser, når du tillader brugerne at køre vilkårlig SQL på forbindelsen, eller hvis brugeren formår at opdage en passende SQL-injektionssårbarhed. I så fald er der intet, der kunne forhindre dem i at indstille et vilkårligt brugernavn. Men fortvivl ikke, der er en masse løsninger på det problem, og vi vil hurtigt gennemgå dem.

Underskrevne sessionsvariable

Den første løsning er en simpel forbedring af sessionsvariablerne - vi kan ikke rigtig forhindre brugerne i at indstille vilkårlig værdi, men hvad nu hvis vi kunne verificere, at værdien ikke blev undergravet? Det er ret nemt at gøre ved at bruge en simpel digital signatur. I stedet for blot at gemme brugernavnet, kan den betroede del (forbindelsespulje, applikation) gøre noget som dette:

signature = sha256(username + timestamp + SECRET)

og gem derefter både værdien og signaturen i sessionsvariablen:

SET my.username = 'username:timestamp:signature'

Forudsat at brugeren ikke kender den SECRET-streng (f.eks. 128B af tilfældige data), burde det ikke være muligt at ændre værdien uden at ugyldiggøre signaturen.

Bemærk :Dette er ikke en ny idé – det er i bund og grund det samme som signerede HTTP-cookies. Django har en ganske fin dokumentation om det.

Den nemmeste måde at beskytte SECRET-værdien på er ved at gemme den i en tabel, der er utilgængelig for brugeren, og ved at angive en security definer funktion, der kræver en adgangskode (så brugeren ikke blot kan underskrive vilkårlige værdier).

CREATE FUNCTION set_username(uname TEXT, pwd TEXT) RETURNS text AS $
DECLARE
    v_key   TEXT;
    v_value TEXT;
BEGIN
    SELECT sign_key INTO v_key FROM secrets;
    v_value := uname || ':' || extract(epoch from now())::int;
    v_value := v_value || ':' || crypt(v_value || ':' || v_key,
                                       gen_salt('bf'));
    PERFORM set_config('my.username', v_value, false);
    RETURN v_value;
END;
$ LANGUAGE plpgsql SECURITY DEFINER STABLE;

Funktionen slår simpelthen signaturnøglen (hemmeligheden) op i en tabel, beregner signaturen og sætter derefter værdien ind i sessionsvariablen. Det returnerer også værdien, mest for nemheds skyld.

Så den betroede del kan gøre dette lige før han overdrager forbindelsen til brugeren (naturligvis er 'passphrase' ikke en særlig god adgangskode til produktion):

SELECT set_username('tomas', 'passphrase')

Og så har vi selvfølgelig brug for en anden funktion, der blot verificerer signaturen og enten fejler eller returnerer brugernavnet, hvis signaturen matcher.

CREATE FUNCTION get_username() RETURNS text AS $
DECLARE
    v_key   TEXT;
    v_parts TEXT[];
    v_uname TEXT;
    v_value TEXT;
    v_timestamp INT;
    v_signature TEXT;
BEGIN

    -- no password verification this time
    SELECT sign_key INTO v_key FROM secrets;

    v_parts := regexp_split_to_array(current_setting('my.username', true), ':');
    v_uname := v_parts[1];
    v_timestamp := v_parts[2];
    v_signature := v_parts[3];

    v_value := v_uname || ':' || v_timestamp || ':' || v_key;
    IF v_signature = crypt(v_value, v_signature) THEN
        RETURN v_uname;
    END IF;

    RAISE EXCEPTION 'invalid username / timestamp';
END;
$ LANGUAGE plpgsql SECURITY DEFINER STABLE;

Og da denne funktion ikke har brug for adgangssætningen, kan brugeren blot gøre dette:

SELECT get_username()

Men get_username() funktion er beregnet til sikkerhedspolitikker, f.eks. sådan her:

CREATE POLICY chat_policy ON chat
    USING (get_username() IN (message_from, message_to))
    WITH CHECK (message_from = get_username())

Et mere komplet eksempel, pakket som en simpel udvidelse, kan findes her.

Bemærk, at alle objekter (tabel og funktioner) ejes af en privilegeret bruger, ikke brugeren, der har adgang til databasen. Brugeren har kun EXECUTE privilegium på funktionerne, der dog er defineret som SECURITY DEFINER . Det er det, der får denne ordning til at fungere, mens den beskytter hemmeligheden fra brugeren. Funktionerne er defineret som STABLE , for at begrænse antallet af opkald til crypt() funktion (som med vilje er dyr for at forhindre bruteforcing).

Eksempelfunktionerne har bestemt brug for mere arbejde. Men forhåbentlig er det godt nok til et proof of concept, der viser, hvordan man gemmer yderligere kontekst i en beskyttet sessionsvariabel.

Hvad skal rettes spørger du? For det første håndterer funktionerne ikke forskellige fejltilstande særlig pænt. For det andet, mens den signerede værdi inkluderer et tidsstempel, gør vi ikke rigtig noget med det - det kan f.eks. bruges til at udløbe værdien. Det er muligt at tilføje yderligere bits i værdien, f.eks. en afdeling af brugeren, eller endda information om sessionen (f.eks. PID af backend-processen for at forhindre genbrug af den samme værdi på andre forbindelser).

Krypto

De to funktioner er afhængige af kryptografi - vi bruger ikke meget undtagen nogle simple hashing-funktioner, men det er stadig et simpelt kryptoskema. Og alle ved, at du ikke bør lave din egen krypto. Derfor brugte jeg pgcrypto-udvidelsen, især crypt() funktion for at omgå dette problem. Men jeg er ikke en kryptograf, så selvom jeg mener, at hele ordningen er i orden, mangler jeg måske noget - lad mig vide, hvis du opdager noget.

Signeringen ville også være et godt match til offentlig nøglekryptering – vi kunne bruge en almindelig PGP-nøgle med en adgangssætning til signeringen og den offentlige del til signaturverifikation. Selvom pgcrypto understøtter PGP til kryptering, understøtter det desværre ikke signeringen.

Alternative tilgange

Der er selvfølgelig forskellige alternative løsninger. For eksempel i stedet for at gemme signeringshemmeligheden i en tabel, kan du hårdkode den ind i funktionen (men så skal du sikre dig, at brugeren ikke kan se kildekoden). Eller du kan lave signeringen i en C-funktion, i hvilket tilfælde den er skjult for alle, der ikke har adgang til hukommelsen (i så fald mistede du alligevel).

Hvis du slet ikke kan lide signeringsmetoden, kan du erstatte den signerede variabel med en mere traditionel "hvælving"-løsning. Vi har brug for en måde at gemme dataene på, men vi skal sikre os, at brugeren ikke kan se eller ændre indholdet vilkårligt, undtagen på en defineret måde. Men hey, det er hvad almindelige tabeller med en API implementeret ved hjælp af security definer funktioner kan gøre!

Jeg vil ikke præsentere hele det omarbejdede eksempel her (tjek denne udvidelse for et komplet eksempel), men hvad vi har brug for er en sessions bord, der fungerer som hvælvingen:

CREATE TABLE sessions (
    session_id    UUID PRIMARY KEY,
    session_user  NAME NOT NULL
)

Tabellen må ikke være tilgængelig for almindelige databasebrugere – en simpel REVOKE ALL FROM ... skal tage sig af det. Og så en API bestående af to hovedfunktioner:

  • set_username(user_name, passphrase) – genererer et tilfældigt UUID, indsætter data i boksen og gemmer UUID'et i en sessionsvariabel
  • get_username() – læser UUID fra en sessionsvariabel og slår rækken op i tabellen (fejl, hvis der ikke er en matchende række)

Denne tilgang erstatter signaturbeskyttelsen med tilfældigheden af ​​UUID'et – brugeren kan justere sessionsvariablen, men sandsynligheden for at ramme et eksisterende ID er ubetydelig (UUID'er er 128-bit tilfældige værdier).

Det er en lidt mere traditionel tilgang, der er afhængig af traditionel rollebaseret sikkerhed, men den har også et par ulemper - for eksempel skriver den faktisk databaser, hvilket betyder, at den i sagens natur er inkompatibel med hot standby-systemer.

Slip af med adgangssætningen

Det er også muligt at designe hvælvingen, så adgangssætningen ikke er nødvendig. Vi har introduceret det, fordi vi antog set_username sker på den samme forbindelse – vi skal beholde funktionen eksekverbar (så at rode med roller eller privilegier er ikke en løsning), og adgangssætningen sikrer, at kun den betroede komponent faktisk kan bruge den.

Men hvad hvis signeringen/sessionsoprettelse sker på en separat forbindelse, og kun resultatet (signeret værdi eller sessions-UUID) kopieres ind i forbindelsen, der er givet til brugeren? Nå, så har vi ikke brug for adgangssætningen mere. (Det minder lidt om, hvad Kerberos gør – genererer en billet på en pålidelig forbindelse, og brug derefter billetten til andre tjenester.)

Oversigt

Så lad mig hurtigt opsummere dette blogindlæg:

  • Mens alle RLS-eksemplerne bruger databasebrugere (ved hjælp af current_user ), er det ikke særlig svært at få RLS til at fungere med applikationsbrugere.
  • Sessionsvariabler er en pålidelig og ret simpel løsning, forudsat at systemet har en pålidelig komponent, der kan indstille variablen, før forbindelsen videregives til en bruger.
  • Når brugeren kan udføre vilkårlig SQL (enten ved design eller takket være en sårbarhed), forhindrer en signeret variabel brugeren i at ændre værdien.
  • Andre løsninger er mulige, f.eks. erstatte sessionsvariablerne med tabel, der gemmer information om sessioner identificeret ved tilfældig UUID.
  • En god ting er, at sessionsvariablerne ikke skriver databaser, så denne tilgang kan fungere på skrivebeskyttede systemer (f.eks. hot standby).

I den næste del af denne blogserie vil vi se på at bruge applikationsbrugere, når systemet ikke har en pålidelig komponent (så det ikke kan indstille sessionsvariablen eller oprette en række i sessions tabel), eller når vi ønsker at udføre (yderligere) brugerdefineret godkendelse i databasen.


  1. Kan ikke bruge et CONTAINS eller FREETEXT prædikat på tabel eller indekseret visning, fordi det ikke er fuldtekstindekseret

  2. Find ud af, om en værdi indeholder mindst ét ​​numerisk ciffer i Oracle

  3. SQL Server Transaction Log — Del 2

  4. Saml kolonner med yderligere (særskilte) filtre