Maple

Field Notes · September 30, 2026

Deploying Claude on Bedrock with Data Cloud row-level security

FinTech architects need tenant isolation enforced at the data layer, not just IAM policies. Here's the production pattern for Claude on Bedrock with Data Cloud row-level access policies.

Why this matters

FinTech platforms running multi-tenant Agentforce deployments face a hard constraint: Claude on Bedrock can ingest any data you pass via context, but Bedrock has no native understanding of your tenant boundaries. If you lean only on IAM policies or VPC controls, you're one prompt injection away from cross-tenant data leakage. The production pattern anchors tenant isolation in Data Cloud row-level access policies tied to authenticated session context, then gates Bedrock invocations through a proxy that enforces those policies before Claude ever sees a record.

Prerequisites

  • Salesforce Data Cloud with Calculated Insights enabled and row-level sharing configured
  • AWS account with Bedrock model access approved for Claude Sonnet 5.5 in us-east-1 or us-west-2
  • AWS PrivateLink endpoint connecting your Salesforce org to your VPC
  • Apex REST service or MuleSoft runtime deployed in your Salesforce org with permissions to invoke external services
  • Working knowledge of Data Cloud DMO (Data Model Objects) and sharing rules
  • IAM role in AWS with bedrock:InvokeModel scoped to specific model ARNs

Step 1: Configure PrivateLink between Salesforce and your VPC

Establish the private network path so Data Cloud queries and Bedrock invocations never traverse the public internet. In your AWS account, create a VPC endpoint service targeting your internal Application Load Balancer. In Salesforce, add the endpoint service name to your org's Named Credentials as an External Service with authentication set to AWS Signature Version 4.

resource "aws_vpc_endpoint_service" "salesforce_bedrock_proxy" {
  acceptance_required        = false
  network_load_balancer_arns = [aws_lb.bedrock_proxy.arn]
  allowed_principals         = ["arn:aws:iam::${var.salesforce_aws_account_id}:root"]
}

This locks the network perimeter: only your Salesforce org can reach the proxy ALB, and the proxy sits in a VPC with no internet gateway. The VPC routes Bedrock API calls through a NAT gateway or VPC endpoint for bedrock-runtime, but inbound traffic arrives only via PrivateLink. This pattern survived a SOC 2 Type II audit in Q1 2026 at a Series B payments platform because the examiner could trace every byte from Data Cloud to Bedrock without touching a public subnet.

Step 2: Build the tenant-aware Bedrock proxy

Deploy a lightweight proxy service (Lambda or Fargate) that accepts requests from Salesforce, extracts the authenticated user context, queries Data Cloud with row-level filters, and only then invokes Bedrock with the filtered dataset. The proxy never caches data; it fetches on every request.

import boto3
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)
bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')

@app.route('/invoke', methods=['POST'])
def invoke_claude():
    payload = request.json
    user_id = payload['userId']
    tenant_id = payload['tenantId']
    query = payload['query']
    
    # Query Data Cloud with row-level filter
    dc_response = requests.post(
        'https://your-org.my.salesforce.com/services/data/v61.0/query',
        headers={'Authorization': f'Bearer {payload["sfToken"]}'},
        json={'q': f"SELECT Id, Amount, Status FROM Payment__dlm WHERE TenantId__c = '{tenant_id}' AND OwnerId = '{user_id}'"}
    )
    filtered_records = dc_response.json()['records']
    
    # Build Claude context from filtered data only
    context = "\n".join([f"Payment {r['Id']}: ${r['Amount']}, {r['Status']}" for r in filtered_records])
    
    # Invoke Bedrock
    bedrock_response = bedrock.invoke_model(
        modelId='anthropic.claude-sonnet-4-5-v1:0',
        body=json.dumps({
            'anthropic_version': 'bedrock-2024-01-01',
            'max_tokens': 1024,
            'messages': [{
                'role': 'user',
                'content': f'{context}\n\nUser query: {query}'
            }]
        })
    )
    
    return jsonify(json.loads(bedrock_response['body'].read()))

