PostgreSQL - von Haus aus unsicher?

Gespeichert von bluthg am So., 03.11.2019 - 19:45

 

Im PostgreSQL Telegram-Channel (https://t.me/pg_sql) kam kürzlich die Frage auf, warum in jeder neuen Datenbank das Schema "public" der Rolle "postgres" gehört und sie für jeden Benutzer beschreibbar ist.

Das Problem

Was meine ich damit? Hier ist eine neu angelegte Datenbank "blog", die mir ("bluthg") gehört:

bluthg@[local] blog # \l+ blog
                                                List of databases
┌──────┬────────┬──────────┬─────────────┬─────────────┬───────────────────┬─────────┬────────────┬─────────────┐
│ Name │ Owner  │ Encoding │   Collate   │    Ctype    │ Access privileges │  Size   │ Tablespace │ Description │
├──────┼────────┼──────────┼─────────────┼─────────────┼───────────────────┼─────────┼────────────┼─────────────┤
│ blog │ bluthg │ UTF8     │ en_US.UTF-8 │ en_US.UTF-8 │                   │ 7759 kB │ pg_default │             │
└──────┴────────┴──────────┴─────────────┴─────────────┴───────────────────┴─────────┴────────────┴─────────────┘
(1 row)

Sie gehört zwar mir ("bluthg"), es sind aber keinerlei Zugriffsrechte gesetzt ("Access privileges" ist leer, s. https://www.postgresql.org/docs/current/ddl-priv.html).

Die neue DB enthält genau ein Schema ("namespace", daher "\dn"), nämlich "public":

bluthg@[local] blog # \dn+
                           List of schemas
┌────────┬──────────┬──────────────────────┬────────────────────────┐
│  Name  │  Owner   │  Access privileges   │      Description       │
├────────┼──────────┼──────────────────────┼────────────────────────┤
│ public │ postgres │ postgres=UC/postgres↵│ standard public schema │
│        │          │ =UC/postgres         │                        │
└────────┴──────────┴──────────────────────┴────────────────────────┘
(1 row)

Warum gehört dieses Schema "postgres" und nicht "bluthg"? Und warum zeigt die Spalte "Access privileges" volle Rechte für "public"?

Wenn ich ein neues Benutzerkonto anlege

bluthg@[local] blog # CREATE ROLE blog LOGIN;
Time: 39.651 ms
bluthg@[local] blog # \du+ blog
                   List of roles
┌───────────┬────────────┬───────────┬─────────────┐
│ Role name │ Attributes │ Member of │ Description │
├───────────┼────────────┼───────────┼─────────────┤
│ blog      │            │ {}        │             │
└───────────┴────────────┴───────────┴─────────────┘

bluthg@[local] blog #

und mich damit anmelde, darf ich mich tatsächlich mit dieser Datenbank verbinden und sogar Objekte anlegen:

:~$ psql blog -h localhost -U blog
Password for user blog:
blog@localhost blog # CREATE TABLE blogdemo (darfichdas INT);
Time: 46.047 ms
blog@localhost blog #

How did we get here?

Ich erinnere mich noch vage an meine ersten Gehversuche mit PostgreSQL. Damals kam MySQL (3.2x) bei jeder Linux-Distribution quasi automatisch mit auf die Festplatte, spätestens wenn man bei der Installation "LAMP" oder "DB-Server" mit angewählt hatte. Und MySQL hat dann direkt eine Datenbank angelegt und nach einem "root"-Passwort gefragt (oder aber auch einfach den UNIX-"root" ohne Passwort hineingelassen). Damit hatte man dann direkt was zum "herumspielen" und konnte sich an SQL versuchen.

Dann hatte ich irgendwann mal Lust auf Transaktionen und wollte "diese andere" Open-Source-Datenbank ausprobieren. Was war das für ein K(r)ampf!

Paket installieren (vielleicht war es sogar ein tar-Archiv?), dann einen UNIX-User anlegen, dann rausfinden, warum das nicht startet. Ok, also, "initdb" muss ausgeführt werden, um einen neuen Cluster (ui, ein Cluster?!? Ich wollte doch nur eine einfache Datenbank?!?) anzulegen! Dann eine Datei namens "pg_hba.conf" bearbeiten. Dazwischen zahllose Fehlversuche, sich mit dieser DB zu verbinden...

(Oracle(tm) war natürlich noch viel schlimmer... ich sage nur TNSNAMES.ORA...)

Das sieht heutzutage entschieden anders aus. Aktuelle Pakete (.deb/.rpm) nehmen einem diese ganze Arbeit ab. Die pg_hba.conf erlaubt auch direkt "peer"-Authentifizierung auf dem UNIX-Socket (bei Debian/Ubuntu, andere mögen abweichen), so das "root" nur zu "postgres" werden muss und losarbeiten kann.Die Paketierer haben ganze Arbeit geleistet. Mittlerweile ist die Inbetriebnahme einer PostgreSQL-Instanz etwa so komfortabel wie bei MySQL.

Aber: Komfort bedeutet nun einmal auch, dass dem (Neu-)Anwender so wenige Hindernisse wie unbedingt nötig in den Weg gelegt werden. Der Anfänger (nennen wir sie "Alice") wird irgendwann (hoffentlich bald!) lernen, dass man nicht mit dem SUPERUSER in der Datenbank herumhantieren sollte (etwas, was man bei dem weiter oben erwähnten Mitbewerber auch heute noch oft genug sieht...).

Was hat Alice bis hierher gemacht? Wahrscheinlich hat sie eine erste Datenbank angelegt:

postgres=# CREATE DATABASE meinedb;
CREATE DATABASE
Time: 744.782 ms
postgres=#

Jetzt lernt sie das Rollenmodell kennen und legt sich einen eigenen Account (mit Passwort) an:

postgres=# CREATE ROLE alice LOGIN;
CREATE ROLE
Time: 61.743 ms
postgres=# \password alice
Enter new password:
Enter it again:
postgres=#

und verbindet sich mit der neuen Datenbank "meinedb". Wenn diese Datenbank jetzt konsequent abgesichert wäre, könnte "alice" sich gar nicht verbinden! Es ist Alice ja schon hoch anzurechnen, dass sie den Datenbanknamen angegeben hat, denn diese Meldung hätte Bob wahrscheinlich schwer verwirrt:

alice:~$ psql
psql: error: could not connect to server: FATAL:  database "alice" does not exist
alice:~$

Aber Alice ist konsequent:

alice:~$ psql meinedb
psql (12.0 (Ubuntu 12.0-1.pgdg18.04+1), server 10.10 (Ubuntu 10.10-1.pgdg18.04+1))
Type "help" for help.

meinedb=>

In dieser neuen Datenbank kann Alice (so ziemlich) alles machen (obwohl sie doch "postgres" gehört). Bob - wenn er dann mal rausfindet, wie man den Datenbanknamen angibt - allerdings auch!

Das ist der Komfort, der als Zugeständnis vor allem an neue User vorgegeben ist.

Für Produktionssysteme ist das aber natürlich nicht geeignet!!!

 

How to fix it?

Einige einfache Schritte machen eine PostgreSQL-Installation (weitgehend) sicher:

1) "postgres"-Datenbank absichern:

REVOKE ALL ON DATABASE postgres FROM public;

Alternativ die Datenbank einfach löschen; sie ist letzlich nur aus Komfortgründen vorhanden!

2) Für jede neue Datenbank eine "owner"-Rolle anlegen:

