Datenbank-Sicherheit

Datenbank-Berechtigungen

Das PostgreSQL-Login per Superuser postgres sollte immer nur über Unix-Domain-Sockets und über localhost erlaubt sein. Der Zugang mit Peer-Authentifizierung in der pg_hba.conf-Datei kann hingegen gewährt werden:

# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             postgres                                peer
host    all             all             10.23.42.1/24           scram-sha-256

Die Datenbank sollte vom Datenbankadministrator angelegt und anschließend so konfiguriert werden, dass sich nicht jeder (PUBLIC) damit verbinden kann:

CREATE DATABASE myapp;
REVOKE ALL ON myapp FROM PUBLIC;

Damit kann sich nur noch der Superuser mit der Datenbank myapp verbinden.

Passwörter speichern

Passwörter sollten niemals im Klartext, also z.B. auch nicht in einer .env-Datei gespeichert werden. Beim Speichern und Übermitteln von Passwörtern sollte dies immer mit Salts versehen sein. Für PostgreSQL gibt es hierfür die Erweiterung pgcrypto, die einfach aktiviert werden kann mit

CREATE EXTENSION pgcrypto;

Daher sollten bereits beim Anlegen sichere Passwörter vergeben werden, die anschließend z.B. in Vault o.ä. gespeichert werden:

CREATE ROLE myapp_users;
CREATE ROLE myapp_reader IN ROLE myapp_users LOGIN PASSWORD '…';
CREATE ROLE myapp_writer IN ROLE myapp_users LOGIN PASSWORD '…';

Anschließend erhalten dann User mit der Rolle myapp_users zunächst CONNECT-Rechte und dann myapp_reader Leserechte und myapp_writer Schreibrechte:

GRANT CONNECT ON DATABASE to myapp_users;
GRANT SELECT ON diagnosis_key TO myapp_reader;
GRANT INSERT ON diagnosis_key TO myapp_writer;

Der User myapp_reader kann damit jedoch alle Daten auf einmal lesen. Auch dies ist ein Angriffspunkt, der besser durch eine Funktion beschnitten wird:

CREATE OR REPLACE FUNCTION get_key_data(in_id UUID)
    RETURNS JSONB
    AS 'SELECT key_data FROM diagnosis_key WHERE id = in_id;'
    LANGUAGE sql SECURITY DEFINER SET search_path = :schema, pg_temp;

Anschließend wird die Funktion myapp_owner zugewiesen, myapp_reader und myapp_writer die Berechtigungen entzogen und schließlich die Ausführung der Funktion myapp_reader erlaubt:

ALTER FUNCTION get_key_data(UUID) OWNER TO myapp_owner;
REVOKE ALL ON FUNCTION get_key_dataUUID) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_key_data(UUID) TO myapp_reader;

Damit kann myapp_reader also nur noch einen einzelnen Datensatz lesen.

id

Die id sollte nicht als serial, bigserial o.ä. realisiert werden. Hochzählende Zahlen könnten von Angreifern leicht erraten werden. Daher ist der UUIDv4-Datentyp deutlich besser geeignet. In PostgreSQL könnt ihr UUIDv4 generieren mit der uuid-ossp-Erweiterung oder für PostgreSQL≥9.4 auch der pgcrypto-Erweiterung:

CREATE EXTENSION "uuid-ossp";
CREATE TABLE diagnosis_key (
  id uuid primary key default uuid_generate_v4() NOT NULL,
  …
);

oder

CREATE EXTENSION "pgcrypto";
CREATE TABLE diagnosis_key (
  id uuid primary key default gen_random_uuid() NOT NULL,
  …
);

Zeitstempel

Gelegentlich werden Datum und Zeit als bigint, also als Zahl, gespeichert, und dies obwohl es auch einen TIMESTAMP-Datentyp gibt. Dies hätte den Vorteil, dass dann auch einfach mit ihnen gerechnet werden kann, also z.B.:

SELECT age(submission_timestamp);
SELECT submission_timestamp - '1 day'::interval;

Außerdem könnten die Daten nach einer bestimmten Zeit gelöscht werden, z.B. nach dreißig Tagen mit:

DELETE FROM diagnosis_key WHERE age(submission_timestamp) > 30;

Das Löschen kann noch beschleunigt werden, wenn für jeden Tag mit der PostgreSQL-Erweiterung pg_partman eine eigene Partition erstellt wird.