Konnektr Logo
Advanced Topics

Security

Security features, TrackLastUpdatedBy, database protection, and best practices.

This guide covers the security features available in Konnektr Graph, including user tracking, database protection, transport security, and operational best practices.


TrackLastUpdatedBy

The TrackLastUpdatedBy feature records which user last modified a digital twin. When enabled, the user ID is stored at two levels in the twin's $metadata, analogous to $lastUpdateTime / lastUpdateTime.

How It Works

When TrackLastUpdatedBy is enabled:

  1. Write operations (CreateOrReplaceDigitalTwin, UpdateDigitalTwin) accept a userId parameter
  2. The SDK sets $metadata.$lastUpdatedBy to the user who last updated any property on the twin
  3. For each property being written or patched, the SDK also sets $metadata.<propertyName>.lastUpdatedBy to the user who last updated that specific property
  4. The metadata is stored as part of the twin's JSON document in the AGE graph

For example, a twin with Temperature and Humidity properties would have:

{
  "$dtId": "room-001",
  "$metadata": {
    "$model": "dtmi:example:Room;1",
    "$lastUpdateTime": "2026-06-17T12:00:00Z",
    "$lastUpdatedBy": "user-abc",
    "Temperature": {
      "lastUpdateTime": "2026-06-17T12:00:00Z",
      "lastUpdatedBy": "user-abc"
    },
    "Humidity": {
      "lastUpdateTime": "2026-06-17T12:00:00Z",
      "lastUpdatedBy": "user-abc"
    }
  },
  "Temperature": 22.5,
  "Humidity": 45
}

Azure SDK Compatibility

When TrackLastUpdatedBy is enabled, the Azure Digital Twins SDK's BasicDigitalTwin and DigitalTwinMetadata types cannot deserialize twin responses. The Azure SDK's DigitalTwinMetadataJsonConverter throws on any unrecognized property within $metadata.

If you use the Azure SDK with TrackLastUpdatedBy enabled, use one of these workarounds:

  • Use JsonDocument or JsonObject as the response type (e.g., GetDigitalTwinAsync<JsonDocument>) to access the raw JSON
  • Use custom DTOs that don't parse the $metadata block
  • The native AgeDigitalTwinsClient SDK has full support for $lastUpdatedBy in $metadata

Configuration

API Service (appsettings.json or environment variable):

{
  "Parameters": {
    "TrackLastUpdatedBy": true
  }
}

Environment variable: Parameters__TrackLastUpdatedBy=true

How the User ID Is Determined

When TrackLastUpdatedBy is enabled and a request reaches the API Service, the user ID is extracted from the JWT token claims via RequestHelper.ParseUserId():

  1. First tries ClaimTypes.NameIdentifier (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier)
  2. Falls back to the sub claim

Returns null if the user is unauthenticated or no suitable claim exists (in which case lastUpdatedBy is omitted).

Events Pipeline Integration

In the Events Service, when TrackLastUpdatedBy is enabled:

  • The SharedEventConsumer passes trackLastUpdatedBy: true to CloudEventFactory.CreateDataHistoryEvents()
  • The factory reads $metadata.$lastUpdatedBy from the twin's new value and adds an updatedBy field to the CloudEvent body

Kusto Sink: When TrackLastUpdatedBy is enabled, the KustoEventSink adds an UpdatedBy column mapping to the AdtPropertyEvents table. This ensures updatedBy values are ingested into Kusto for historical querying.

Enabling TrackLastUpdatedBy

To enable end-to-end tracking:

  1. API Service: Set Parameters:TrackLastUpdatedBy: true
  2. Events Service: Set Parameters:TrackLastUpdatedBy: true (so updatedBy fields appear in CloudEvents)
  3. Ensure authentication is enabled so the JWT contains a user identifier claim

Azure SDK Note

When TrackLastUpdatedBy is enabled, the Azure Digital Twins SDK's BasicDigitalTwin and DigitalTwinMetadata types cannot deserialize twin responses because they don't recognize $lastUpdatedBy inside $metadata. Use JsonDocument, JsonObject, or custom DTOs as workarounds. See Using Azure Digital Twins SDKs for details.

Privacy Considerations

  • The $lastUpdatedBy value comes from the JWT sub or nameidentifier claim — typically an opaque user or service account ID
  • Consider whether your use case requires storing user identifiers in twin metadata for compliance or auditing purposes
  • This feature is opt-in and disabled by default

