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:
- Write operations (
CreateOrReplaceDigitalTwin,UpdateDigitalTwin) accept auserIdparameter - The SDK sets
$metadata.$lastUpdatedByto the user who last updated any property on the twin - For each property being written or patched, the SDK also sets
$metadata.<propertyName>.lastUpdatedByto the user who last updated that specific property - 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
JsonDocumentorJsonObjectas the response type (e.g.,GetDigitalTwinAsync<JsonDocument>) to access the raw JSON - Use custom DTOs that don't parse the
$metadatablock - The native
AgeDigitalTwinsClientSDK has full support for$lastUpdatedByin$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():
- First tries
ClaimTypes.NameIdentifier(http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier) - Falls back to the
subclaim
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
SharedEventConsumerpassestrackLastUpdatedBy: truetoCloudEventFactory.CreateDataHistoryEvents() - The factory reads
$metadata.$lastUpdatedByfrom the twin's new value and adds anupdatedByfield 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:
- API Service: Set
Parameters:TrackLastUpdatedBy: true - Events Service: Set
Parameters:TrackLastUpdatedBy: true(soupdatedByfields appear in CloudEvents) - 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
$lastUpdatedByvalue comes from the JWTsubornameidentifierclaim — 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).
| Configuration | Default | Description |
|---|---|---|
Parameters:MaxConcurrentRequestsPerUser | 20 | Returns HTTP 429 when exceeded |
Query Complexity Limit
Tracks a sliding-window complexity score per user specifically for /query endpoint requests.
| Configuration | Default | Description |
|---|---|---|
Parameters:MaxQueryComplexityPerWindow | 1000 | Query units allowed per window |
Parameters:BaseQueryComplexity | 10 | Units added per query request |
Parameters:QueryComplexityWindowMinutes | 1 | Window duration in minutes |
Slow Request Detection
Logs warnings when database operations exceed a threshold, helpful for identifying problematic queries.
| Configuration | Default | Description |
|---|---|---|
Parameters:SlowRequestThresholdMs | 5000 | Logs Warning when exceeded |
Cleanup
Per-user metrics are retained for a configurable period and cleaned up periodically.
| Configuration | Default | Description |
|---|---|---|
Parameters:MetricsRetentionMinutes | 10 | Minutes 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-Afterheader 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:
- Runs after
UseAuthentication()and beforeUseAuthorization()in the ASP.NET Core pipeline - Reads the configured header name from
Parameters:UserIdHeaderName - Creates a synthetic
ClaimsIdentitycarrying the header value assub,NameIdentifier, andName - Copies relevant claims (
permissions,roles,scp) from the original managed identity into the synthetic identity - 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
}
}| Key | Default | Description |
|---|---|---|
Parameters:UserIdHeaderName | "" (disabled) | HTTP header name to read. Empty string disables the middleware entirely. |
Parameters:UserIdHeaderRequired | false | When 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
| Feature | Without header | With header |
|---|---|---|
TrackLastUpdatedBy | Records the managed identity ID | Records the B2C user ID from the header |
ClaimsPermissionProvider | Checks managed identity's permission claims | Managed identity's permission claims are preserved and still checked |
ApiPermissionProvider | Looks up permissions by managed identity sub | Looks up permissions by the B2C user ID from the header |
| Rate limiting | Partitioned by managed identity sub | Partitioned by the B2C user ID |
| Database protection | Keyed by managed identity name | Keyed by the B2C user ID |
Best Practices
- Enable authentication — The managed identity token should be validated first; the middleware enriches rather than replaces the authentication layer
- Use
UserIdHeaderRequired: truewhen the proxy is always present — This ensures requests cannot bypass the proxy and reach the API service without a delegated user ID - Prefer
Apiauthorization provider — The managed identity typically doesn't carry individual user permissions in its claims. ConfigureAuthorization:Provider: Apito look up permissions by the delegated user ID from an external service - Secure the header — Ensure the proxy strips or overwrites any
X-User-Idheader 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-123Authentication & 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
- Enable authentication — Set
Authentication:Enabled: trueand configure a proper OAuth provider - Enable authorization — Set
Authorization:Enabled: trueand assign least-privilege permissions - Use strong connection secrets — Rotate database credentials regularly
- Keep connection strings out of source control — Use environment variables, secrets managers, or Azure Key Vault
Network Security
- Deploy PostgreSQL in a private network (not publicly accessible)
- Use Kubernetes NetworkPolicies to restrict pod-to-pod communication
- If direct database access is needed, use a bastion or proxy
Monitoring
- Enable OpenTelemetry — Set
OTEL_EXPORTER_OTLP_ENDPOINTto forward traces and metrics - Monitor rate limit rejections — HTTP 429 responses indicate potential abuse or misconfiguration
- Watch for slow queries — The database protection middleware logs warnings for requests exceeding
SlowRequestThresholdMs - Audit authentication logs — Log levels
Microsoft.AspNetCore.Authentication:Debugprovide JWT validation details
Operational Security
- Rotate secrets — Client secrets, API keys, and database passwords should be rotated regularly
- Use short-lived tokens — Configure OAuth token expiry to 1 hour or less
- Separate read/write permissions — Use granular permissions (
digitaltwins/read,digitaltwins/write,digitaltwins/delete) rather than wildcards - 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 timeDead 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:
| Endpoint | Service | Purpose |
|---|---|---|
GET /health | Both | Full readiness check (including DB connection) |
GET /alive | Both | Liveness check (process is running) |
GET /health | Events | Includes replication health and event sink connectivity |