CREATE ROLE blog_owner NOLOGIN;

3) Neue Datenbank anlegen und der neuen Rolle zuweisen:

CREATE DATABASE blog OWNER blog_owner;

4) Als Superuser mit der neuen DB verbinden und erst einmal "zu" machen:

REVOKE ALL ON DATABASE blog FROM public;

5) Wo wir schon da sind, auch gleich das Schema "public" der Owner-Rolle zuweisen und "zu" machen:

ALTER SCHEMA public OWNER TO blog_owner;
REVOKE ALL ON SCHEMA public FROM public;

6) Jetzt die eigentliche Applikations-Rolle anlegen:

CREATE ROLE blog LOGIN;

7) Und die benötigten Rechte vergeben:

GRANT CONNECT ON DATABASE blog TO blog;
GRANT USAGE ON SCHEMA public TO blog;

Hinweis: das Schema "public" zu benutzen ist zwar nicht 100% "reine Lehre", aber durchaus in Ordnung (sonst muss der search_path der Rolle angepasst oder generell voll qualifizierte Relationsnamen genutzt werden).

Ergebnis:

blog=# \l+ blog
                                                     List of databases
 Name |   Owner    | Encoding |   Collate   |    Ctype    |     Access privileges     |  Size   | Tablespace | Description
------+------------+----------+-------------+-------------+---------------------------+---------+------------+-------------
 blog | blog_owner | UTF8     | en_US.UTF-8 | en_US.UTF-8 | blog_owner=CTc/blog_owner+| 7759 kB | pg_default |
      |            |          |             |             | blog=c/blog_owner         |         |            |
