Security#

AgensGraph’s security model is PostgreSQL’s. Roles, pg_hba.conf, TLS, GRANT and row-level security all work exactly as they do there, and PostgreSQL’s client-authentication and database-roles chapters are the reference.

What this page adds is how those mechanisms land on a graph. A graph is a schema and a label is a table, so the grants a Cypher query needs are not always the ones you would guess.

Authentication#

pg_hba.conf in the data directory decides who may connect, from where, to which database, and how. Rules are matched top to bottom and the first match wins.

# TYPE  DATABASE  USER      ADDRESS          METHOD
local   all       all                        peer
host    mydb      appuser   10.0.0.0/24      scram-sha-256
hostssl mydb      analyst   0.0.0.0/0        scram-sha-256
host    all       all       0.0.0.0/0        reject

Reload after editing:

ag_ctl -D $AGDATA reload

Three things to get right before a server is reachable:

Replace the default trust rules. initdb writes trust for local connections, which means anyone with a shell account can connect as any role, superuser included. Use peer for local Unix-socket connections and scram-sha-256 for anything over the network.

Use scram-sha-256, not md5. Set password_encryption = scram-sha-256 before you create or change passwords, so the stored verifier is the strong one.

Require TLS for network connections. hostssl in the rule, plus ssl = on and a certificate and key in postgresql.conf. sslmode=verify-full on the client is what actually prevents an interception; require only encrypts.

The methods available are PostgreSQL’s full set — peer, scram-sha-256, cert, ldap, gss, sspi, pam, radius, ident, trust, reject — with the same options. See PostgreSQL’s authentication-methods chapter.

When a connection is refused, the server log says why, and the message is more specific than what the client is told. Check it before changing rules.

Roles#

Roles are cluster-wide — they belong to the server, not to a database — and a role with LOGIN is what other systems call a user.

CREATE ROLE appuser LOGIN PASSWORD 'secret';
CREATE ROLE readonly;                       -- a group, no LOGIN
GRANT readonly TO appuser;

The attributes that matter operationally are SUPERUSER, CREATEDB, CREATEROLE, REPLICATION and BYPASSRLS; grant none of them to an application role. ALTER ROLE … SET pins a setting for a role, which is a clean way to force graph_path, or to keep enable_graph_dml off for everyone but a maintenance role:

ALTER ROLE appuser SET graph_path = moviekb;
ALTER ROLE appuser SET enable_graph_dml = off;

Permissions on a graph#

A graph is a schema and each label is a table, so the grants are ordinary ones. The surprises come from elsewhere: a Cypher query touches more objects than the labels it names.

Reading#

A read needs USAGE on the graph’s schema and SELECT on each label it touches. Follow this sequence to see where each grant becomes necessary. Start with the schema only:

CREATE ROLE analyst;
GRANT USAGE ON SCHEMA moviekb TO analyst;
SET ROLE analyst;

A Cypher read now fails as a table permission error, naming the label:

MATCH (m:movie) RETURN count(m);
ERROR:  permission denied for table movie

Grant the label and it works:

RESET ROLE;
GRANT SELECT ON moviekb.movie TO analyst;
SET ROLE analyst;
MATCH (m:movie) RETURN count(m);
 count
-------
 18

An unlabelled pattern needs ag_vertex as well. MATCH (n) scans the graph’s ag_vertex parent and every label under it, so a role that can read movie but not ag_vertex still cannot run it:

MATCH (n) RETURN count(n);
ERROR:  permission denied for table ag_vertex

The same applies to ag_edge for an unlabelled edge. So for a read-only analyst role, grant across the schema rather than label by label:

RESET ROLE;
GRANT SELECT ON ALL TABLES IN SCHEMA moviekb TO analyst;
ALTER DEFAULT PRIVILEGES IN SCHEMA moviekb GRANT SELECT ON TABLES TO analyst;

The ALTER DEFAULT PRIVILEGES line is what makes a label created next month readable too.

Writing#

Creating a vertex or an edge needs INSERT on the label and USAGE on the label’s id sequence. That second one is easy to miss. Grant only the INSERT:

RESET ROLE;
GRANT INSERT ON moviekb.movie TO analyst;
SET ROLE analyst;
CREATE (:movie {title: 'X'});
ERROR:  permission denied for sequence movie_id_seq

Add the sequence and the write goes through:

RESET ROLE;
GRANT USAGE ON SEQUENCE moviekb.movie_id_seq TO analyst;

SET and REMOVE need UPDATE on the label. DELETE needs DELETE on it. And DETACH DELETE needs a grant on ag_edge, because that is how it finds the edges to remove:

ERROR:  permission denied for table ag_edge

An error naming ag_edge or ag_vertex is a missing grant on the graph’s parent label, not a row-security result — worth knowing, because it is easy to read it as one.

A working set of grants for a role that reads and writes a whole graph:

GRANT USAGE ON SCHEMA moviekb TO appuser;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA moviekb TO appuser;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA moviekb TO appuser;
ALTER DEFAULT PRIVILEGES IN SCHEMA moviekb
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO appuser;
ALTER DEFAULT PRIVILEGES IN SCHEMA moviekb GRANT USAGE ON SEQUENCES TO appuser;

Creating labels#

Creating a label needs CREATE on the graph’s schema. Creating a graph needs CREATE on the database.

Row-level security#

Row-level security works on a label, so a policy can restrict which vertices or edges a role sees and writes:

ALTER TABLE moviekb.movie ENABLE ROW LEVEL SECURITY;

CREATE POLICY movie_visible ON moviekb.movie
  FOR SELECT TO analyst
  USING ((properties->>'genre') = 'Sci-Fi');

Warning

Re-test any RLS policy you had on 2.17. Before 2.18, policies were not properly enforced on graph writes: the USING clause of an UPDATE or DELETE policy was attached to a relation that is never scanned, so it filtered nothing — a role with no DELETE policy at all could delete rows. INSERT policies were checked against the wrong row, and edge creation was not checked at all, so an edge label with row security accepted every insert.

2.18 enforces all of it. That is the correct behaviour, and it means writes your application performs today may start being refused. Test before you upgrade, not after. See Release notes.

Two practical notes:

  • A policy expression reads the label’s columns, so it works on properties — and on a promoted column, which is faster and lets the policy use an index.

  • BYPASSRLS and table ownership both skip policies. An application role should have neither.

Two settings that are part of the security posture#

enable_graph_dml and enable_graph_ddl are off by default, and they gate writing to and reshaping a label with SQL rather than Cypher. Leaving them off keeps an application on the Cypher path, where the graph’s own checks run — for example that an edge’s endpoints exist.

They are not the whole of a graph’s integrity, and should not be read as it. COPY writes a label without consulting either setting. What does hold on every path, whatever the settings say, is that a row’s id names the label that stores it: a write that would break that is refused, whether it comes from Cypher, INSERT, UPDATE, COPY or a replication apply.

Turn them on for maintenance work and turn them off again, or pin them off per role with ALTER ROLE … SET.

cypher_allow_unsafe_ddl likewise. See Server settings.

Encryption at rest#

AgensGraph does not encrypt data files itself. Use filesystem or block-device encryption, or Agens TDE where transparent data encryption is a requirement. pgcrypto covers column-level encryption, and a property is an ordinary jsonb value, so it can hold a pgcrypto ciphertext like any other column.