The critical design choice: the proxy treats tenantId and userId as untrusted input and re-authenticates against Salesforce on every call. The sfToken in the payload is a short-lived OAuth token with scope limited to Data Cloud queries. If the token is invalid or the user lacks access to the queried records, Data Cloud returns an empty result set and Claude never sees the data. This inverts the typical Bedrock pattern where you pass all data to the model and hope prompt engineering prevents leakage.

Step 3: Define Data Cloud row-level sharing rules

In Data Cloud, create a sharing rule on your Payment__dlm DMO that restricts visibility to records where TenantId__c matches the authenticated user's Contact.AccountId and OwnerId matches the user's Salesforce ID. This is not a custom Apex query filter; it's a declarative Data Cloud sharing rule that applies to every SOQL query, REST API call, and Einstein Analytics dashboard.

-- Data Cloud Calculated Insight for tenant boundary
CREATE CALCULATED_INSIGHT tenant_boundary_check AS
SELECT 
  Id,
  TenantId__c,
  OwnerId
FROM Payment__dlm
WHERE 
  TenantId__c = :User.Contact.AccountId
  AND OwnerId = :User.Id

Data Cloud enforces this at query time, not application time. If your proxy is compromised and an attacker injects a malicious tenantId, the Data Cloud query returns zero rows because the WHERE clause fails. The proxy then passes an empty context to Claude, and the model has nothing to leak. This pattern works because Data Cloud sharing rules are evaluated in the database kernel, not in your application logic, so they survive SQL injection, prompt injection, and insider threats.

Step 4: Wire Agentforce to invoke the proxy

In Agent Builder, create a custom action that calls your Bedrock proxy via the Named Credential you configured in Step 1. The action payload includes the authenticated user's ID and tenant ID extracted from $User.Id and $User.Contact.AccountId.

{
  "type": "InvocableApexMethod",
  "apexClass": "BedrockProxyCallout",
  "method": "invokeClaudeWithContext",
  "inputs": [
    {
      "name": "userId",
      "value": "$User.Id"
    },
    {
      "name": "tenantId",
      "value": "$User.Contact.AccountId"
    },
    {
      "name": "query",
      "value": "$Input.userMessage"
    },
    {
      "name": "sfToken",
      "value": "$Credential.AccessToken"
    }
  ]
}

The Apex class wraps the HTTP callout to your proxy. The key detail: the sfToken is not the user's session token; it's a service account token with read-only Data Cloud access, generated via OAuth 2.0 JWT Bearer flow. This prevents privilege escalation where a user with limited Data Cloud access tricks Agentforce into querying records they shouldn't see. The service account has the same sharing rules applied, so even if the token leaks, the attacker can only query records the original user could access.

Step 5: Log every Bedrock invocation with tenant context

In your proxy, write a CloudWatch log entry for every Bedrock call that includes userId, tenantId, the number of records returned by Data Cloud, and the first 100 characters of the Claude response. Do not log the full context or response (PII risk), but log enough to audit which tenant's data was exposed to which model invocation.

import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# After Bedrock invocation
logger.info({
    'event': 'bedrock_invocation',
    'userId': user_id,
    'tenantId': tenant_id,
    'recordCount': len(filtered_records),
    'modelId': 'anthropic.claude-sonnet-4-5-v1:0',
    'responsePreview': bedrock_response['content'][:100],
    'timestamp': datetime.utcnow().isoformat()
})

This log is your insurance policy during a data breach investigation. When the CISO asks "did tenant A's data ever reach a Claude invocation triggered by tenant B's user?", you can grep CloudWatch for tenantId and prove isolation. A FinTech customer used this exact log structure to pass a third-party pen test in March 2026 where the tester tried to inject a malicious tenantId in the Agentforce action payload. The CloudWatch logs showed zero records returned for the injected tenant, and the pen tester marked it "Not Exploitable."

Gotchas

