18

Goal

Create a database with three users and restrict their privileges (I'm just thinking out loud, so my user separation is also open to correction):

  1. Superuser - this user allows for the very initial provisioning of the database. Create the application database, create the other users, set their privileges. Default postgres superuser works for me, so this one is done.
  2. Administrator - this user has access only to the database that was created during provisioning. Administrator can CRUD all data in all tables, and can also CRUD tables, etc. "Superuser for only this database" type of situation. When the application is being updated, the administrator is the user used by automated tooling to handle database migrations.
  3. App user - this user is ultimately the one who supports the web app's functionality. Note this has nothing to do with users on web pages etc - this is the user the server leverages to run queries, insert and remove data. I explicitly do not want this user to be able to modify permissions of anything, nor create/destroy tables or indices or anything structural.

What I've tried

First off, looking at the (generally excellent) PostgreSQL documentation, the page on Grant pretty much leaves me cross-eyed. After spending a few hours reading about PostgreSQL roles and privileges I'm generally confused. I think with a bit more work I'll be able to nail down what I want for the admin user, but I'm pretty stuck on the "app user". I've gotten about this far (naming and passwords are all just placeholders):

$ psql -U postgres
postgres=# CREATE USER "app-admin" WITH PASSWORD 'password';
CREATE ROLE
postgres=# CREATE USER "app-user" WITH PASSWORD 'password';
CREATE ROLE
postgres=# CREATE DATABASE "test-database" WITH OWNER "app-admin";
CREATE DATABASE
postgres=# \c "test-database"
You are now connected to database "test-database" as user "postgres".
test-database=# DROP SCHEMA "public";
DROP SCHEMA
test-database=# CREATE SCHEMA "app" AUTHORIZATION "app-admin";
CREATE SCHEMA

And here's where I get unsure. I feel like the answer I'm trying to avoid is "revoke everything by default then enumerate all the privileges you'll need at all the different levels on all the different objects". I'm trying to avoid that because I straight up don't know what I need there. If that ends up being the answer, then I'll just have to hunker down and read a bunch more, but generally when I start going down paths like that I've missed something.

Issues

How do I restrict privileges for app-user so they are unable to modify any structural data (e.g. cannot add or destroy tables) but are able to connect and do anything with rows (row level security is not even on my radar). Is this general model of privileges not really in sync with what PostgreSQL expects? I feel like I'm missing something if I have to walk through every option on that "grant" page to accomplish something like this - whether it be my motivation for doing it in the first place or the means by which I'm going about it.

Context

I'm trying to build my first end-to-end web application. I've done enough general software development and web app development, now I'm trying to understand the pieces that I generally take for granted day to day. I'm trying to set up a PostgreSQL server while keeping the principle of least privilege in mind.

Side-quest

I haven't seen this done on web apps where I have simply joined the development team, although they're generally small and not heavily used. Does doing this actually accomplish anything? Does anyone have compelling reasons for why to do something like this, or why it's a bad or ineffective idea? My assumption was that if I ultimately ended up with a SQL injection vulnerability, this would mitigate the damage because the database user would have limited access. Is that misguided?

Neat articles I've found on the subject:

user2152081
  • 777
  • 1
  • 9
  • 17

2 Answers2

18

I'll answer your “side-quest” question first:

you are completely right with your worries and concerns, and everybody who designs an application should think about the same things. Everything else is sloppy and careless.

To mitigate the damage that can be caused by a successful SQL injection attack, you should definitely employ the principle of least privilege.

It should be quite simple to set up a system that matches your requirements.

I'll use the object names from your exaple, except that I'll use underscores instead of minuses. It is good practive to use only lower case letters, underscores and numbers in object names, since it will make your life easier.

/* create the database */
\c postgres postgres
CREATE DATABASE test_database WITH OWNER app_admin;
\c test_database postgres

/* drop public schema; other, less invasive option is to
   REVOKE ALL ON SCHEMA public FROM PUBLIC */
DROP SCHEMA public;
/* create an application schema */
CREATE SCHEMA app AUTHORIZATION app_admin;
/* further operations won't need superuser access */
\c test_database app_admin
/* allow app_user to access, but not create objects in the schema */
GRANT USAGE ON SCHEMA app TO app_user;

/* PUBLIC should not be allowed to execute functions created by app_admin */
ALTER DEFAULT PRIVILEGES FOR ROLE app_admin
   REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;

/* assuming that app_user should be allowed to do anything
   with data in all tables in that schema, allow access for all
   objects that app_admin will create there */
ALTER DEFAULT PRIVILEGES FOR ROLE app_admin IN SCHEMA app
   GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES FOR ROLE app_admin IN SCHEMA app
   GRANT SELECT, USAGE ON SEQUENCES TO app_user;
ALTER DEFAULT PRIVILEGES FOR ROLE app_admin IN SCHEMA app
   GRANT EXECUTE ON FUNCTIONS TO app_user;

But if you take the principle of least seriously, you should grant table permissions individually and e.g. not allow app_user to DELETE and UPDATE data in tables where there is no need for the user to do so.

Laurenz Albe
  • 209,280
  • 17
  • 206
  • 263
  • 1
    Thanks for such a thorough explanation! Consistent with industrial software development in general, it seems the status quo is closer to the "sloppy and careless" end of the spectrum than not. I guess continuing in the same vein it makes sense to have an "administrative" and "unprivileged" connection pool to separate the ever-present difference between the regular users of an app and the administrators who need to add/edit/remove users, their permissions, etc. I hadn't really thought about mapping through "least privilege" to that level, but it makes sense. – user2152081 Jan 10 '17 at 04:44
  • That sounds like you are mixing up "application users" (that log into the application) and database users (which the application uses to connect to the database). Or do you have a database user for each application user? It might be difficult to switch to a different database user just because an "admin user" check box was checked in the application. If you drive this too far, you might end up with something very complicated, and that's bad for security too. – Laurenz Albe Jan 10 '17 at 10:05
  • I'm trying to reconcile your comment about "grant table permissions individually" with how that plays out on an application level. If I have two distinct classes of privileged users where one class has a superset of the permissions the other class has (e.g admins can modify all the regular data, but they can also interact with tables regular users can't touch), doesn't it make sense to split into two connection pools - one with more permissions than the other? I'm aware that you could keep saying "doesn't it make sense" until you have 1-1 DB user to app user, so I'm trying to find the balance. – user2152081 Jan 11 '17 at 04:34
  • I see. With "individual permissions" I did not mean "different permissions for different application users" but "rather than allow `app_user` to do everything with a default privilege, add a `GRANT` statement after each `CREATE TABLE` that gives `app_user` the necessary privileges". I think it is good practice to use a single database user for an application to log in. – Laurenz Albe Jan 11 '17 at 07:34
  • Ah, I had misunderstood then. I'll give this a shot shortly - thanks so much for all your help! – user2152081 Jan 11 '17 at 16:26
  • 2
    I never updated this after trying it out, it worked exactly as desired for me. I've come back to this answer most times I've set up a new database for a web app. I'd re-upvote if I could! – user2152081 Nov 21 '18 at 22:37
