11. Security
By the end of this chapter: You change the installation defaults for production use and protect connections, accounts, privileges, and files.
- Right after installation, read this chapter after chapter 5 (task map in chapter 0).
11.1 Security at a glance
| Area | Details | How to protect |
|---|---|---|
| Incoming connections | TCP: one PostgreSQL port (default 5432). 2 Unix sockets: PostgreSQL (/tmp/.s.PGSQL.<port>) and the supervisor process (run/grit_admin.sock, for the stop command) |
11.3, 11.4, and 11.7 |
| Outgoing connections | GritDB sends no communication on its own. It has no license or statistics communication. Only an analytics setup subscription opens an outgoing connection to the operational database port (6.4) | 1.6 and 6.4.1 step 4 |
| Places where original SQL text and data values remain | postgresql.log, GritDB logs, core files, datafiles/query_text_archive.jsonl, and analytic query history (6.5) |
9.7 and 11.7 |
| Accounts and privileges | Right after installation, every OS user on the same server connects as superuser (11.2) | 11.3 and 11.5 |
11.2 Installation defaults
| Item | Installation default | What to do before production |
|---|---|---|
| Authentication | trust (local, 127.0.0.1, and ::1 connections on the same server are allowed without a password) | 11.3 |
| superuser | Service account name (gritdb), no password |
11.3 |
| Listen address | localhost only |
11.4 |
| Data checksums | Off | — |
| File permissions | The permissions from the installation package, unchanged (including group write) | 11.7 |
- ⚠ If the installation defaults stay unchanged, every OS user on the same server can connect as superuser.
11.3 Change authentication
Change the installation default (trust). Set the local line of the service account to peer. GritDB internal work uses this connection.
- If the server has other OS users, do step 2 right after installation, before the first start. In that case, do steps 1, 3, 4, and 5 after the start (QuickStart 3.4).
-
Set the superuser password. Run the command below in psql as the service account.
\password gritdb\passwordencrypts the password before it sends it. AnALTER ROLE … PASSWORD '…'statement can remain inpostgresql.log, depending on the audit logging settings (11.9). It remains even if the statement fails (log_min_error_statement).- Check whether other login accounts have passwords stored as MD5. Run
SELECT rolname FROM pg_authid WHERE rolcanlogin AND rolpassword LIKE 'md5%';. It returns 0 rows. Accounts in the result (accounts moved from another PostgreSQL, 8.3) cannot connect through thescram-sha-256lines of step 2. Before step 2, set their passwords again with\password <account>. The default ofpassword_encryptionisscram-sha-256.
-
Change the setting lines of
~/gritdb1/datafiles/pgdata/pg_hba.conf(pg_hba.conf) as below. Leave the comment lines as they are.# TYPE DATABASE USER ADDRESS METHOD local all gritdb peer local all all scram-sha-256 host all all 127.0.0.1/32 scram-sha-256 host all all ::1/128 scram-sha-256- The first line lets the service account (
gritdb) connect without a password only from the OS account with the same name (peer). The other lines usescram-sha-256. If you leave the first line astrust, other OS users can also connect as superuser with-U gritdb. - Delete the
replicationlines if you do not use them. If you use them, change them toscram-sha-256. - For remote connection lines, see 11.4.
- The first line lets the service account (
-
Check that there are no error lines, then reload the configuration. Run the queries below in psql as the service account.
SELECT line_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL; SELECT pg_reload_conf();- The first query must return 0 rows. If there is even one error line, PostgreSQL refuses to reload and keeps the old rules (the installation default,
trust). In that case,postgresql.loghaspg_hba.conf was not reloaded.pg_reload_conf()still returnst. pg_hba_file_rulesshows the file on disk, not the rules that the server uses. To see whether the rules changed, use the test in step 4.
- The first query must return 0 rows. If there is even one error line, PostgreSQL refuses to reload and keeps the old rules (the installation default,
-
Test the connections.
- As the service account, run
~/gritdb1/bin/psql -p 5432 -d postgres -c "SELECT 1". It returns1without a password. - The service account TCP test checks whether the new rules are in use. Run
~/gritdb1/bin/psql -h 127.0.0.1 -p 5432 -d postgres -w -c "SELECT 1". It returnsfe_sendauth: no password supplied. If you get1, the new rules are not in use. In that case, check withgrep 'pg_hba.conf was not reloaded' ~/gritdb1/log/postgresql.logand start again from step 3. If~/.pgpasshas a line that starts with127.0.0.1or*, the password is supplied and you get1. In that case, check with the next item (another OS user). - Test as another OS user on the same server. Do this before you narrow the permissions in 11.7, when that user can run
<installation directory>/bin/psql. As that user, run<installation directory>/bin/psql -h /tmp -p 5432 -d postgres -U gritdb -c "SELECT 1". It returnsFATAL: Peer authentication failed for user "gritdb". With-h 127.0.0.1instead, it asks for a password.
- As the service account, run
-
Test a GritDB restart. Run
gritdb-stopfirst, thengritdb-start, and check it with 4.1. Then check that the GritDB internal connections are connected.SELECT count(*) FROM pg_stat_activity WHERE application_name LIKE 'gritdb\_catalog\_%';- It is normal if the query returns
6(the default, 7.1).
- It is normal if the query returns
- To check the rules in the file, run
SELECT count(*) FROM pg_hba_file_rules WHERE auth_method = 'trust' OR error IS NOT NULL;. It returns0. To check the rules that the server uses, run the service account TCP test in step 4 (11.10,pg_hba_file_rules).
⚠ Do not change the Unix socket location (
unix_socket_directories), and do not block thelocalline of the service account
- Reason: GritDB internal work connects as the service account to the default socket (
/tmp) without a password. If you block it, GritDB may not start.- Instead: Set the
localline of the service account topeer, and change only the other lines to password authentication.
11.4 Open remote connections
With the installation defaults, GritDB accepts connections only from the same server (localhost). Open remote connections when applications, BI tools, or CDC and ETL tools on other servers need to connect.
-
Set the addresses to listen on. Run the statement below as the service account.
ALTER SYSTEM SET listen_addresses = 'localhost,<server address>'; -
Restart GritDB. Run
gritdb-stopfirst, thengritdb-start. This setting needs a restart (5.4). -
Add lines to
pg_hba.confthat allow the connections.host <db> <account> <client address range> scram-sha-256 -
Check for error lines, then reload the configuration (11.3 step 3). If there is an error line, the added lines are not used.
-
In the firewall, open only the PostgreSQL port (default 5432), and only for the address ranges that connect.
- To check, run
psql "host=<server address> port=5432 dbname=<db> user=<account>" -c "SELECT 1"on the server that connects. It returns1after you enter the password. Connections from addresses that are not allowed are rejected. - 1.1 has no connection encryption (TLS), so open access only inside a segregated network or an access-controlled zone (11.8).
- If addresses change, update
listen_addressesandpg_hba.confon both sides. For an analytics setup, runALTER SUBSCRIPTION <sub> CONNECTION '<new connection info>';(6.4.5).
11.5 Accounts and privileges
Separate accounts by use.
| Account | Use | Privileges |
|---|---|---|
Administrator account: service account (gritdb) |
Installation, start, stop, register, unregister, configuration, and monitoring | superuser |
| Application account | Writes from applications, CDC tools, and ETL tools, and regular session queries | SELECT, INSERT, UPDATE, and DELETE on the tables it writes |
| Analytics account | Analytics session queries from reports and BI tools (read only) | Only SELECT on the tables it reads |
Create the analytics account. Run the statements in psql as the service account. Create the application account in the same way.
CREATE ROLE <analytics account> LOGIN;
\password <analytics account>
GRANT SELECT ON ALL TABLES IN SCHEMA <s> TO <analytics account>;
- For database and schema privileges, see “Security requirements for registered tables” below. The commands are described in
GRANTandREVOKE. ON ALL TABLESgrants only on the tables that exist now. To include tables created later, runALTER DEFAULT PRIVILEGES FOR ROLE <account that creates tables> IN SCHEMA <s> GRANT SELECT ON TABLES TO <analytics account>;(ALTER DEFAULT PRIVILEGES). The application account follows the same pattern.- Create tables, views, and functions with the schema name (
CREATE TABLE <s>.<t> …). For the service account (gritdb), the first entry ofsearch_path,"$user", points to thegritdbschema. So if you create an object without a schema, it goes into thegritdbschema. Under the security requirements below, do not keep registered tables in thegritdbschema. To check, runSELECT current_schema();as the service account. If it returnsgritdb, this rule applies. - Keep only one superuser, the service account. A superuser can read the connection password of an analytics setup subscription (6.4.1 step 8).
- Do not give write privileges to the analytics account. Use analytics sessions as read-only (6.6).
- Only a superuser can change the read-only setting of analytics sessions (
gritsurfer_write_visibility_guard). Keep the default (on).
With the installation defaults, every account can use the gritdb schema and run the functions in it. These include the register, unregister, and status functions. Before production, revoke the function execute privileges in each database, and grant roles only the functions they use.
- Why revoke: The analytic query history functions (6.5) show the original text of all queries in that database, regardless of account. An account with execute privilege reads the original query text that other accounts sent.
- An account with execute privilege can unregister the registered tables of other accounts. Unregistering deletes the cold files of those tables (6.3).
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA gritdb FROM PUBLIC;
GRANT EXECUTE ON FUNCTION gritdb.get_gritsurfer_tables() TO <role>;
- The targets are all databases that
SELECT datname FROM pg_database WHERE datallowconn;returns, includingpostgresandtemplate1. Connect to each database and run the statements. - If you revoke them in
template1, databases that you create afterwards also start with the privileges revoked (6.1). - Choose the functions to grant from the status functions in the A.3 table (
get_gritsurfer_*,show_gritsurfer_*, andgritsurfer_version). Only the service account uses the register and unregister functions, so do not grant them to roles (A.3). For functions with arguments, include the argument types (for example,gritdb.get_gritsurfer_query_history(integer, timestamptz)). - The service account (superuser) runs all functions regardless of these privileges, so register and unregister tables as the service account.
- Do not block access by revoking
USAGEon thegritdbschema. Block it with the function execute privileges above. When GritDB starts, it grantsUSAGEon thegritdbschema of thepostgresdatabase to all accounts again. - To check, run the query below in each database. It returns
0.
SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'gritdb' AND has_function_privilege('public', p.oid, 'EXECUTE');
- Function:
has_function_privilege
GritDB internal connections (gritdb_catalog_*, 7.1) connect as the service account through the Unix socket without a password. Therefore, do not block the local line of the service account (11.3).
Security requirements for registered tables (mandatory):
- Keep in registered tables only data that every account that can connect to that database and use that schema may read.
- Do not register tables whose visibility must be split by privileges (tables where the readable rows or columns differ by account). For tables that are not registered, PostgreSQL runs the queries even in analytics sessions (6.6).
- If you must separate data by account, keep a separate database or schema that only the accounts allowed to read the data can enter, and register only the tables in it.
Database level:
REVOKE CONNECT ON DATABASE <db> FROM PUBLIC;
GRANT CONNECT ON DATABASE <db> TO <analytics account>, <application account>;
- To check, run
~/gritdb1/bin/psql -p 5432 -d <db> -U <another account>as another account. It returnspermission denied for database "<db>". - To check, run
~/gritdb1/bin/psql -p 5432 -d <db> -U <application account> -c "SELECT 1"as the application account. It returns1.
Schema level:
REVOKE ALL ON SCHEMA <s> FROM PUBLIC;
GRANT USAGE ON SCHEMA <s> TO <analytics account>;
-
To check, run
SELECT count(*) FROM <s>.<t>;as another account. It returnspermission denied for schema <s>. -
Do not keep registered tables in the
publicorgritdbschema. All accounts haveUSAGEon both schemas. -
An account that receives USAGE must be allowed to read all registered tables in that schema (all columns).
-
An account that has
pg_read_all_datareads the tables even without USAGE. -
An analytics setup (6.4) receives tables under the same schema names as the operational database. So use this only for tables that are already separated by schema on the operational database.
-
The service account (superuser) and GritDB internal connections connect and read regardless of these privileges.
11.6 Analytics sessions and row security
- Analytics sessions in 1.1 do not apply row-level security (RLS) policies or security barrier views (
security_barrier). Query data whose visibility must be split by RLS in a regular session. - Do not register tables that have RLS policies. For tables that are not registered, PostgreSQL runs the queries even in analytics sessions, so the policies apply (6.6). If you already registered such a table, unregister it (6.3).
- To check, run the query below in each database that has registered tables. It returns 0 rows.
SELECT t.schema_name, t.table_name
FROM get_gritsurfer_tables() t
JOIN pg_class c ON c.oid = t.table_id
WHERE c.relrowsecurity;
- Also do not register tables whose visibility must be split by table or column privileges (11.5).
- To check column privileges, run the query below in each database that has registered tables. It returns 0 rows. Tables in the result have privileges granted separately per column. For such tables, revoke the column privileges or unregister the table (6.3, 11.5).
SELECT DISTINCT t.schema_name, t.table_name
FROM get_gritsurfer_tables() t
JOIN pg_attribute a ON a.attrelid = t.table_id
WHERE a.attnum > 0 AND NOT a.attisdropped AND a.attacl IS NOT NULL;
- Column privileges are in
attaclofpg_attribute. It has a value only when privileges were granted separately on the column.
11.7 File and directory permissions
Files keep the permissions from the installation package (including group write, 11.2). Narrow the installation directory so that only the service account can use it.
chmod 700 ~/gritdb1
chmod 700 "$(readlink -f ~/gritdb1/datafiles)"
stat -c '%a %U %n' ~/gritdb1 "$(readlink -f ~/gritdb1/datafiles)"
- It is normal if both lines show
700 gritdb. - If you relocated the data volume (2.4), give its real path the same permissions. The second line does this. The same applies if you placed
pgdataseparately.
| Location | What to protect |
|---|---|
run/ |
Contains the stop command socket (grit_admin.sock). A user who can write to this socket can stop GritDB, so make sure other users cannot enter it. If the installation directory is 700, they are blocked |
datafiles/, log/, core file location, and backup copies |
Contain original SQL text and data values (11.1). Allow only the service account to read them |
Service account home (~/.pgpass) |
Contains the analytics setup subscription connection password in plain text (6.4.1 step 8). Set its permissions to 600. If the permissions are wider, PostgreSQL does not use this file |
Service account home (~/.psql_history) |
Statements entered in psql remain, including statements that contain passwords. Narrow the home directory permissions and delete the lines that contain passwords (6.4.1 step 8) |
| Shell that starts GritDB | Start GritDB from a shell without environment variables that start with PG and without LD_LIBRARY_PATH. The environment of the starting shell passes to the GritDB processes (4.1) |
TMPDIR of the starting shell (/tmp if unset, 1.4 ④) |
Temporary files of GritDB engine cleanup work (__tkidx_spill_*.tmp) contain key values of registered tables. The file permissions follow the umask of the starting shell. In /tmp, other users can usually read them. So create a directory that only the service account can enter (mkdir -m 700 <directory>) and set it as TMPDIR. Use the same value at every start (4.1, 4.6) |
- GritDB logs that PostgreSQL backends write have file mode 0600. The installation directory permissions (700) protect other logs and
datafiles/. GritDB creates the directories underdatafiles/cold_file/with mode775. File permissions follow theumaskof the starting shell. Even if you narrow the permissions inside, new directories are775again. So keep the installation directory and the relocated real path at 700 (the check above).
11.8 Security limits of 1.1
| Limit | What to do |
|---|---|
| No connection encryption (TLS) | Network segregation, access-controlled zone, SSH tunnel |
| No external authentication (LDAP, PAM, GSSAPI) | PostgreSQL password authentication (scram-sha-256) |
| No encryption of stored data | OS or storage encryption (not yet tested) |
| Audit logging: PostgreSQL log settings | 11.9 |
| No password policy (complexity, lockout after failed logins) | PostgreSQL default (VALID UNTIL) and checks in 11.10 |
| Analytics sessions: do not apply row-level security (RLS) or security barrier views | Do not register RLS tables. Query them in a regular session (11.6) |
11.9 Audit logging settings
Record connections and DDL in postgresql.log. Run the statements as the service account.
ALTER SYSTEM SET log_connections = on;
ALTER SYSTEM SET log_disconnections = on;
ALTER SYSTEM SET log_statement = 'ddl';
ALTER SYSTEM SET log_line_prefix = '%m [%p] %u@%d %r ';
SELECT pg_reload_conf();
- The settings take effect right after reload, in open sessions too.
log_connectionsandlog_disconnectionstake effect from new connections. - Settings:
log_connections,log_disconnections,log_statement, andlog_line_prefix - Keep the first part of
log_line_prefix(%m [%p]) as the PostgreSQL default. Lines must start with the timestamp for the ERROR count commands in QuickStart 5.2 to work as they are. Add the account (%u), database (%d), and client address (%r) after it. - Do not set
log_statementtoall. All original SQL text and values remain in the log (9.7). - With
ddl, passwords in statements such asCREATE ROLE … PASSWORDandCREATE SUBSCRIPTION … password=also remain in the log. So set passwords with\password(11.3). For subscriptions, see 6.4.1 step 8 andCREATE SUBSCRIPTION. Failed statements remain regardless of thelog_statementvalue (log_min_error_statement). - For rotation and retention, see 9.4 and 9.5.
- To check, run
SHOW log_statement;. It returnsddl. After a new connection, run the command below. It shows the account that just connected.
grep "connection authorized" ~/gritdb1/log/postgresql.log | tail -1
Analytic query records:
- Analytic query history (6.5) has the first 1024 characters of the original query text, stage times,
pid, andsession_start_time. There is no account name column, so match the account bypidwith the connection records above ([<pid>]). datafiles/query_text_archive.jsonlhas the full original text of analytics session queries (9.1).- There is no dedicated audit extension in 1.1 (10.6).
11.10 Security checklist
Check before production (7.7) and in the monthly routine check (7.3). Run the SQL as the service account. Run the per-database items in each database that has registered tables.
| # | Item | Check | Good | What to do in 1.1 |
|---|---|---|---|---|
| 1 | trust removed | SELECT count(*) FROM pg_hba_file_rules WHERE auth_method = 'trust' OR error IS NOT NULL;, service account TCP test (11.3 step 4) |
0, fe_sendauth: no password supplied |
11.3 |
| 2 | superuser password | SELECT rolname, rolpassword IS NOT NULL AS has_password FROM pg_authid WHERE rolsuper; |
One row for the service account, t |
11.3 and 11.5 |
| 3 | Connection address restriction | SHOW listen_addresses;, SELECT type, database, user_name, address, auth_method FROM pg_hba_file_rules; |
Only the addresses you set, host lines use scram-sha-256 |
11.4 |
| 4 | Function execute privileges | Check query in 11.5 (in each database) | 0 |
11.5. Check again after reinitialization (3.1) and after a patch version (3.5) |
| 5 | RLS tables and tables whose visibility must be split by privileges are not registered | Check queries in 11.6, review of the registration list (6.5) | 0 rows. Every account that enters that database and schema may read the tables in the list | 11.5 and 11.6 |
| 6 | Privileges of databases and schemas with registered tables (CONNECT, USAGE, 11.5) |
SELECT has_database_privilege('public', '<db>', 'CONNECT');, SELECT has_schema_privilege('public', '<s>', 'USAGE'); |
f for the level you chose (database or schema) |
11.5 |
| 7 | File permissions | stat command in 11.7, stat -c '%a %n' ~/.pgpass (if the file exists), stat -c '%a %U %n' <TMPDIR directory> (if you set it) |
700 gritdb, 600, 700 gritdb |
11.7 |
| 8 | Audit logging | SHOW log_connections;, SHOW log_statement; |
on, ddl |
11.9 |
| 9 | Export of logs and core files | Export records | Reviewed and masked before export | 9.7 |
| 10 | Patch version | jq -r .product_version ~/gritdb1/share/gritdb/manifest.json |
The latest patch version you received | 3.5 |
| 11 | Release-channel installation package | jq -r .channel ~/gritdb1/share/gritdb/manifest.json |
release. No development files (chapter 0) |
Chapter 0 and 3.5 |
| 12 | No registered tables in the gritdb schema |
SELECT count(*) FROM gritdb.get_gritsurfer_tables() WHERE schema_name = 'gritdb' AND table_name <> 'grit_surfer_heartbeat'; |
0 |
11.5 and 6.2 |
| 13 | Login account passwords | SELECT rolname, rolvaliduntil, rolpassword LIKE 'md5%' AS md5 FROM pg_authid WHERE rolcanlogin ORDER BY 1; |
Accounts that connect with a password have an expiration date under your organization’s rules (ALTER ROLE <account> VALID UNTIL '<timestamp>'). md5 is f |
11.3 and 11.8 |
- The expiration in item 13 applies only to password authentication. It does not block
peerconnections of the service account or GritDB internal connections. - If a vulnerability scanner reports known vulnerabilities from the PostgreSQL version number (
SHOW server_version;), contact support (chapter 0). In that case, also see the patch version (3.5).
If a problem occurs
- If GritDB fails to start after an authentication change, see TroubleShooting.
Learn more
- For log export, see 9.7. For limitations, see chapter 10.
- PostgreSQL 17 documentation: Privileges, Schemas and
search_path, Predefined roles (pg_read_all_data),CREATE ROLE(VALID UNTIL), andpassword_encryption