DEV Community

AlaiKrm
AlaiKrm

Posted on

Securing AI Access to HR Systems: The Architecture That Actually Works

I get asked this more than almost any other architecture question right now. How do you give an AI assistant access to HR data without creating a security and compliance disaster? Here is the architecture I have landed on after working through it on several deployments.

The short version: you do not give the AI access to HR data directly. You give specific AI agents access to specific HR data subsets, under specific access policies, with full audit logging. These are four separate design decisions and most teams collapse them into one.

The threat model first

Before designing anything, you need to be clear about what you are protecting against.

The external threat is straightforward: you do not want HR data accessible to anyone outside the authorized user set, which means your inference infrastructure cannot call out to external APIs with HR context in the payload.

The internal threat is less obvious but more common in practice: you do not want an employee to be able to query HR data they would not have access to through normal channels. An SDR should not be able to ask your AI what their manager's performance review said. A new hire should not be able to ask what the company's compensation bands are if that data is restricted.

Most RAG deployments handle the external threat reasonably well by using enterprise agreements with LLM providers. Almost none of them handle the internal threat adequately without deliberate architectural design.

The architecture

The pattern I use separates the knowledge base into access-controlled partitions that map to your existing permission structure.

User Query
    |
    v
Query Router (authenticated, knows user role/permissions)
    |
    +-- If user has HR_GENERAL access --> HR General partition
    |   (org chart, public policies, benefits info)
    |
    +-- If user has HR_MANAGER access --> HR Manager partition
    |   (team performance data, review summaries)
    |
    +-- If user has HR_ADMIN access  --> HR Admin partition
    |   (compensation data, disciplinary records)
    |
    v
Retrieval runs ONLY against authorized partitions
    |
    v
LLM receives context only from authorized partitions
Enter fullscreen mode Exit fullscreen mode

The retrieval layer enforces access before the LLM sees anything. The LLM cannot reason about data it was never given. This is the key property that most architectures miss: filtering after retrieval is not equivalent to never retrieving in the first place.

Implementation with metadata filtering

In practice this looks like tagging every document at ingestion time with its access tier:

def ingest_hr_document(doc_path, access_tier, department=None):
    metadata = {
        "access_tier": access_tier,          # "hr_general", "hr_manager", "hr_admin"
        "doc_category": "hr",
        "department": department or "all",
        "ingested_at": datetime.now().isoformat(),
        "status": "current"
    }
    chunks = chunk_document(doc_path)
    for chunk in chunks:
        chunk.metadata.update(metadata)
    vectorstore.add_documents(chunks)
Enter fullscreen mode Exit fullscreen mode

And filtering at query time based on the authenticated user's permissions:

def retrieve_with_access_control(query, user_permissions):
    allowed_tiers = []
    if "hr_general" in user_permissions:
        allowed_tiers.append("hr_general")
    if "hr_manager" in user_permissions:
        allowed_tiers.append("hr_manager")
    if "hr_admin" in user_permissions:
        allowed_tiers.append("hr_admin")

    results = vectorstore.similarity_search(
        query=query,
        filter={"access_tier": {"$in": allowed_tiers}},
        k=5
    )
    return results
Enter fullscreen mode Exit fullscreen mode

This is the minimum viable implementation. It assumes your permission tiers are stable enough to hardcode. If your permission structure is more dynamic, you need the filter to call out to your IAM system at query time rather than using a static list.

The audit logging requirement

Every HR-related AI query needs an audit trail that captures: who asked, what they asked, which documents were retrieved, what access tier those documents were in, and what response was generated. This is not optional if you are in a regulated industry or if you have employees in jurisdictions with strong data rights.

def log_hr_query(user_id, query, retrieved_docs, response, session_id):
    audit_record = {
        "timestamp": datetime.now().isoformat(),
        "session_id": session_id,
        "user_id": user_id,
        "query_hash": hash(query),    # hash to avoid storing PII in the log
        "retrieved_doc_ids": [doc.metadata["doc_id"] for doc in retrieved_docs],
        "access_tiers_accessed": list(set([doc.metadata["access_tier"] for doc in retrieved_docs])),
        "response_length": len(response)
    }
    audit_store.insert(audit_record)
Enter fullscreen mode Exit fullscreen mode

Store the query hash rather than the raw query if the query itself might contain sensitive information. Store doc IDs rather than doc content. You want the audit log to be auditable without itself being a vector for data exposure.

On self-hosted vs cloud for this use case

I want to be direct about one thing. The access control architecture above can be implemented on top of external LLM APIs with enterprise agreements. But there is a fundamental limitation: even with perfect metadata filtering, the assembled prompt still contains HR data that travels to an external inference endpoint.

For most organizations this is an acceptable risk given enterprise agreements and "zero training" commitments. For organizations in healthcare, financial services, or jurisdictions with strict data residency requirements, it is not acceptable regardless of the contract terms.

The clean solution for those cases is inference on premises, where the assembled prompt containing HR context never leaves your network. A few platforms now package this as a deployable product rather than a DIY infrastructure project. PrivOS (https://privos.ai/) is one of the ones I have evaluated that handles the room-scoped isolation model natively, meaning the access control is built into the data model rather than implemented as a filter layer on top of a general-purpose vector store. Worth evaluating if your threat model requires true data residency.

The architecture I described above is the right architecture for this problem. The implementation details vary based on your stack and your threat model, but the shape of the solution is consistent.

Top comments (0)