Skip to content

Add a gRPC interceptor that seals and opens payloads - #267

Open
pseudomuto wants to merge 1 commit into
mainfrom
encryption-interceptor
Open

Add a gRPC interceptor that seals and opens payloads#267
pseudomuto wants to merge 1 commit into
mainfrom
encryption-interceptor

Conversation

@pseudomuto

Copy link
Copy Markdown
Contributor

The vault knows how to seal and open payloads, but nothing in the request path used it. This adds a client interceptor that seals the payloads of every outbound request and opens the payloads of every inbound response, including those carried in gRPC error details. Search attributes are skipped in both directions because the server indexes and queries them, and a sealed one would be useless.

Enabled and Vault are separate knobs. A vault without Enabled only opens, so switching encryption off does not strand data that was sealed while it was on, and payloads that were never sealed pass through untouched so mixed traffic is fine. Only sealing needs a namespace: a sealed message names the KEK that wrapped its DEK, so the two directions resolve it differently.

NamespaceKey says where on the request context to find the namespace whose key should seal a payload. Nothing stamps that value yet, so an enabled Encryptor currently fails any request it would have to seal. That is deliberate. The namespace picks the KEK, and defaulting it would encrypt a tenant's data under a key that is not theirs, which is the quiet version of the failure this is meant to prevent. Reading the namespace off the incoming request comes next. Practically, this would fall back to the default key, but that's an implementation detail of the Vault rather than something outlined here.

NB: This isn't wired up yet. The payload visitor is unary only, so replication streams are not covered by it.

@pseudomuto
pseudomuto requested a review from a team as a code owner August 31, 2026 14:53
The vault knows how to seal and open payloads, but nothing sat in the
request path to use it. This adds a client interceptor that does: it
seals the payloads of every outbound request and opens the ones on
every inbound response, including the payloads carried in gRPC error
details. Search attributes are skipped in both directions, since the
server indexes and queries them and a sealed one would be useless.

Enabled and Vault are separate knobs. A vault without Enabled only
opens, so switching encryption off does not strand data that was sealed
while it was on, and payloads that were never sealed pass through
untouched so mixed traffic is fine. Only sealing needs a namespace: a
sealed message names the KEK that wrapped its DEK, so the two
directions resolve it differently.

NamespaceKey says where on the request context to find the namespace
whose key should seal a payload. Nothing stamps that value yet, so an
enabled Encryptor currently fails any request it would have to seal.
That is deliberate. The namespace picks the KEK, and defaulting it
would encrypt a tenant's data under a key that is not theirs, which is
the quiet version of the failure this is meant to prevent. Reading the
namespace off the incoming request comes next. Practically, this would
fallback to the default key, but that's an implementation detail of the
Vault rather than something outlined here.

NB: This isn't wired up yet. The payload visitor is unary only, so
replication streams are not covered by it.
@pseudomuto
pseudomuto force-pushed the encryption-interceptor branch from 6bc0a97 to 8214ed7 Compare September 1, 2026 14:10
Comment thread interceptor/encryptor.go
}

unary, err := proxy.NewPayloadVisitorInterceptor(proxy.PayloadVisitorInterceptorOptions{
Inbound: visitPayloads(in, codec.Chain.Decode, noNamespace),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so incoming payload to proxy is encrypted? what does noNamespace mean here?

Comment thread interceptor/encryptor.go
// Required when Enabled and unused otherwise, since only sealing needs a
// namespace. There is no default: the namespace picks the KEK, so a
// guess means encrypting a tenant's data under a key that is not theirs.
NamespaceKey any

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why type is any?

Comment thread interceptor/encryptor.go

return &proxy.VisitPayloadsOptions{
ConcurrencyLimit: runtime.NumCPU(),
SkipSearchAttributes: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the implication to skip SA? does it mean SA value won't be encrypted?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants