Database objects and the Principle of Least Privilege
See original GitHub issueCurrent Implementation
Currently, the database is used with a PostgreSQL superuser, which has full rights the the database cluster. The Table diagnosis_key is accessible to the “PUBLIC” pseudo user, so every other database user has full access too.
Suggested Enhancement
Following the Principle of Least Privilege, the application user(s) should have only the least possible privileges. e.g. It is not necessary to have the rights for DELETE, TRUNCATE, UPDATE or use DDL statements.
I describe my suggestion in more detail below. I am not fully aware of the application server design in the cwa-server, so maybe there are some differences in your current implementation. So, let’s assume, there are two main applications, application parts or micro services. Ons is for inserting, and one for reading. In this case, there should be two users:
- inserter
- reader
For the inserter, it should only be possible to insert values. This user should not have the right to read, delete or update something.
The reader should only be able to read, but not modify any data.
See below for examples and for two different approaches
e.g. see also: BSI IT-Grundschutz, APP.4.3 Relationale Datenbanksysteme
Expected Benefits
It should not be possible for an attacker to delete the database, read data without permission, create new database objects or perform a denial of service attack by creating millions of rows of random data, etc.
Details
PostgreSQL config
The cwa-server seems to use the postgres superuser. Usually connections by superusers should only be possible via unix domain sockets and via localhost. An entry with method “peer” in pg_hba.conf is OK:
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host all all 10.23.42.1/24 scram-sha-256
With this and when no password is set for the postgres superuser, connection is only possible via the postgres unix user and via local unix domain sockets. Example:
## As root (connecting to database postgres with user postgres):
# psql postgres postgres
psql: Fehler: konnte nicht mit Server verbinden: FATAL: Peer authentication failed for user "postgres"
# # as postgres user (connecting to database postgres with user postgres):
# su - postgres
> psql
psql (12.3)
Geben Sie »help« für Hilfe ein.
postgres=#
Basic variant with PostgreSQL Roles and permissions
A script may look like this:
-- New database
CREATE DATABAE cwa;
-- connect to cwa DB
\connect cwa
BEGIN;
-- revoke all permissions from this database for all users (beside superuser)
REVOKE ALL ON DATABASE cwa FROM PUBLIC;
-- One role (gets connection right) and two users:
CREATE ROLE cwa_users;
CREATE ROLE cwa_reader IN ROLE cwa_users LOGIN PASSWORD 'change-me-in-production';
CREATE ROLE cwa_inserter IN ROLE cwa_users LOGIN PASSWORD 'change-me-in-production';
-- ("CREATE USER […] "instead of "CREATE ROLE […] LOGIN […]" is the same
-- Both Login roles get connection right:
GRANT CONNECT ON DATABASE cwa TO cwa_users;
CREATE TABLE diagnosis_key (
key_data BYTEA NOT NULL PRIMARY KEY, -- really BYTEA? Why not UUID? Is 16 bytes, bytea takes 20 (4 bytes length) and has dynamic length
rolling_period INTEGER NOT NULL, -- is this a timestamp?!?
rolling_start_number INTEGER NOT NULL,
submission_timestamp BIGINT NOT NULL, -- usually better: TIMESTAMP WITH TIME ZONE!
transmission_risk_level INTEGER NOT NULL
);
-- revoke from everyone
REVOKE ALL ON diagnosis_key FROM PUBLIC;
REVOKE ALL ON diagnosis_key FROM current_user;
-- Set rights to the users
GRANT SELECT ON diagnosis_key TO cwa_reader; -- reader can do only SELECTs
GRANT INSERT ON diagnosis_key TO cwa_inserter; -- inserter can do only INSERTs
COMMIT;
Recreating the database and everything
Usually it is a good idea to have a set of scripts, which creates a database and all needed users from scratch, and maybe this can delete an old database etc.
Create a “create all” script and include all others with \ir …
(I may provide you with some sql-scripts for this if needed.)
Better variant with least privileges via functions
It is better, more secure and often may give better performance, creating functions for the table access. Then the login users get NO (!) access to the tables, they can only call functions like “insert_key” or “get_key_by_id” or whatever. The following is only an example; in the application logic here, you may use other queries (e.g. returning multiple rows).
-- New database
CREATE DATABAE cwa;
-- connect cwa DB (long form: \connect)
\c cwa
BEGIN;
-- revoke all permissions from this database for all users (beside superuser)
REVOKE ALL ON DATABASE cwa FROM PUBLIC;
-- One role (gets connection right) and two users:
CREATE ROLE cwa_users;
CREATE ROLE cwa_reader IN ROLE cwa_users LOGIN PASSWORD 'change-me-in-production';
CREATE ROLE cwa_inserter IN ROLE cwa_users LOGIN PASSWORD 'change-me-in-production';
-- ("CREATE USER […] "instead of "CREATE ROLE […] LOGIN […]" is the same
-- Both Login roles get connection right:
GRANT CONNECT ON DATABASE cwa TO cwa_users;
CREATE TABLE diagnosis_key (
key_data UUID NOT NULL PRIMARY KEY, -- changed to uuid ;-)
rolling_period INTEGER NOT NULL, -- is this a timestamp too?!?
rolling_start_number INTEGER NOT NULL,
submission_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), -- set transaction time by default
transmission_risk_level INTEGER NOT NULL
);
-- revoke from everyone
REVOKE ALL ON diagnosis_key FROM PUBLIC;
REVOKE ALL ON diagnosis_key FROM current_user;
-- now create functions for accessing the data
CREATE OR REPLACE FUNCTION get_diagnosis_data_for_key(in_key UUID)
RETURNS diagnosis_key
AS
$code$
SELECT * FROM diagnosis_key WHERE key_data = in_key;
$code$
LANGUAGE sql
SECURITY DEFINER
SET search_path = public, pg_temp;
-- maybe alter owner, if there is an extra owner:
-- ALTER FUNCTION get_diagnosis_data_for_key(UUID) OWNER TO cwa_owner;
-- set execute permission ONLY to cwa_reader:
REVOKE ALL ON FUNCTION get_diagnosis_data_for_key) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_diagnosis_data_for_key(UUID) TO cwa_reader;
-- and a function for inserting too!
COMMIT;
This is only an example for showing the principle!
In this example, it is only possible to read one row; an attacker, wo takes control over the “inserter” application can only call the inserter function, nothing else. In reality you need other functions, this is only an example.
The benefit is, that this is much more secure.
See also
In this thread I mentioned some other possible improvements (e.g. partitioning for automatic deletion of old data) too.
An example for using functions in an application is Posemo, PostgreSQL Secure Monitoring. There all functions are generated by Code, e.g. see the checks folder or documentation how to write new checks.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:5
- Comments:7 (4 by maintainers)
Hi @alvar-freude,
thanks for giving feedback to the CWA and sharing your thoughts here. All ideas helping us making CWA better are well appreciated!
Let’s go through your points one by one:
Least Privilege Users
We are following the principle of Least Privilege based on separation of the necessary scope of our microservices. We introduced dedicated database users for the different scenarios we have:
The different roles were introduced with https://github.com/corona-warn-app/cwa-server/pull/396.
Our DBA’s are creating the three database users with dedicated SQL scripts, which are not part of the code base, they are only part of internal runbooks. The scripts are working the same way you explained in your post, especially with regards of revocation of permissions for PUBLIC.
Postgres superuser
CWA server does not use any superuser (like postgres) on any deployed instance. This repository only contains the source code & setup for local environment, e.g. with the help of docker compose.
The actual deployment is done differently. We are using helm charts internally and are currently evaluating to open source them as well. The actual secrets are stored in Hashicorp Vault.
Recreating the database and everything
As already mentioned above, we are using the SQL scripts in the runbooks for creation of the database users. In order to construct the necessary tables & perform database migration we delegate the task to Flyway, which manages migration scripts. Flyway uses the dedicated migration user, while the microservices use their assigned database user (submission/distribution) in order to enforce the concept of least privilege.
Variant via functions
Using functions here are a valid option for achieving the database access & control. However, we also need to account for other aspects as well. Therefore we decided to go with Spring Data JPA in order to stick with the ORM approach. In the end, this question of ORM vs database native capabilities is also a question of personal preference and style.
We believe that both approaches are valid, but we decided to stick to the ORM.
Proposed Changes on the Diagnosis Key Table
Those topics were also raised in a couple of days ago, and answered in this https://github.com/corona-warn-app/cwa-server/issues/345#issuecomment-635588014
Thanks again for having a critical eye on the repo and helping us making CWA better.
Chris
Since this topic is gaining a lot of visibility everywhere, I would like to make one thing very clear. The issue description states a false fact:
Currently, the database is used with a PostgreSQL superuser, which has full rights the the database cluster. The Table diagnosis_key is accessible to the "PUBLIC" pseudo user, so every other database user has full access too.
You are referring here to the local development / demo setup, used within the docker compose steps we provided for enabling the community & Fraunhofer Institute to easily set up the CWA server on you local machines.
On any deployed environment we are using, we are neither using a superuser, nor is the table diagnosis_key accessible to PUBLIC.
We realize that the details about the actual deployment are currently not transparently available in the codebase. I hope we can add those in the near future.