(1 row)

blog=# \dn+
                             List of schemas
  Name  |   Owner    |    Access privileges     |      Description
--------+------------+--------------------------+------------------------
 public | blog_owner | blog_owner=UC/blog_owner+| standard public schema
        |            | blog=U/blog_owner        |
(1 row)

blog=#

Arbeiten mit der DB

"blog" darf in dieser Datenbank nichts machen, außer sich verbinden und "schauen" (USAGE auf dem Schema).

Objekte (Tabellen, Views, Sequenzen, ...) muss von "blog_owner" angelegt werden und kann anschließend auch nur von dieser Rolle gelesen werden:

blog=# SET ROLE blog_owner ;
SET
Time: 0.264 ms
blog=> CREATE TABLE nurichdarf (das INT);
CREATE TABLE
Time: 65.134 ms

blog=> \dp+ nurichdarf
                               Access privileges
 Schema |    Name    | Type  | Access privileges | Column privileges | Policies
--------+------------+-------+-------------------+-------------------+----------
 public | nurichdarf | table |                   |                   |
(1 row)
blog=> SET ROLE blog;
SET
Time: 0.221 ms
blog=> SELECT * FROM nurichdarf ;
ERROR:  permission denied for relation nurichdarf
Time: 0.514 ms
blog=>

Also müssen explizite GRANTs her:

blog=> SET ROLE blog_owner;
SET
Time: 0.196 ms
blog=> GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE nurichdarf TO blog;
GRANT
Time: 61.688 ms
blog=> SET ROLE blog;
SET
Time: 0.227 ms
blog=> INSERT INTO nurichdarf (das) VALUES (1);
INSERT 0 1
Time: 61.876 ms
blog=> SELECT * FROM nurichdarf ;
 das
-----
   1
(1 row)

Time: 0.316 ms
blog=>
blog=> TRUNCATE nurichdarf ;
ERROR:  permission denied for relation nurichdarf
Time: 0.248 ms
blog=>
blog=> DELETE FROM nurichdarf WHERE das = 1;
DELETE 1
Time: 43.389 ms
blog=>

Um diese GRANTs nicht jedes Mal explizit machen zu müssen (was aber durchaus gängige und sinnvolle Praxis ist!), kann der Eigentümer (oder ein Superuser) sog. DEFAULT PRIVILEGES vergeben:

blog=> ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT,INSERT,UPDATE,DELETE ON TABLES TO blog;
ALTER DEFAULT PRIVILEGES
Time: 53.569 ms
blog=> ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT,USAGE ON SEQUENCES TO blog;
ALTER DEFAULT PRIVILEGES
Time: 47.475 ms
blog=>

Check, ob und wie das greift:

blog=> CREATE TABLE dudarfst (auch SERIAL);
CREATE TABLE
Time: 63.338 ms
blog=> \dp dudarfst
                                    Access privileges
 Schema |   Name   | Type  |       Access privileges       | Column privileges | Policies
--------+----------+-------+-------------------------------+-------------------+----------
 public | dudarfst | table | blog=arwd/blog_owner         +|                   |
        |          |       | blog_owner=arwdDxt/blog_owner |                   |
(1 row)

blog=> SET ROLE blog;
SET
Time: 0.245 ms
blog=> INSERT INTO dudarfst (auch) VALUES (default);
INSERT 0 1
Time: 37.123 ms
blog=> TABLE dudarfst;
 auch
------
    1
(1 row)

Time: 0.285 ms
blog=>

Neuen Kommentar hinzufügen