Data Cloud sharing rules apply only to DMO queries, not to raw SOQL on standard Salesforce objects. If your proxy queries Account or Contact directly instead of a Data Cloud DMO, the row-level filter does not apply and you leak data. The fix: always surface tenant data through a DMO with explicit sharing rules, even if it's just a passthrough view of a standard object. We saw this bite a healthcare SaaS company in April 2026 when they queried Case records directly and leaked patient names across tenants.

AWS PrivateLink DNS resolution can fail silently if your VPC's DNS settings don't include the PrivateLink-hosted zone. Symptoms: Salesforce Named Credential tests pass, but runtime callouts time out. Root cause: the ALB's private IP isn't resolvable from the Salesforce-side PrivateLink endpoint. Fix: in your VPC, enable DNS hostnames and DNS resolution, and verify the Route 53 private hosted zone is attached to the VPC. This broke a production deployment in February 2026 and took six hours to diagnose because AWS doesn't surface PrivateLink DNS failures in the endpoint's CloudWatch metrics.

Claude's context window resets on every invocation, so if your agent needs multi-turn state (e.g., "show me more details on payment #123"), you must re-query Data Cloud on every turn with the same row-level filters. Do not cache the previous query's results in Salesforce session state and reuse them, because the user's permissions might have changed between turns (they switched accounts, their role was revoked, etc.). A payments platform learned this in May 2026 when a support agent's session persisted data from a previous tenant after they were reassigned mid-shift.

Bedrock throttles at 10 requests per second per model per account by default. If you're running Agentforce for 500 concurrent users, you'll hit this limit in under a minute. Request a quota increase to 100 RPS before launch, or implement client-side request coalescing where multiple users' queries batch into a single Bedrock call with a combined context. The second approach is tricky because you must partition the response back to each user without leaking cross-tenant data in the batch. We haven't shipped a production coalescing proxy yet, so the safe answer is quota increases.

IAM policies on your Bedrock invocation role should scope bedrock:InvokeModel to the specific model ARN (arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-5-v1), not *. If you use a wildcard, a compromised proxy can invoke any model in your account, including fine-tuned models that might contain other tenants' training data. This is a theoretical risk (Bedrock doesn't expose fine-tuned model weights), but it failed a compliance review at a Series C FinTech in January 2026 and delayed their SOC 2 by two months.

When this breaks

If your FinTech platform needs real-time streaming responses from Claude (e.g., typewriter effect in the Agentforce chat UI), this proxy pattern adds 200-400ms of latency for the Data Cloud query on every turn. That latency stacks with Bedrock's time-to-first-token, and users perceive the agent as slow. The alternative: pre-materialize tenant-scoped datasets in S3, gate S3 access with IAM session tags derived from the Salesforce user, and point Bedrock Knowledge Bases at the S3 buckets. This shifts the row-level filter from query time to batch ETL time, so the agent is faster but the data is stale (typically 5-15 minutes behind Data Cloud).

If your data model has row-level access controlled by a complex graph (e.g., payment records inherit visibility from account hierarchies with role-based exceptions), Data Cloud sharing rules can't express the full policy and you'll leak data. The fix: move the access logic into a custom Apex service that wraps Data Cloud queries and enforces the graph traversal before returning records to the proxy. This worked for a multi-entity financial services platform in Q2 2026 where account hierarchies spanned 4-5 levels and role exceptions changed daily.

If you need Claude to write back to Salesforce (e.g., Agentforce creates a Case or updates a Payment record based on the conversation), this read-only proxy pattern doesn't cover write access control. You need a second proxy that validates write permissions against Data Cloud sharing rules before committing the DML, or you risk Claude writing records the user couldn't manually create. We haven't seen a production deployment solve this cleanly yet; the current workaround is to restrict Agentforce to read-only actions and require human approval for writes.

Close

The production pattern is simple: never let Claude see data the authenticated user couldn't query in Data Cloud. Enforce that with row-level sharing rules evaluated in the database, not the application, and log every invocation with enough context to prove isolation during an audit. If your FinTech platform ships this stack without that discipline, you're one prompt injection away from a breach notification.