- Mon 09 October 2023
- server admin
- Gaige B. Paulsen
- #server admin, #vault, #postgres
This is part of a multi-part series on using postgres databases, vault, and a variety of other tools to effect short-lived database credentials for real use.
As postgres uses user and role interchangably, so will I, although I'll generally try to use user to refer to a role with login permissions.
Postgres roles and privileges
There are some really interesting and powerful capabilities for managing roles in postgres, and as I've become more familiar with them, I understand why so many of us are in the habit of granting overly-broad privileges in the database realm.
A few important things to know about roles and privileges:
-
The owner of a new object is the user/role that created that object. As such, any default privileges for objects owned by that user must either be assigned by that user, or assigned on behalf of the user. Be careful/consistent about the role that creates tables and sequences so that you prevent problems. Only the owner is allowed to alter tables and sequences.
-
By default new objects are not provided privileges to existing roles. In order to ensure that new tables, sequences, etc. are usable by the roles that we are creating, they must be either granted explicitly each time a new object is created, or you must have an appropriate default permission for the
public
role or an explicit role. NOTE: defaults for new objects are owner-dictated, and thus the default role must be granted by the owner of the table, or on behalf of the owner of the table or it won't work as desired. -
There are rules about dropping roles that may not be obvious. An owning role cannot be deleted until that owned object is removed or ownership is given to another role. Additionally any privileges owned by the role need to be dropped before dropping the role.
-
By default, all users have
create
privileges in thepublic
schema for each database. This is not necessary for the creation of temporary tables, but generally makes it easier for all new users to work with a database. When designing your access controls, it probably makes sense to revoke all unnecessary privileges from public (revoke create on schema public from public
). -
Roles that are not marked
noinherit
in their definition will be inherited by any user granted that role unless you explicitly mark the grantnoinherit
. This may make sense when you are looking at privileges that don't grant ownership, but if you are trying to ensure that you don't have unexpectedly-owned tables, you will want your ownership roles to havenoinherit
set or grant themnoinherit
. -
Prior to version 16 It's worth noting that non-owning users with the
createrole
permission do not have the ability to directly set their roles to be any arbitrary role on the system. However, I've found nothing to prevent a user withcreaterole
permission from granting itself any role on the system except superuser. Keep that in mind that these granting roles are potent. -
As of PostgreSQL 16, there is indeed a specific mechanism for making sure that roles are not granted by just any user who has the
createrole
permission. As of version 16, the grantor must have role admin capability on any roles that it will assign. As such, ourvault
role needs to haveGRANT owner,readwrite,readonly to vault-admin with admin option
set in order to create the roles with the appropriate role. As such, thevault-admin
role is now more limited in what it can do.
Role design
Designing an appropriate role structure is complicated and this is not intended to be one-size-fits-all in the least. However, I hope it serves as a useful starting-off point. This design expects tob e used with dynamic roles through vault, and I'll detail that at the end.
The recommended "group" roles are:
role | purpose | privileges |
---|---|---|
postgres |
superuser role (maybe break glass) | SUPERUSER , LOGIN |
vault-admin |
used by vault to manage dyanmic users | CREATEROLE , LOGIN , granted each role with ADMIN OPTION |
owner |
create/modify tables | ALL on SCHEMA public and ALL on tables and sequences |
readwrite |
read/write to tables | SELECT on SCHEMA public and ALL on tables and sequences |
readonly |
read-only on tables | SELECT on SCHEMA public and SELECT on tables and SELECT on sequences |
NOTE: The readwrite
user is granted ALL
on tables and sequences.
This may be a bit more than necessary, consider if you want to grant
REFERENCES
and TRIGGER
tables, since these are frequently unnecssary.
Although the USAGE
privilege is separate on sequences, it's not
particularly useful to allow UPDATE
without allowing USAGE
, as that
won't allow you to use the sequence in a nextval
call.
When creating the dynamic roles (assuming use with vault), there will be three role templates used:
role | creation statement |
---|---|
owner |
CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' IN ROLE \"owner\" NOINHERIT; |
readwrite |
CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' IN ROLE \"readwrite\" INHERIT; |
readonly |
CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' IN ROLE \"readonly\" INHERIT; |
The creation statements are all very similar, except for the specifics of which role is assigned and if they are inherited.
In my case, I'm choosing to use inherit
for the non-ownership roles
because it removes the need to set role
. However, it's not necessary
and unless you need the (temporary) user name, setting role is probably
preferred.
This structure can be used for any application framework, and with or without a secrets maanger like vault, but since my example here started as a vault example, the creation of these items would be sequenced thusly:
- Create the new database
- Create the
owner
role and grant it appropriate privileges - Create the
readwrite
role and grant it the appropriate privileges and defaults in the schema on behalf ofowner
- Create the
readwrite
role and grant it the appropriate privileges and defaults in the schema on behalf ofowner
At this point, you have what you need to create the database admin user, the reader, and the writer. If you're using vault, then use the creation statements above. If not, you can use similar statements manually so that if you have multiple users, you can control access centrally.