Database Protection Middleware

The API Service includes a middleware layer that protects the database from overload caused by excessive or misbehaving clients.

Concurrent Request Limit

Limits the number of simultaneous requests per user (or per IP if unauthenticated).

ConfigurationDefaultDescription
Parameters:MaxConcurrentRequestsPerUser20Returns HTTP 429 when exceeded

Query Complexity Limit

Tracks a sliding-window complexity score per user specifically for /query endpoint requests.

ConfigurationDefaultDescription
Parameters:MaxQueryComplexityPerWindow1000Query units allowed per window
Parameters:BaseQueryComplexity10Units added per query request
Parameters:QueryComplexityWindowMinutes1Window duration in minutes

Slow Request Detection

Logs warnings when database operations exceed a threshold, helpful for identifying problematic queries.

ConfigurationDefaultDescription
Parameters:SlowRequestThresholdMs5000Logs Warning when exceeded

Cleanup

Per-user metrics are retained for a configurable period and cleaned up periodically.

ConfigurationDefaultDescription
Parameters:MetricsRetentionMinutes10Minutes before inactive user metrics are evicted

Enable / Disable

The database protection middleware only runs when rate limiting is enabled:

{
  "Parameters": {
    "RateLimitingEnabled": true
  }
}

Rate Limiting

Rate limiting protects the API from abuse and ensures fair resource distribution. See Performance for detailed rate limit policy configurations.

Key security aspects of the rate limiter:

  • Per-user/IP partitioning — limits are applied independently to each client
  • Rejection response — HTTP 429 with Retry-After header and JSON error body
  • Queue-based overflow — requests beyond the limit are queued (up to a configurable limit) rather than immediately rejected, preventing thundering herd problems

Transport Security

TLS / HTTPS

Konnektr Graph does not terminate TLS itself. In production:

  • TLS termination: Terminate TLS at your ingress gateway (Kubernetes Ingress, Azure Application Gateway, nginx, etc.)
  • Internal communication between the API Service and PostgreSQL should use TLS if they are not on the same network

Database Connection Security

  • Connection strings support all standard PostgreSQL SSL modes (sslmode=require, sslmode=verify-full, etc.)
  • For cloud deployments, use private networking (VNet, peering) between services and the database

User ID Header Delegation (Proxy Scenario)

When Konnektr Graph is deployed behind an authenticating proxy (such as a reverse proxy that validates Azure B2C tokens and then re-authenticates to the API service using a managed identity), the ClaimsPrincipal always represents the managed identity — never the original end user. This breaks TrackLastUpdatedBy (which records the managed identity ID instead of the actual user) and authorization lookups (which check the managed identity's permissions).

To solve this, Konnektr Graph supports delegating the user identity via an HTTP header.

How It Works

The proxy validates the end-user's token (e.g., an Azure B2C token), extracts the user ID, and injects it into a configurable HTTP header (e.g., X-User-Id). The API service's UserIdHeaderMiddleware reads this header and replaces the effective identity for the remainder of the request.

The middleware:

  1. Runs after UseAuthentication() and before UseAuthorization() in the ASP.NET Core pipeline
  2. Reads the configured header name from Parameters:UserIdHeaderName
  3. Creates a synthetic ClaimsIdentity carrying the header value as sub, NameIdentifier, and Name
  4. Copies relevant claims (permissions, roles, scp) from the original managed identity into the synthetic identity
  5. Prepends the synthetic identity to the ClaimsPrincipal — making it the primary identity for all downstream consumers

Configuration

API Service (appsettings.json):

{
  "Parameters": {
    "UserIdHeaderName": "X-User-Id",
    "UserIdHeaderRequired": false
  }
}
KeyDefaultDescription
Parameters:UserIdHeaderName"" (disabled)HTTP header name to read. Empty string disables the middleware entirely.
Parameters:UserIdHeaderRequiredfalseWhen true, requests missing the header receive 401 Unauthorized immediately. When false, the request falls back to the managed identity's claims.

Effect on Security Features

FeatureWithout headerWith header
TrackLastUpdatedByRecords the managed identity IDRecords the B2C user ID from the header
ClaimsPermissionProviderChecks managed identity's permission claimsManaged identity's permission claims are preserved and still checked
ApiPermissionProviderLooks up permissions by managed identity subLooks up permissions by the B2C user ID from the header
Rate limitingPartitioned by managed identity subPartitioned by the B2C user ID
Database protectionKeyed by managed identity nameKeyed by the B2C user ID

Best Practices

  1. Enable authentication — The managed identity token should be validated first; the middleware enriches rather than replaces the authentication layer
  2. Use UserIdHeaderRequired: true when the proxy is always present — This ensures requests cannot bypass the proxy and reach the API service without a delegated user ID
  3. Prefer Api authorization provider — The managed identity typically doesn't carry individual user permissions in its claims. Configure Authorization:Provider: Api to look up permissions by the delegated user ID from an external service
  4. Secure the header — Ensure the proxy strips or overwrites any X-User-Id header from incoming client requests so end users cannot impersonate others

Example: Proxy Configuration

End User (B2C token)
    │
    ▼
Authenticating Proxy
  - Validates B2C token
  - Extracts B2C user ID (e.g., "user-abc-123")
  - Authenticates to API with managed identity token
  - Injects X-User-Id: user-abc-123 header
    │
    ▼
Konnektr Graph API Service
  - Validates managed identity JWT (UseAuthentication)
  - UserIdHeaderMiddleware reads X-User-Id
  - Creates synthetic identity with sub=user-abc-123
  - UseAuthorization checks permissions for user-abc-123
  - Twin writes record lastUpdatedBy=user-abc-123

Authentication & Authorization

See the dedicated Authentication & Authorization guide for detailed configuration of:

  • JWT Bearer token validation
  • Permission-based authorization (Claims provider vs API provider)
  • Integration with Auth0, Azure AD, Keycloak, Okta
  • Error responses (401, 403)

Input Validation

DTDL Model Validation

All DTDL models are validated during upload using the open-source DTDLParser. This ensures:

  • Valid DTDL syntax (v2 through v4 supported)
  • Schema conformance
  • Reference integrity within model dependencies

Digital Twin Validation

Twins are validated when created or replaced:

  • Required properties are enforced
  • Property types must match the DTDL model definition
  • Component references must exist

JSON Patch Validation

JSON Patch operations for twin and component updates are validated before application, including:

  • Path existence checking
  • Value type validation against the DTDL model
  • Referential integrity for component updates

Security Best Practices

Production Deployment

  1. Enable authentication — Set Authentication:Enabled: true and configure a proper OAuth provider
  2. Enable authorization — Set Authorization:Enabled: true and assign least-privilege permissions
  3. Use strong connection secrets — Rotate database credentials regularly
  4. Keep connection strings out of source control — Use environment variables, secrets managers, or Azure Key Vault

Network Security

  1. Deploy PostgreSQL in a private network (not publicly accessible)
  2. Use Kubernetes NetworkPolicies to restrict pod-to-pod communication
  3. If direct database access is needed, use a bastion or proxy

Monitoring

  1. Enable OpenTelemetry — Set OTEL_EXPORTER_OTLP_ENDPOINT to forward traces and metrics
  2. Monitor rate limit rejections — HTTP 429 responses indicate potential abuse or misconfiguration
  3. Watch for slow queries — The database protection middleware logs warnings for requests exceeding SlowRequestThresholdMs
  4. Audit authentication logs — Log levels Microsoft.AspNetCore.Authentication:Debug provide JWT validation details

Operational Security

  1. Rotate secrets — Client secrets, API keys, and database passwords should be rotated regularly
  2. Use short-lived tokens — Configure OAuth token expiry to 1 hour or less
  3. Separate read/write permissions — Use granular permissions (digitaltwins/read, digitaltwins/write, digitaltwins/delete) rather than wildcards
  4. Use service accounts — Dedicated credentials for automated integrations

Security Event Monitoring

TrackLastUpdatedBy for Audit

With TrackLastUpdatedBy enabled, you can trace every twin change to the user or service account that made it. Combined with DataHistory event routing to Kusto, this provides a complete audit trail:

Which user changed which twin, at what time

Dead Letter Queue

The Events Service writes failed event deliveries to a dead letter queue table (digitaltwins_eventing.dead_letter_queue). Monitor this table for:

  • Sink connectivity issues (network, credentials)
  • Schema mismatches (e.g., Kusto table column changes)
  • Event format errors

Health Checks

Both services expose health check endpoints that can be integrated with monitoring systems:

EndpointServicePurpose
GET /healthBothFull readiness check (including DB connection)
GET /aliveBothLiveness check (process is running)
GET /healthEventsIncludes replication health and event sink connectivity

On this page