Knowledge Graph Security for AI Platforms

TL;DR — Knowledge graphs power AI retrieval by encoding relationships between entities, but they introduce unique security challenges. Graph traversals can cross tenant boundaries through shared relationships. Properties on a single node may have different sensitivity levels. Access control must operate at four levels: node-level (who can see this entity), property-level (who can see this field), relationship-level (who can traverse this edge), and subgraph-level (who can reach this cluster of nodes). Implement RBAC with DENY precedence, query rewriting for row-level security, tenant partitioning, and audit logging. Use the knowledge graph for access-controlled, relationship-aware retrieval alongside a vector database for semantic search. Never allow unfiltered graph traversals in a multi-tenant environment.

Why Knowledge Graphs Need Different Security

Relational databases store data in rows and tables. Security is straightforward: row-level security filters rows, column-level security filters columns, and queries are bounded by JOINs that the developer explicitly writes.

Knowledge graphs store data as nodes and relationships. A single Cypher query can traverse thousands of relationships, crossing from public data to private data through shared nodes. A user who has access to a "Project" node might traverse an "owned_by" relationship to reach a "User" node containing PII — without ever explicitly querying for user data.

This makes knowledge graph security fundamentally different:

Property Relational DB Knowledge Graph
Data model Rows and tables Nodes, relationships, properties
Access unit Row Node, property, relationship, subgraph
Query scope Bounded by explicit JOINs Unbounded traversals
Cross-tenant risk Low (separate tables) High (shared relationship nodes)
Query rewriting WHERE clause injection Path filtering + relationship restrictions
Permission granularity Table, row, column Node, property, relationship, traversal

Four Levels of Graph Access Control

Level 1: Node-Level Security

Control which nodes a user can see based on their role. This is the graph equivalent of row-level security.

@node("Document")
@secured(
    read=["reader", "editor", "admin"],
    write=["editor", "admin"],
    create=["editor", "admin"],
    delete=["admin"]
)
class Document:
    id: str
    title: str
    content: str
    tenant_id: str
    sensitivity: str  # "public", "internal", "restricted"

Enforcement: filter nodes by role after query execution, or rewrite the query to include role-based predicates.

Level 2: Property-Level Security

Control which properties a user can see on a node they already have access to. A reader can see a Person's name and email but not their SSN.

@node("Person")
@secured(
    read=["reader", "editor", "admin"],
    deny_read_properties={
        "ssn": ["reader", "editor"],      # Only admin can read SSN
        "salary": ["reader", "editor"],   # Only admin can read salary
        "internal_notes": ["reader"]      # Editor and admin can read notes
    },
    deny_write_properties={
        "ssn": ["editor"]                 # Only admin can write SSN
    }
)
class Person:
    name: str
    email: str
    ssn: str
    salary: float
    internal_notes: str

DENY precedence: DENY always takes precedence over GRANT. If a property is denied for a role, no amount of grants on the node can override it.

Enforcement: Property filtering happens post-query (filter denied properties from returned nodes) or at query-time (rewrite the query to exclude denied properties). Post-query is simpler but less efficient — the database retrieves data that is then filtered out. Query-time is more efficient but requires query rewriting.

Level 3: Relationship-Level (Traversal) Security

Control which relationships a user can follow during a traversal. This is unique to graph databases and has no equivalent in relational databases.

@relationship("REFERENCES")
@secured(
    traverse=["reader", "editor", "admin"],
    create=["editor", "admin"],
    delete=["admin"]
)

@relationship("OWNED_BY")
@secured(
    traverse=["admin"],  # Only admin can traverse ownership
    create=["admin"],
    delete=["admin"]
)

Why this matters: A user with READ access to a Document node might traverse a "references" relationship to reach connected documents. Without traversal control, a single accessible node becomes a bridge to the entire graph.

Cross-tenant risk: In a multi-tenant graph, shared nodes (like a public taxonomy or a shared category) can become bridges between tenants. Traversal security prevents a user in tenant A from reaching tenant B's nodes through a shared "categorized_as" relationship.

Level 4: Subgraph-Level Security

Control which clusters of nodes a user can reach, considering the full traversal path. This is the most complex but most important level for multi-tenant environments.

