ext

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package ext is a starting point for building a temporal-proxy extension server.

The proxy delegates two decisions to servers the operator runs: whether an inbound caller may proceed (api.auth.v1.AuthService) and how a data encryption key is wrapped (api.kms.v1.EncryptionService). Implement Auth, KMS, or both and hand them to Serve for the listener, signal handling, and graceful shutdown. Registering the generated services by hand works as well; nothing here is privileged. Both are registered either way, and an unsupplied one answers Unimplemented rather than refusing the connection.

Two unrelated authentication decisions meet here and are easy to conflate. Auth is the proxy asking about a worker that connected to the gateway; WithServerAuth is about the caller of this server, which is the proxy. Answering the first does not imply enforcing the second, and a server that skips the second admits anyone who finds its port.

The proxy fails closed, so any error from either service denies the request. Return a google.golang.org/grpc/status error: the code tells the proxy whether the caller can fix this or should retry, and a plain error arrives as Unknown.

Serve listens in plaintext unless WithServerOption supplies credentials. Both are supported, but the ends must agree, since the proxy dials plaintext when the extension server's TLS block is absent.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Serve

func Serve(ctx context.Context, opts ...Option) error

Serve runs an extension server until ctx is cancelled or the process is signalled, then shuts down and returns nil. A non-nil return means the server never started, never that a caller was turned away.

Both generated services are registered whether or not WithAuth and WithKMS were given, and one left unset answers Unimplemented. Defaults are :8900 on every interface, a five second shutdown grace period, and the transport and limits WithServerOption describes. Serve blocks and installs its own handler for [signals], so a caller with no lifecycle of its own can pass context.Background.

Types

type Auth

type Auth interface {
	Authenticate(context.Context, []*auth.CallerCredential) error
}

Auth decides whether an inbound caller of the proxy may proceed. Register an implementation with WithAuth.

The credentials are what the proxy lifted from the caller's stream, one entry per configured credential header the caller actually sent, so an empty slice means it presented none. The proxy's own credential to this server is not among them and stays in the request metadata, alongside the caller's other metadata.

Return nil to admit and an error to deny, and make it a google.golang.org/grpc/status error: the proxy keeps the code and discards the message, which it assumes was written for whoever operates this server. Implementations must be safe for concurrent use and must not block indefinitely, since a caller is waiting and the proxy denies on timeout.

type CredentialCheck

type CredentialCheck func(string) bool

CredentialCheck reports whether a credential presented to this server is valid. WithServerAuth installs it and describes the checks made first.

type KMS

type KMS interface {
	Wrap(context.Context, string, []byte) ([]byte, error)
	Unwrap(context.Context, []byte) ([]byte, error)
}

KMS wraps and unwraps the proxy's data encryption keys. Register an implementation with WithKMS.

Only key material crosses the wire. The plaintext handed to Wrap is a DEK, never a payload, so an implementation is free to make each call a round trip to an HSM; the proxy caches the DEK and does the bulk encryption itself.

Wrap receives the namespace, so an implementation may hold a distinct key per namespace. Unwrap does not: it gets only the ciphertext, so whatever identifies the key has to be inside what Wrap returned, usually an opaque header framed around it. That makes Unwrap's input a durable format worth versioning, and retiring a key destroys every payload it wrapped.

A google.golang.org/grpc/status error is passed through with its code intact, so an implementation that can tell a bad ciphertext from an unreachable backend can say which it was; any other error takes the code documented on the method that called it.

Implementations must be safe for concurrent use.

type Option

type Option func(*options)

Option configures the server built by Serve. The set is closed: options is unexported, so a caller cannot name it.

func WithAddr

func WithAddr(hostPort string) Option

WithAddr sets the address to listen on, defaulting to :8900 on every interface. Narrow it to loopback where the proxy reaches this server over one: an extension server can admit callers and unwrap key material, so it should not be published needlessly. Ignored when WithListener supplies a listener.

func WithAuth

func WithAuth(a Auth) Option

WithAuth registers an implementation of api.auth.v1.AuthService. Without it the service answers Unimplemented, and since the proxy fails closed, that denies every caller rather than admitting them.

func WithKMS

func WithKMS(kms KMS) Option

WithKMS registers an implementation of api.kms.v1.EncryptionService. Without it the service answers Unimplemented, failing every Seal and, more consequentially, every Open of a payload already sealed under it.

func WithListener

func WithListener(lis net.Listener) Option

WithListener serves on lis instead of a listener opened from WithAddr. Use it when something else owns the socket: a test that would rather not bind a port, or a process handed one by its supervisor. The server closes lis during shutdown, so a caller must not.

func WithLogger

func WithLogger(l logger.Logger) Option

WithLogger sets the logger for the server's lifecycle, defaulting to logger.Default. Handlers are not given it. A nil logger is ignored rather than installed, matching logger.SetDefault; the alternative is a panic on the first line the server logs.

func WithServerAuth

func WithServerAuth(header string, check CredentialCheck) Option

WithServerAuth guards this server's unary methods with check, which receives the single value of the header metadata. It authenticates the proxy to this server, unlike the Auth service, which is how the proxy asks about somebody else. Unary covers the whole surface today, since both generated services are unary; adding a streaming method to either proto means pairing this with a grpc.StreamServerInterceptor.

A call is rejected with Unauthenticated unless the header is present exactly once and check accepts its value. Repeats are refused rather than searched, so a caller cannot spray guesses in one call. Compare in constant time (crypto/subtle.ConstantTimeCompare) for a shared secret. A nil check installs nothing and leaves the server open, while a non-nil one with an empty header would reject everything, so Serve treats it as a configuration error and declines to start.

func WithServerOption

func WithServerOption(opts ...grpc.ServerOption) Option

WithServerOption adds gRPC server options, and is the escape hatch for anything this package does not name: TLS via grpc.Creds, a stats handler, a raised limit.

Options accumulate, and Serve starts with insecure credentials, 128 concurrent streams, a 1MiB receive limit sized for key material rather than payloads, and keepalive settings that floor client ping intervals. An option given here is applied after those, so it wins for any setting gRPC resolves to one value: serving TLS is grpc.Creds(credentials.NewTLS(...)), with nothing to clear first.

Interceptor order is visible. The guard WithServerAuth installs is chained ahead of anything added here, so a grpc.ChainUnaryInterceptor sees only admitted calls. grpc.UnaryInterceptor is gRPC's own exception: prepended ahead of the whole chain, it observes calls about to be rejected.

func WithShutdownTimeout

func WithShutdownTimeout(t time.Duration) Option

WithShutdownTimeout bounds how long shutdown waits for in-flight calls before dropping connections. It defaults to five seconds and is clamped to a 50ms floor, so a zero or negative value still lets an already-answered call flush. Set it below the grace period of whatever supervises the process; overrunning that trades the graceful shutdown for a SIGKILL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL