# Multi-Tenant Agent Isolation and Security

**Last updated:** 2026-02
**Status:** GA
**Category:** Agent Orchestration & Automation

---

## Introduksjon

Multi-tenant agentarkitektur er en fundamental utfordring for organisasjoner som tilbyr AI-agenter til flere kunder, avdelinger eller forretningsenheter. Korrekt isolasjon sikrer at data, konfigurasjoner og agentoppførsel ikke lekker mellom tenants, samtidig som man opprettholder kostnadseffektivitet og skalerbarhet.

Azure-plattformen tilbyr flere isolasjonsmodeller for AI-agenter -- fra logisk isolasjon med delt infrastruktur til fullstendig fysisk separasjon med dedikerte ressurser per tenant. Valget avhenger av regulatoriske krav, dataklassifisering, ytelsesgarantier og kostnadsbetraktninger. Microsoft Entra ID utgjør fundamentet for tenant-aware tilgangskontroll, mens Azure API Management, Azure OpenAI og Foundry Agent Service tilbyr tenant-isolasjon på ulike nivåer.

For norsk offentlig sektor med strenge krav til dataisolasjon (Schrems II, NSM, Sikkerhetsloven) er det kritisk å velge riktig isolasjonsmodell. Mange offentlige virksomheter opererer med skjermingsverdig informasjon som krever sterkere isolasjonsgarantier enn standard multi-tenant-løsninger tilbyr.

## Kjernekomponenter

| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Identity Isolation | Tenant-bevisst autentisering og autorisasjon | Microsoft Entra ID, Entra Agent ID |
| Data Isolation | Separasjon av tenant-data i lagring og retrieval | Cosmos DB partitions, Azure AI Search per-tenant indexes |
| Model Isolation | Dedikerte eller delte modelldeployments | Azure OpenAI deployment models |
| Network Isolation | Nettverkssegmentering mellom tenants | Azure VNet, Private Endpoints |
| Quota Isolation | Ressurskvoter per tenant | Azure APIM rate limiting, TPM-kvoter |
| Audit Segregation | Separate revisjonslogger per tenant | Log Analytics workspaces, tenant-tagged telemetry |

## Isolasjonsmodeller for Azure OpenAI

### Oversikt

| Modell | Dataisolasjon | Ytelsesisolasjon | Kompleksitet | Kostnad |
|--------|---------------|-------------------|--------------|---------|
| Delt instans, delt deployment | Lav | Lav | Lav | Lavest |
| Delt instans, dedikert deployment per tenant | Medium | Høy | Medium | Medium |
| Dedikert instans per tenant (provider-sub) | Høy | Høy | Lav-Medium | Høy |
| Tenant-provided instans (tenant-sub) | Høy | Høy | Lav for provider | Høyest |

### Pattern 1: Logisk isolasjon med delt infrastruktur

```csharp
// Tenant-bevisst agentorkestrering med Semantic Kernel
public class MultiTenantAgentOrchestrator
{
    private readonly IKernelFactory _kernelFactory;
    private readonly ITenantResolver _tenantResolver;

    public async Task<AgentResponse> ProcessRequest(
        string userId,
        string prompt)
    {
        // Resolve tenant fra brukerens identitet
        var tenant = await _tenantResolver.ResolveTenant(userId);

        // Opprett tenant-scoped Kernel
        var kernel = _kernelFactory.CreateForTenant(tenant.Id);

        // Sett tenant-kontekst for alle operasjoner
        kernel.Data["TenantId"] = tenant.Id;
        kernel.Data["DataBoundary"] = tenant.DataBoundary;

        // Agenten arver tenant-begrensninger
        var agent = new ChatCompletionAgent
        {
            Name = "TenantScopedAgent",
            Instructions = $"""
                Du er en assistent for {tenant.Name}.
                Du har KUN tilgang til data merket med tenant {tenant.Id}.
                Aldri referer til eller avslør data fra andre tenants.
                """,
            Kernel = kernel
        };

        return await agent.InvokeAsync(prompt);
    }
}
```

### Pattern 2: Per-tenant Azure OpenAI deployments

```csharp
// Dedikert modell-deployment per tenant for ytelsesisolasjon
public class TenantDeploymentManager
{
    private readonly Dictionary<string, string> _tenantDeployments;

    public AzureChatCompletion GetServiceForTenant(string tenantId)
    {
        var deploymentName = _tenantDeployments[tenantId];

        return new AzureChatCompletion(
            deploymentName: deploymentName,  // f.eks. "gpt-4o-tenant-svv"
            endpoint: _endpoint,
            credentials: new DefaultAzureCredential()
        );
    }
}
```

### Pattern 3: Hybrid multi-tenant med Deployment Stamps