def enforce_subgraph_security(query: str, user: User, tenant: Tenant) -> str:
    """Rewrite query to restrict traversals to the user's permitted subgraph."""

    # Inject tenant filter at every node match
    query = inject_tenant_filter(query, tenant.id)

    # Remove relationships that cross tenant boundaries
    query = remove_cross_tenant_traversals(query, tenant.id)

    # Add sensitivity filter based on user's clearance level
    query = inject_sensitivity_filter(query, user.clearance_level)

    return query

Example: A query that traverses from a Document through "owned_by" to a User, then through "member_of" to a Team, must be validated at every hop:
- Can the user read this Document? (node-level)
- Can the user traverse "owned_by"? (relationship-level)
- Can the user read this User? (node-level)
- Can the user traverse "member_of"? (relationship-level)
- Can the user read this Team? (node-level)

If any check fails, the traversal must stop — not return partial results that reveal the existence of the blocked node.

Tenant Isolation Strategies

Strategy 1: Separate Graphs Per Tenant (Strongest)

Each tenant gets their own graph database or graph namespace. No shared nodes, no cross-tenant traversals possible.

def get_graph_for_tenant(tenant_id: str) -> Graph:
    return Graph(f"graph:{tenant_id}")

# Tenant A's queries can never reach tenant B's data
graph_a = get_graph_for_tenant("tenant-a")
results = graph_a.query("MATCH (d:Document) RETURN d")

Pros: Strongest isolation, no cross-tenant leakage possible, simple security model.
Cons: Resource overhead (one graph per tenant), no shared knowledge (taxonomies, ontologies must be duplicated), harder to maintain consistency.

Strategy 2: Tenant-Labeled Nodes with Query Rewriting (Balanced)

All tenants share one graph, but every node has a tenant_id property. Every query is rewritten to filter by tenant.

-- Original query
MATCH (d:Document)-[:owned_by]->(u:User)
RETURN d, u

-- Rewritten query (tenant-a)
MATCH (d:Document {tenant_id: "tenant-a"})-[:owned_by]->(u:User {tenant_id: "tenant-a"})
RETURN d, u

Pros: Efficient resource usage, shared schema, simpler management.
Cons: Query rewriting must be bulletproof — a single unrewritten query leaks data. Shared nodes (taxonomies) create cross-tenant bridge risk.

Implementation:

def rewrite_query_with_tenant_filter(query: str, tenant_id: str) -> str:
    """Inject tenant_id filter into every node pattern in the query."""
    # Parse the Cypher query
    parsed = parse_cypher(query)

    # For every node pattern, add tenant_id property
    for node in parsed.node_patterns:
        if node.label in TENANT_SCOPED_LABELS:
            node.add_property("tenant_id", tenant_id)

    # For every relationship pattern, verify both endpoints have same tenant_id
    for rel in parsed.relationship_patterns:
        if rel.crosses_tenant_boundary:
            raise SecurityError("Cross-tenant traversal detected")

    return parsed.to_cypher()

Strategy 3: Shared Graph with Tenant-Scoped Traversals (Weakest)

All tenants share one graph, including some shared nodes. Traversals are scoped to the tenant's permitted subgraph.

Pros: Maximum flexibility, shared knowledge graphs, efficient.
Cons: Highest risk — shared nodes are bridges. Requires comprehensive traversal security. One bug in traversal filtering exposes all tenants.

Recommendation: Use Strategy 2 for most enterprise deployments. Use Strategy 1 for tenants with strict compliance requirements (healthcare, defense). Avoid Strategy 3 unless you have a dedicated security team.

Query Rewriting for Row-Level Security

Row-level security in graph databases requires query rewriting — automatically injecting security predicates into every query.

Cypher Query Rewriting

class GraphQueryRewriter:
    def __init__(self, security_context: SecurityContext):
        self.context = security_context

    def rewrite(self, query: str) -> str:
        parsed = parse_cypher(query)

        for match_clause in parsed.match_clauses:
            for node in match_clause.nodes:
                # Inject tenant filter
                if self.context.tenant_id:
                    node.add_property("tenant_id", self.context.tenant_id)

                # Inject sensitivity filter
                if self.context.clearance_level:
                    node.add_property_constraint(
                        "sensitivity", "<=", self.context.clearance_level
                    )

                # Inject owner filter for personal documents
                if node.label == "PersonalDocument":
                    node.add_property_constraint(
                        "owner", "=", self.context.user_id
                    )

        # Remove denied relationships from traversals
        for match_clause in parsed.match_clauses:
            for rel in match_clause.relationships:
                if rel.type in self.context.denied_traversals:
                    raise SecurityError(
                        f"Traversal denied for relationship type: {rel.type}"
                    )

        return parsed.to_cypher()

Example: Before and After Rewriting

Original query (user input):

MATCH (d:Document)-[:references]->(r:Document)
WHERE d.title CONTAINS "quarterly"
RETURN d, r

Rewritten query (after security injection):

MATCH (d:Document {
    tenant_id: "tenant-456",
    sensitivity: "internal"
})-[:references]->(r:Document {
    tenant_id: "tenant-456",
    sensitivity: "internal"
})
WHERE d.title CONTAINS "quarterly"
RETURN d.title, r.title

Note: property-level security also rewrites the RETURN clause to exclude denied properties.

Audit Logging for Graph Databases

Graph database audit logs must capture more than relational database logs:

{
  "timestamp": "2026-07-07T10:15:00Z",
  "user_id": "alice@company.com",
  "tenant_id": "tenant-456",
  "role": "analyst",
  "query": "MATCH (d:Document)-[:references]->(r:Document) WHERE d.title CONTAINS 'quarterly' RETURN d, r",
  "rewritten_query": "MATCH (d:Document {tenant_id: 'tenant-456', ...})...",
  "nodes_accessed": [
    {"id": "node-123", "label": "Document", "properties_read": ["title", "content"]},
    {"id": "node-456", "label": "Document", "properties_read": ["title"]}
  ],
  "relationships_traversed": [
    {"type": "references", "from": "node-123", "to": "node-456"}
  ],
  "nodes_denied": [
    {"id": "node-789", "label": "Document", "reason": "sensitivity_exceeds_clearance"}
  ],
  "properties_denied": [
    {"node_id": "node-123", "property": "internal_notes", "reason": "role_denied"}
  ],
  "result_count": 2,
  "execution_time_ms": 15
}

Key fields that graph audit logs must capture:
- Nodes accessed: Which nodes were returned, with which properties
- Relationships traversed: Which relationship types were followed
- Nodes denied: Which nodes were filtered out and why
- Properties denied: Which properties were redacted and why
- Rewritten query: The actual query executed after security injection

Knowledge Graph + Vector Database Architecture

For enterprise AI, the knowledge graph and vector database serve complementary roles:

User Query
    ↓
Knowledge Graph (access-controlled retrieval)
    ├── Resolve entity relationships
    ├── Enforce node/property/traversal permissions
    ├── Return permitted subgraph
    ↓
Vector Database (semantic search within permitted subgraph)
    ├── Embed permitted nodes
    ├── Semantic similarity search
    ├── Rank by relevance
    ↓
LLM (generation)
    ├── Grounded in permitted, relevant context
    └── Generate response with citations

Why this order matters: The knowledge graph filters first (access control), then the vector database ranks (relevance). If the vector database filters first, it might retrieve semantically relevant but access-denied content — which then must be filtered out, potentially leaking information through timing or result count side channels.

Per-Tenant Vector Partitions

Even with knowledge graph filtering, vector databases should use per-tenant partitions:

def search_vector_db(query_embedding: list, tenant_id: str, permitted_node_ids: list):
    # Search only within the tenant's partition and permitted nodes
    results = vector_db.search(
        query_embedding,
        filter={
            "tenant_id": tenant_id,
            "node_id": {"$in": permitted_node_ids}
        },
        top_k=10
    )
    return results

Per-Tenant Security Configuration

tenant_graph_security:
  tenant_id: "tenant-456"
  isolation_strategy: "labeled_nodes"  # or "separate_graphs"
  rbac:
    enabled: true
    deny_precedence: true
    role_hierarchy:
      - admin
      - editor
      - reader
      - PUBLIC
  property_level_security:
    enabled: true
    default_deny: ["ssn", "salary", "internal_notes"]
  traversal_security:
    enabled: true
    denied_traversals_for_reader:
      - "owned_by"
      - "accessed_by"
  query_rewriting:
    enabled: true
    inject_tenant_filter: true
    inject_sensitivity_filter: true
  audit:
    log_nodes_accessed: true
    log_traversals: true
    log_denials: true
    log_rewritten_queries: true
    retention_days: 365
  vector_db:
    per_tenant_partition: true
    search_after_graph_filter: true