4

For Web Applications, I split the permissions into three roles, where each role inherits from its predecessor.

  1. Read Only - Used for SELECT queries and function calls
  2. Insert - Used for INSERT statements
  3. Update and Delete - These are used mostly for Administration, as the public facing front-end application does not usually modify or deletes data

That way, even if some hacker manages to do SQL Injection he is limited to the permissions of the role that is used, usually only SELECT or INSERT.

My web applications usually do not need the more intrusive permissions like CREATE, DROP, TRUNCATE, etc., so I don't GRANT those permissions to web apps.

In the rare instances where the the second role needs to update or delete something, I either give it permission for that specific table, or put the code in a function that is created with SECURITY DEFINER.

/** role_read is read-only with SELECT and EXECUTE */
CREATE ROLE role_read;
/** role_read_add adds INSERT */
CREATE ROLE role_read_add;
/** role_read_add_modify adds UPDATE and DELETE */
CREATE ROLE role_read_add_modify;


GRANT USAGE ON SCHEMA <schema> TO role_read;

/** for existing objects */
GRANT SELECT  ON ALL TABLES    IN SCHEMA <schema> TO role_read;
GRANT SELECT  ON ALL SEQUENCES IN SCHEMA <schema> TO role_read;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA <schema> TO role_read;

/** for future objects */
ALTER DEFAULT PRIVILEGES IN SCHEMA <schema>
    GRANT SELECT ON TABLES TO role_read;

ALTER DEFAULT PRIVILEGES IN SCHEMA <schema>
    GRANT SELECT ON SEQUENCES TO role_read;

/** role_read_add inherits from role_read */
GRANT role_read TO role_read_add;

/** for existing objects */
GRANT INSERT ON ALL TABLES IN SCHEMA <schema> TO role_read_add;
GRANT ALL ON ALL SEQUENCES IN SCHEMA <schema> TO role_read;

/** for future objects */
ALTER DEFAULT PRIVILEGES IN SCHEMA <schema>
    GRANT INSERT ON TABLES TO role_read_add;

ALTER DEFAULT PRIVILEGES IN SCHEMA <schema>
    GRANT ALL ON SEQUENCES TO role_read_add;

/** role_read_add_modify inherits from role_read_add */
GRANT role_read_add TO role_read_add_modify;

/** for existing objects */
GRANT UPDATE, DELETE ON ALL TABLES IN SCHEMA <schema> 
    TO role_read_add_modify;

/** for future objects */
ALTER DEFAULT PRIVILEGES IN SCHEMA <schema>
    GRANT UPDATE, DELETE ON TABLES TO role_read_add_modify;
isapir
  • 21,295
  • 13
  • 115
  • 116