Sensitive Data RBAC — Design Document
This document outlines the available implementation options. Preferred choices are indicated with a ✓ symbol.
Background
include_sensitive: bool exists on GetIntentRequest and BlameConfigRequest in sdc-protos. The data-server render pipeline fully honors it — when true, redaction is skipped and plaintext values are returned. Today no caller sets it and no auth gate exists. Any process that can reach data-server's gRPC port can set the flag and receive unredacted data.
Two independent sensitivity sources compose at render time:
- Schema-level — a leaf annotated with
sdcio-ext:sensitive in a YANG overlay propagates LeafSchema.sensitive = true through schema-server
- Intent path markers —
sensitive_paths stored per-intent in the data-server cache, populated by config-server during K8s Secret resolution
Both sources are treated uniformly from an access-control perspective. There is no meaningful security boundary between them.
WatchDeviations has no include_sensitive field at all — streaming deviations are always redacted today, with no bypass mechanism.
Goals
- Allow authorized users to retrieve unredacted sensitive config values via northbound APIs
- data-server must be able to run standalone — no Kubernetes dependency, no config-server dependency
- config-server adds a Kubernetes-native authorization layer on top when running in-cluster
- Operators control which identities can request plaintext using standard, familiar tooling
- CLI tools expose the capability behind an explicit opt-in flag
Non-Goals
- Path-scoped access — granular per-YANG-path ACLs are out of scope; access is all-or-nothing
- Tier distinction — no separate roles for schema-sensitive vs. intent-sensitive data; both are treated as one sensitivity level
- Audit trail / structured access logging in data-server — deferred, possibly never needed
- Encrypt-at-rest — a separate KMS concern, unrelated to northbound visibility
Decision 1: Enforcement Architecture
Options considered
Option A — data-server only
A single interceptor in data-server validates caller identity before honoring include_sensitive. All clients, including config-server, go through one gate.
Rejected: config-server acts on behalf of K8s users whose identity lives in their bearer token, not in a cert. Propagating that identity into gRPC requires data-server to call K8s TokenReview or SubjectAccessReview, introducing a hard Kubernetes dependency into a component that is explicitly designed to run without Kubernetes.
Option B — config-server only
config-server performs a K8s SubjectAccessReview against the incoming caller's bearer token and only sets include_sensitive: true on the outbound data-server gRPC call if the SAR passes.
Rejected: sdctl talks directly to data-server gRPC, bypassing config-server entirely. This path is completely ungated unless data-server is network-isolated from sdctl — an operational constraint rather than a code contract. It cannot be relied upon as a security boundary.
Option C — both layers, defense in depth ✓
config-server performs a K8s SubjectAccessReview as its gate. data-server independently validates an mTLS client certificate. Each layer enforces autonomously. data-server remains Kubernetes-agnostic and works standalone.
Decision: Option C
kubectl-sdc → K8s API → config-server ──[SAR check]──► sets include_sensitive
│
│ gRPC + mTLS client cert (OU=sensitive-reader)
▼
data-server ──[cert OU check]──► honors include_sensitive
sdctl ────────────────────────────────── gRPC + mTLS client cert (OU=sensitive-reader)
│
▼
data-server ──[cert OU check]──► honors include_sensitive
Decision 2: Auth Mechanism at data-server
Options considered
Option: K8s bearer token forwarding
config-server extracts the caller's SA token and forwards it via gRPC metadata. data-server calls K8s TokenReview and SubjectAccessReview to validate it.
Rejected: Introduces a Kubernetes dependency in data-server, breaking standalone deployment. Token audience and TTL semantics are fragile across a service boundary. Tight coupling to config-server as the only valid intermediary.
Option: Validated-role claim forwarding
config-server performs the SAR and, if the caller is authorized, forwards a validated_role: "sensitive-reader" claim as a proto field or gRPC metadata header. data-server trusts the claim.
Rejected: The claim is only as trustworthy as the network between config-server and data-server. Any process with network access can forge the header unless the connection is already mutually authenticated — at which point the mTLS certificate itself is the proof of identity and the separate claim is redundant.
Option: mTLS client certificate + configurable OU ✓
data-server verifies the TLS client certificate chain against a configurable CA. It checks for a required OU attribute on the certificate. No external calls are needed. Works fully standalone. The CA file and required OU value are configurable at startup via environment variables, allowing operators to bring their own PKI.
Decision: mTLS with configurable CA and OU
data-server startup environment variables:
DS_SENSITIVE_TLS_ENABLED=true
DS_SENSITIVE_TLS_CLIENT_CA_FILE=/etc/sdcio/sensitive-ca.crt # PEM bundle
DS_SENSITIVE_TLS_CLIENT_OU=sensitive-reader # required OU value
Interceptor behavior:
- If
DS_SENSITIVE_TLS_ENABLED is false, include_sensitive is always treated as false.
- If the client did not present a certificate, demote
include_sensitive to false and continue — no error returned.
- If the certificate chain does not validate against
DS_SENSITIVE_TLS_CLIENT_CA_FILE, demote and continue.
- If no certificate in the chain has
OU == DS_SENSITIVE_TLS_CLIENT_OU, demote and continue.
- All checks pass → honor the
include_sensitive value from the request.
Clients without a matching certificate receive a fully redacted response. No error is returned and no information about the gate's existence is leaked.
Decision 3: config-server SAR Resource
Options considered
Option: get sensitiveconfigs
The SensitiveConfig CR already exists. No new handler would be needed.
Rejected: Semantically incorrect. get sensitiveconfigs means "read the encrypted payload CR." It does not express "reveal plaintext config values." Administrators writing RBAC policies would be confused by the mismatch between the verb and its actual effect.
Option: custom verb reveal on configs
kubectl auth can-i reveal configs expresses intent clearly.
Rejected: Custom verbs are valid K8s RBAC but are an uncommon pattern. Standard tooling, audit logs, and RBAC visualization tools handle subresources more naturally than custom verbs.
Option: get configs/sensitive subresource ✓
A standard K8s subresource. Idiomatic. Requires a new handler in config-server's aggregated API server, which is a small, contained addition.
Decision: configs/sensitive subresource
ClusterRole for sensitive readers:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: sensitive-reader
rules:
- apiGroups: ["config.sdcio.dev"]
resources: ["configs/sensitive"]
verbs: ["get"]
config-server performs a SubjectAccessReview against the incoming caller's bearer token before setting include_sensitive: true on the outbound data-server gRPC call. The resource and verb are hard-coded — they are not operator-configurable.
Decision 4: Cert Issuance Control
Options considered
Option: ClusterIssuer only, no request policy
Any principal that can create a Certificate CR can request any subject, including OU=sensitive-reader.
Rejected: Any namespace administrator would be able to obtain a sensitive-reader certificate on demand. This completely undermines the authorization model.
Option: Manual CertificateRequest approval
A cluster administrator manually patches the Approved condition on each CertificateRequest.
Rejected: Does not scale. Blocks automated certificate rotation for services. Operationally expensive.
Option: cert-manager approver-policy ✓
The CertificateRequestPolicy CRD from the cert-manager approver-policy project constrains exactly which subject attributes, usages, and issuers are permitted for a given policy. A standard K8s RBAC use binding on the policy controls which principals can trigger auto-approval.
Decision: cert-manager approver-policy
Setup:
# Policy: permits OU=sensitive-reader client auth certs only
apiVersion: policy.cert-manager.io/v1alpha1
kind: CertificateRequestPolicy
metadata:
name: sensitive-reader-policy
spec:
allowed:
subject:
organizations:
values: ["sdcio"]
organizationalUnits:
values: ["sensitive-reader"]
usages: ["client auth"]
selector:
issuerRef:
name: sdcio-internal-ca
kind: ClusterIssuer
# RBAC: controls who can use the policy
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: can-get-sensitive-cert
rules:
- apiGroups: ["policy.cert-manager.io"]
resources: ["certificaterequestpolicies"]
verbs: ["use"]
resourceNames: ["sensitive-reader-policy"]
To grant an operator access: kubectl create clusterrolebinding alice-sensitive --clusterrole=can-get-sensitive-cert --user=alice
Without this binding, any Certificate CR requesting OU=sensitive-reader is automatically denied by the policy controller, regardless of who submits it.
Certificates are issued with a 24-hour TTL and are automatically rotated by cert-manager for service accounts. Human operators obtain their certificates via kubectl sdc cert get (see CLI section below).
Decision 5: Proto Changes
Options considered
Option: Replace bool include_sensitive with a structured SensitiveAccess message
Carry an identity proof inside the proto (bearer token, certificate DER, validated role name, access scope).
Rejected: This duplicates what TLS already provides at the transport layer. If mTLS is the authentication mechanism, the certificate is already exchanged during the handshake — re-encoding it in the proto payload is redundant and expands the attack surface. A scope field for partial access was also considered but rejected because access is all-or-nothing.
Option: Keep bool include_sensitive, enforce via transport ✓
The proto remains simple. The gRPC interceptor checks the mTLS certificate and either honors or silently zeros out the boolean. Authentication is a transport-layer concern; the proto carries only the intent.
Decision: Keep bool, add to WatchDeviationRequest
All changes land in a single proto release:
WatchDeviationRequest: add bool include_sensitive = N — this field is missing today and the behavior is hardcoded to false with no bypass path
GetIntentRequest.include_sensitive: no change (field 4, already exists)
BlameConfigRequest.include_sensitive: no change (field 2, already exists)
Decision 6: Sensitivity Granularity
Options considered
Option: Two tiers — schema-sensitive vs. intent-sensitive
Separate roles for schema-annotated leaves (YANG sdcio-ext:sensitive) and operator-injected path markers (sensitive_paths on intents).
Rejected: There is no meaningful security boundary between the two. Both represent a deliberate decision by an operator that a value should not be visible northbound. Introducing separate roles adds RBAC complexity for administrators without a practical benefit.
Option: Path-scoped access
A role grants plaintext access only to specific YANG path prefixes.
Rejected: K8s RBAC has no native path-matching mechanism. Implementing it would require a custom admission webhook or OPA integration. No use case was identified that requires partial plaintext access.
Option: All-or-nothing ✓
One role. An authorized caller sees all sensitive values in plaintext. No partial access.
Decision: All-or-nothing
CLI Changes
kubectl-sdc
-
Add --include-sensitive flag to the blame and runningconfig subcommands. When set, the request targets the configs/sensitive subresource path in config-server, which performs the SAR check before forwarding include_sensitive: true to data-server.
-
Add kubectl sdc cert get subcommand with the following flow:
- Creates a
Certificate CR (24h TTL, OU=sensitive-reader) in the caller's namespace
- Waits for the
Ready condition
- Extracts
tls.crt and tls.key from the resulting Secret to local files (e.g. ~/.sdcio/certs/)
- Prints a usage hint showing the correct sdctl invocation
The subcommand requires the caller to already be bound to can-get-sensitive-cert — it is self-service but gated.
sdctl
- Add
--tls-cert and --tls-key flags to data subcommands (blame, get-intent, list-intent, etc.)
- The certificate is obtained via
kubectl sdc cert get or from the operator's own PKI
Open Questions
| # |
Question |
Status |
| 1 |
Exact K8s RBAC binding strategy — per-user, per-OIDC group, per-ServiceAccount namespace? |
Organizational/team policy decision, not a technical one |
| 2 |
Should WatchDeviations expose a privileged unredacted stream long-term, or remain always-redacted? |
Deferred — the proto field is added in this release but behavioral requirements are not yet defined |
Implementation Order
- sdc-protos — add
include_sensitive to WatchDeviationRequest, cut new release
- data-server — mTLS interceptor, env var configuration, consume new proto
- config-server —
configs/sensitive subresource handler, SAR check, consume new proto
- kubectl-sdcio —
--include-sensitive flag on blame and runningconfig, kubectl sdc cert get subcommand
- sdctl —
--tls-cert / --tls-key flags on data subcommands
- integration-tests — implement TC7 (currently skipped with a TODO tag)
Sensitive Data RBAC — Design Document
This document outlines the available implementation options. Preferred choices are indicated with a ✓ symbol.
Background
include_sensitive: boolexists onGetIntentRequestandBlameConfigRequestin sdc-protos. The data-server render pipeline fully honors it — when true, redaction is skipped and plaintext values are returned. Today no caller sets it and no auth gate exists. Any process that can reach data-server's gRPC port can set the flag and receive unredacted data.Two independent sensitivity sources compose at render time:
sdcio-ext:sensitivein a YANG overlay propagatesLeafSchema.sensitive = truethrough schema-serversensitive_pathsstored per-intent in the data-server cache, populated by config-server during K8s Secret resolutionBoth sources are treated uniformly from an access-control perspective. There is no meaningful security boundary between them.
WatchDeviationshas noinclude_sensitivefield at all — streaming deviations are always redacted today, with no bypass mechanism.Goals
Non-Goals
Decision 1: Enforcement Architecture
Options considered
Option A — data-server only
A single interceptor in data-server validates caller identity before honoring
include_sensitive. All clients, including config-server, go through one gate.Rejected: config-server acts on behalf of K8s users whose identity lives in their bearer token, not in a cert. Propagating that identity into gRPC requires data-server to call K8s TokenReview or SubjectAccessReview, introducing a hard Kubernetes dependency into a component that is explicitly designed to run without Kubernetes.
Option B — config-server only
config-server performs a K8s SubjectAccessReview against the incoming caller's bearer token and only sets
include_sensitive: trueon the outbound data-server gRPC call if the SAR passes.Rejected: sdctl talks directly to data-server gRPC, bypassing config-server entirely. This path is completely ungated unless data-server is network-isolated from sdctl — an operational constraint rather than a code contract. It cannot be relied upon as a security boundary.
Option C — both layers, defense in depth ✓
config-server performs a K8s SubjectAccessReview as its gate. data-server independently validates an mTLS client certificate. Each layer enforces autonomously. data-server remains Kubernetes-agnostic and works standalone.
Decision: Option C
Decision 2: Auth Mechanism at data-server
Options considered
Option: K8s bearer token forwarding
config-server extracts the caller's SA token and forwards it via gRPC metadata. data-server calls K8s TokenReview and SubjectAccessReview to validate it.
Rejected: Introduces a Kubernetes dependency in data-server, breaking standalone deployment. Token audience and TTL semantics are fragile across a service boundary. Tight coupling to config-server as the only valid intermediary.
Option: Validated-role claim forwarding
config-server performs the SAR and, if the caller is authorized, forwards a
validated_role: "sensitive-reader"claim as a proto field or gRPC metadata header. data-server trusts the claim.Rejected: The claim is only as trustworthy as the network between config-server and data-server. Any process with network access can forge the header unless the connection is already mutually authenticated — at which point the mTLS certificate itself is the proof of identity and the separate claim is redundant.
Option: mTLS client certificate + configurable OU ✓
data-server verifies the TLS client certificate chain against a configurable CA. It checks for a required OU attribute on the certificate. No external calls are needed. Works fully standalone. The CA file and required OU value are configurable at startup via environment variables, allowing operators to bring their own PKI.
Decision: mTLS with configurable CA and OU
data-server startup environment variables:
Interceptor behavior:
DS_SENSITIVE_TLS_ENABLEDis false,include_sensitiveis always treated as false.include_sensitiveto false and continue — no error returned.DS_SENSITIVE_TLS_CLIENT_CA_FILE, demote and continue.OU == DS_SENSITIVE_TLS_CLIENT_OU, demote and continue.include_sensitivevalue from the request.Clients without a matching certificate receive a fully redacted response. No error is returned and no information about the gate's existence is leaked.
Decision 3: config-server SAR Resource
Options considered
Option:
get sensitiveconfigsThe
SensitiveConfigCR already exists. No new handler would be needed.Rejected: Semantically incorrect.
get sensitiveconfigsmeans "read the encrypted payload CR." It does not express "reveal plaintext config values." Administrators writing RBAC policies would be confused by the mismatch between the verb and its actual effect.Option: custom verb
revealonconfigskubectl auth can-i reveal configsexpresses intent clearly.Rejected: Custom verbs are valid K8s RBAC but are an uncommon pattern. Standard tooling, audit logs, and RBAC visualization tools handle subresources more naturally than custom verbs.
Option:
get configs/sensitivesubresource ✓A standard K8s subresource. Idiomatic. Requires a new handler in config-server's aggregated API server, which is a small, contained addition.
Decision:
configs/sensitivesubresourceClusterRole for sensitive readers:
config-server performs a SubjectAccessReview against the incoming caller's bearer token before setting
include_sensitive: trueon the outbound data-server gRPC call. The resource and verb are hard-coded — they are not operator-configurable.Decision 4: Cert Issuance Control
Options considered
Option: ClusterIssuer only, no request policy
Any principal that can create a
CertificateCR can request any subject, includingOU=sensitive-reader.Rejected: Any namespace administrator would be able to obtain a sensitive-reader certificate on demand. This completely undermines the authorization model.
Option: Manual CertificateRequest approval
A cluster administrator manually patches the
Approvedcondition on eachCertificateRequest.Rejected: Does not scale. Blocks automated certificate rotation for services. Operationally expensive.
Option: cert-manager approver-policy ✓
The
CertificateRequestPolicyCRD from the cert-manager approver-policy project constrains exactly which subject attributes, usages, and issuers are permitted for a given policy. A standard K8s RBACusebinding on the policy controls which principals can trigger auto-approval.Decision: cert-manager approver-policy
Setup:
To grant an operator access:
kubectl create clusterrolebinding alice-sensitive --clusterrole=can-get-sensitive-cert --user=aliceWithout this binding, any
CertificateCR requestingOU=sensitive-readeris automatically denied by the policy controller, regardless of who submits it.Certificates are issued with a 24-hour TTL and are automatically rotated by cert-manager for service accounts. Human operators obtain their certificates via
kubectl sdc cert get(see CLI section below).Decision 5: Proto Changes
Options considered
Option: Replace
bool include_sensitivewith a structuredSensitiveAccessmessageCarry an identity proof inside the proto (bearer token, certificate DER, validated role name, access scope).
Rejected: This duplicates what TLS already provides at the transport layer. If mTLS is the authentication mechanism, the certificate is already exchanged during the handshake — re-encoding it in the proto payload is redundant and expands the attack surface. A
scopefield for partial access was also considered but rejected because access is all-or-nothing.Option: Keep
bool include_sensitive, enforce via transport ✓The proto remains simple. The gRPC interceptor checks the mTLS certificate and either honors or silently zeros out the boolean. Authentication is a transport-layer concern; the proto carries only the intent.
Decision: Keep bool, add to WatchDeviationRequest
All changes land in a single proto release:
WatchDeviationRequest: addbool include_sensitive = N— this field is missing today and the behavior is hardcoded to false with no bypass pathGetIntentRequest.include_sensitive: no change (field 4, already exists)BlameConfigRequest.include_sensitive: no change (field 2, already exists)Decision 6: Sensitivity Granularity
Options considered
Option: Two tiers — schema-sensitive vs. intent-sensitive
Separate roles for schema-annotated leaves (YANG
sdcio-ext:sensitive) and operator-injected path markers (sensitive_pathson intents).Rejected: There is no meaningful security boundary between the two. Both represent a deliberate decision by an operator that a value should not be visible northbound. Introducing separate roles adds RBAC complexity for administrators without a practical benefit.
Option: Path-scoped access
A role grants plaintext access only to specific YANG path prefixes.
Rejected: K8s RBAC has no native path-matching mechanism. Implementing it would require a custom admission webhook or OPA integration. No use case was identified that requires partial plaintext access.
Option: All-or-nothing ✓
One role. An authorized caller sees all sensitive values in plaintext. No partial access.
Decision: All-or-nothing
CLI Changes
kubectl-sdc
Add
--include-sensitiveflag to theblameandrunningconfigsubcommands. When set, the request targets theconfigs/sensitivesubresource path in config-server, which performs the SAR check before forwardinginclude_sensitive: trueto data-server.Add
kubectl sdc cert getsubcommand with the following flow:CertificateCR (24h TTL,OU=sensitive-reader) in the caller's namespaceReadyconditiontls.crtandtls.keyfrom the resulting Secret to local files (e.g.~/.sdcio/certs/)The subcommand requires the caller to already be bound to
can-get-sensitive-cert— it is self-service but gated.sdctl
--tls-certand--tls-keyflags to data subcommands (blame,get-intent,list-intent, etc.)kubectl sdc cert getor from the operator's own PKIOpen Questions
WatchDeviationsexpose a privileged unredacted stream long-term, or remain always-redacted?Implementation Order
include_sensitivetoWatchDeviationRequest, cut new releaseconfigs/sensitivesubresource handler, SAR check, consume new proto--include-sensitiveflag onblameandrunningconfig,kubectl sdc cert getsubcommand--tls-cert/--tls-keyflags on data subcommands