Common Security Mistakes

Unfiltered Graph Traversals

Allowing users to write arbitrary Cypher queries without query rewriting. A user can traverse from an accessible node to any connected node, crossing tenant boundaries through shared relationships.

Fix: Never allow raw user queries. All queries must pass through the query rewriter that injects tenant and security filters.

Post-Query Property Filtering Only

Filtering denied properties after the query returns all data from the database. This means the database reads sensitive data into memory, even if it's filtered before returning to the user. A bug in the filtering logic exposes everything.

Fix: Use query-time property filtering (exclude denied properties from the RETURN clause) wherever possible. Post-query filtering should be a defense-in-depth layer, not the primary control.

Shared Nodes as Tenant Bridges

A shared taxonomy node ("Category: Finance") connected to documents from multiple tenants. A traversal from the shared node reaches all connected documents regardless of tenant.

Fix: Mark shared nodes as "non-traversable from" — users can match them directly but cannot traverse relationships from them to other nodes. Or duplicate shared nodes per tenant.

Missing Traversal Security

Implementing node-level and property-level security but forgetting relationship-level security. Users can traverse relationships they shouldn't, reaching nodes through paths that bypass node-level filters.

Fix: Explicitly define traversal permissions for every relationship type. Default-deny: if a relationship type is not explicitly granted, traversal is denied.

Inconsistent Query Paths

Different code paths for querying the graph — some go through the query rewriter, others don't. An unrewritten query bypasses all security.

Fix: Centralize all graph access through a single gateway that enforces query rewriting. No code should have direct access to the graph database client.

Implementation Checklist

  • [ ] Implement node-level RBAC (READ, WRITE, CREATE, DELETE per role)
  • [ ] Implement property-level security with DENY precedence
  • [ ] Implement relationship-level (traversal) security
  • [ ] Implement subgraph-level security for multi-tenant traversals
  • [ ] Choose tenant isolation strategy (separate graphs or labeled nodes)
  • [ ] Implement query rewriting for row-level security
  • [ ] Inject tenant_id filter into every node pattern
  • [ ] Inject sensitivity filter based on user clearance
  • [ ] Block cross-tenant traversals through shared nodes
  • [ ] Default-deny for unconfigured relationship traversals
  • [ ] Centralize all graph access through a security gateway
  • [ ] Never allow raw user Cypher/Gremlin queries
  • [ ] Use query-time property filtering, not just post-query
  • [ ] Log all nodes accessed, properties read, and traversals followed
  • [ ] Log denied nodes, denied properties, and denied traversals
  • [ ] Log rewritten queries for audit trail
  • [ ] Configure per-tenant security policies
  • [ ] Use knowledge graph filtering before vector database search
  • [ ] Partition vector database per tenant
  • [ ] Test cross-tenant traversal prevention with adversarial queries

Conclusion

Knowledge graphs are powerful tools for AI retrieval — they encode relationships that vector databases cannot. But their power is also their security risk: graph traversals can cross tenant boundaries, reach sensitive nodes through indirect paths, and expose properties that users should not see.

The defense is four-level access control: node-level (who can see this entity), property-level (who can see this field), relationship-level (who can traverse this edge), and subgraph-level (who can reach this cluster). Implement query rewriting that injects tenant and security filters into every query. Use DENY precedence for property-level security. Default-deny for traversal security. Centralize all graph access through a security gateway — never allow raw user queries.

For multi-tenant platforms, choose labeled nodes with query rewriting for balanced isolation, or separate graphs for strict compliance tenants. Never allow shared nodes to become tenant bridges. Use the knowledge graph for access-controlled retrieval before vector database search — permissions first, relevance second.

Enterprise customers will ask about graph traversal security in their review. Having a clear answer — four-level access control, query rewriting, tenant isolation, and comprehensive audit logging — is essential for closing enterprise deals.