```
┌──────────────────────────────────────────────┐
│              Shared Infrastructure            │
│  ┌─────────────────────────────────────────┐  │
│  │  Azure API Management (AI Gateway)      │  │
│  │  - Tenant routing                       │  │
│  │  - Rate limiting per tenant             │  │
│  │  - Token quota enforcement              │  │
│  └───────────┬────────────┬────────────────┘  │
│              │            │                   │
│  ┌───────────▼──┐  ┌─────▼──────────┐       │
│  │ Standard     │  │  Premium        │       │
│  │ Tenants      │  │  Tenants        │       │
│  │              │  │                 │       │
│  │ Shared AOAI  │  │ Dedicated AOAI  │       │
│  │ Shared Index │  │ Per-tenant Index│       │
│  │ Shared CosmosDB│ │ Dedicated DB   │       │
│  └──────────────┘  └────────────────┘       │
└──────────────────────────────────────────────┘
```

## Tenant Data Isolation

### RAG med tenant-isolasjon

```python
# Azure AI Search med tenant-scoped retrieval
from azure.search.documents import SearchClient
from azure.identity import DefaultAzureCredential

class TenantScopedRetriever:
    def __init__(self, search_endpoint: str, index_name: str):
        self.client = SearchClient(
            endpoint=search_endpoint,
            index_name=index_name,
            credential=DefaultAzureCredential()
        )

    def search(self, query: str, tenant_id: str) -> list:
        """Søk med obligatorisk tenant-filter"""
        results = self.client.search(
            search_text=query,
            filter=f"tenant_id eq '{tenant_id}'",
            # OData-filter sikrer at BARE tenant-data returneres
            select=["content", "title", "source"],
            top=5
        )
        return [r for r in results]
```

### Cosmos DB partisjonering

```csharp
// Tenant-ID som partition key i Cosmos DB
public class AgentConversationStore
{
    private readonly Container _container;

    public async Task<ConversationThread> GetThread(
        string tenantId, string threadId)
    {
        // Partition key = tenant_id sikrer fysisk isolasjon
        var response = await _container.ReadItemAsync<ConversationThread>(
            id: threadId,
            partitionKey: new PartitionKey(tenantId)
        );
        return response.Resource;
    }

    // Cross-tenant access er umulig uten riktig partition key
}
```

## Permission Enforcement

### Microsoft Entra Agent ID

Microsoft Entra Agent ID gir agenter dedikerte identiteter med kontrollerte tilganger:

```csharp
// Opprett agent-identitet med tenant-scoped tilganger
public class AgentIdentityManager
{
    public async Task<AgentIdentity> CreateTenantAgent(
        string tenantId,
        string agentPurpose)
    {
        // Hver tenant-agent får en unik managed identity
        var identity = await _entraClient.CreateAgentIdentity(new
        {
            DisplayName = $"Agent-{agentPurpose}-{tenantId}",
            // Scoped til kun denne tenantens ressurser
            Permissions = new[]
            {
                $"data.read.tenant.{tenantId}",
                $"tools.invoke.tenant.{tenantId}"
            },
            // Kort-levd tokens for minste privilegium
            TokenLifetime = TimeSpan.FromMinutes(15)
        });

        return identity;
    }
}
```

### RBAC-modell for multi-tenant agenter

| Rolle | Tilgang | Scope |
|-------|---------|-------|
| Agent.Reader | Les tenant-data via RAG | Spesifikk tenant |
| Agent.Writer | Opprett/oppdater agentressurser | Spesifikk tenant |
| Agent.Admin | Full kontroll over agenter | Spesifikk tenant |
| Platform.Admin | Konfigurer tverrtenants infrastruktur | Plattform-nivå |
| Audit.Reader | Les revisjonslogger | Per tenant eller global |

## Audit Segregation

```python
# Tenant-segregert logging til Application Insights
from opentelemetry import trace

tracer = trace.get_tracer("multi-tenant-agent")

def process_agent_request(tenant_id: str, user_id: str, prompt: str):
    with tracer.start_as_current_span("agent_invocation") as span:
        # Tenant-kontekst på alle spans
        span.set_attribute("tenant.id", tenant_id)
        span.set_attribute("user.id", user_id)
        span.set_attribute("agent.name", "customer-support")

        # Tenant-spesifikk logging
        # KQL kan filtrere: traces | where customDimensions.tenant_id == "xxx"

        result = invoke_agent(prompt, tenant_id)

        span.set_attribute("response.token_count", result.token_count)
        span.set_attribute("response.evaluation.relevance", result.relevance)

        return result
```

### KQL for tenant-isolert rapportering

```kql
// Kostnads- og bruksrapport per tenant
traces
| where timestamp > ago(30d)
| where customDimensions.tenant_id != ""
| summarize
    total_requests = count(),
    total_tokens = sum(todouble(customDimensions["response.token_count"])),
    avg_latency = avg(duration),
    avg_relevance = avg(todouble(customDimensions["response.evaluation.relevance"]))
    by tostring(customDimensions["tenant_id"])
| order by total_tokens desc
```

## Cross-Tenant Attack Prevention

### Trusselmodell

| Trussel | Angrepsvei | Mitigering |
|---------|-----------|------------|
| Prompt injection for data-lekkasje | Bruker crafter prompt som ber om annen tenants data | Mandatory tenant-filter i alle data-queries |
| Token-sharing mellom tenants | Delt autentiseringstoken | Per-tenant managed identities |
| Side-channel via shared model | Informasjon lekker gjennom modellens kontekst | Separate deployments for sensitive tenants |
| Indirekte prompt injection | Malicious data i en tenants kunnskapsbase påvirker en annen | Strikt input-validering + Content Safety |
| Privilege escalation | Agent eskalerer tilgang utover tenant-scope | RBAC med kort-levde tokens + audit logging |

### Defence-in-depth for tenant-isolasjon

```csharp
// Multi-layer isolasjonsvalidering
public class TenantIsolationMiddleware
{
    public async Task ValidateRequest(AgentRequest request)
    {
        // Layer 1: Identity verification
        var tenantFromToken = ExtractTenantFromToken(request.AuthToken);

        // Layer 2: Request parameter validation
        if (request.TenantId != tenantFromToken)
            throw new SecurityException("Tenant mismatch");

        // Layer 3: Data access filter injection
        request.DataFilters.Add(new TenantFilter(tenantFromToken));

        // Layer 4: Response validation
        // Verifiser at responsen ikke inneholder data fra andre tenants
        request.OnResponseGenerated += async (response) =>
        {
            await ValidateResponseDoesNotLeakCrossTenantData(
                response, tenantFromToken);
        };
    }
}
```

## Resource Quotas

### APIM-basert quota enforcement

```xml
<!-- Azure API Management policy for per-tenant kvoter -->
<policies>
    <inbound>
        <set-variable name="tenantId"
            value="@(context.Request.Headers
                .GetValueOrDefault("X-Tenant-Id","unknown"))" />

        <!-- Token rate limiting per tenant -->
        <azure-openai-token-limit
            counter-key="@((string)context.Variables["tenantId"])"
            tokens-per-minute="50000"
            estimate-prompt-tokens="true" />

        <!-- Request rate limiting per tenant -->
        <rate-limit-by-key
            calls="100"
            renewal-period="60"
            counter-key="@((string)context.Variables["tenantId"])" />
    </inbound>
</policies>
```

## Norsk offentlig sektor

### Spesielle krav

| Krav | Regulering | Implementering |
|------|-----------|----------------|
| Data-suverenitet | Schrems II | Azure Norway East/West, eller Azure Local |
| Sikkerhetsgradert info | Sikkerhetsloven | Fysisk isolasjon, dedikert infrastruktur |
| Personvern | GDPR Art. 28 | Databehandleravtale per tenant, DPA |
| Logging | Arkivloven | Retensjonspolicies per tenant, minimum 5 år |
| Tilgangskontroll | eInnsyn, offentlighetsloven | Transparent tilgangslogg per tenant |

### Anbefalt isolasjonsmodell for offentlig sektor

For de fleste offentlige virksomheter i Norge anbefales **hybrid modell**:
- **Logisk isolasjon** for standard tjenester (Cosmos DB partitioning, tenant-filter i AI Search)
- **Fysisk isolasjon** for sensitiv informasjon (dedikert Azure OpenAI, VNet-isolert)
- **Azure Local** for skjermingsverdig informasjon som ikke kan forlate norsk territorium

## Beslutningsrammeverk

| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| SaaS med mange småkunder | Logisk isolasjon, delt AOAI | Kostnadseffektivt, skalerer |
| Enterprise med compliance-krav | Hybrid: delt infra + dedikerte AOAI | Balanse mellom kostnad og isolasjon |
| Offentlig sektor standard | Hybrid + Azure Norway region | Datalokalitet + kostnadseffektivitet |
| Skjermingsverdig informasjon | Full fysisk isolasjon | Regulatorisk krav, ingen kompromisser |
| Multi-region med data residency | Deployment stamps per region | Datasuverenitet per jurisdiksjon |

## For Cosmo

- **Velg isolasjonsmodell basert på dataklassifisering** -- ikke overengineer for åpne data, men underestimer aldri kravene for sensitiv informasjon. Hybrid er vanligvis riktig svar.
- **Tenant-filter i RAG er obligatorisk** -- aldri stol på at agentens instruksjoner alene hindrer datalekkasje. Implementer tekniske kontroller (OData-filter, partition keys) som ikke kan omgås.
- **Microsoft Entra Agent ID** er den anbefalte måten å gi agenter identitet med tenant-scoped tilganger -- bruk kort-levde tokens og minste privilegium.
- **APIM som AI Gateway** er essensielt for multi-tenant -- det gir token rate limiting, kostnadsfordeling og audit logging per tenant uten kodeendringer i agentene.
- **For norsk offentlig sektor**: Start med Azure Norway East, vurder Azure Local for høysikkerhet, og sørg for at databehandleravtaler er på plass per tenant.
