kinit, their OS mints a ticket, and the Sidecar carries it to the
database untouched. Policy, masking and the audit trail behave as they do
on a password session.
For the commands, start at Get Started.
For every config field, see the Config File reference.
Why a relay can sit in the middle
A Kerberos service ticket names a service. The machine that answers stays out of it. Sodb.corp.example can resolve to your Envoy. The client dials that name and
asks the KDC for a ticket naming that service. The KDC encrypts the ticket with
the database’s key, so Envoy and the Sidecar carry bytes neither can read, and
the database decrypts what it would have decrypted anyway.
The name the client dials has to match the SPN the database holds. The two
protocols build that name differently:
Point that DNS name at Envoy and register the SPN against the database’s service
account. The rest of Kerberos stays as it was.
MSSQL
TDS gives the authentication exchange its own packet types, which is what lets the Sidecar forward a credential it cannot interpret.
Inspection begins at the first
0x01 or 0x03. The protocol’s own message
typing draws that line, so no heuristic guesses where the credential ends.
The client leg needs TDS 8.0
TDS 8.0 runs TCP, then TLS, then the protocol. The handshake is ordinary TLS-on-connect, so Envoy terminates it with a plainDownstreamTlsContext and no
TDS awareness:
Encrypt=strict.
TDS 7.x encrypts the login and nothing else
A 7.x client wraps its TLS handshake inside0x12 packets, which Envoy cannot
speak, so no terminator sits in front of that lane. The Sidecar takes the
connection directly and reads it anyway, because of what PRELOGIN negotiated.
ENCRYPT_OFF says “encryption off” and means “encrypt the login only”. MS-TDS
3.2.5.3 puts the first LOGIN7 packet inside TLS and leaves every other packet
in the clear. A SQL Server with encryption administratively disabled still does
this: with no certificate installed it mints a self-signed one at startup and
encrypts credentials with that. go-mssqldb sends ENCRYPT_OFF by default, so
this is the ordinary 7.x case rather than an exotic one.
The Sidecar walks that encrypted region by TLS record framing, resumes on the
byte where plaintext returns, and inspects every statement after it. Those
statements carry mssql.login_encrypted in the audit trail, so an operator
reading the trail sees the window nobody could observe.
Verified against SQL Server 2017, 2019 and 2022 in
deploy/docker-compose/envoy-stack/mssql2019/.
The Sidecar refuses a routing redirect
A login response can carry a routing ENVCHANGE, telling the driver to reconnect elsewhere. AlwaysOn and Azure both use it, and drivers obey without telling the user. Forward it and the client lands on a socket the Sidecar has no hold on, where the session continues with no policy, no masking and no audit, leaving no trace that it stopped being watched. The Sidecar refuses it and ends the connection with a message naming the redirect target. No rule enables this behaviour and none can switch it off.The principal stays anonymous
Under integrated authentication, LOGIN7’s username field is empty and the name lives inside the encrypted ticket. Reading it would mean implementing Kerberos. Audit rows on an MSSQL lane recordprincipal: anonymous. To name the actor,
populate it from whatever terminated the client’s TLS through
proxy.Config.IdentityFn: an mTLS peer certificate subject or a verified JWT.
Postgres
pgwire carries its Kerberos exchange in ordinary tagged messages, which the codec skips by length and forwards untouched:GSS encryption is refused
This is the one to understand, because a mistake here shows no symptom.libpq defaults gssencmode=prefer, so a client holding a ticket asks to wrap
the whole session in GSSAPI before anything else, ahead of TLS. Accept it and
each later byte is ciphertext: no statements, no masking, no audit trail, and no
error anywhere saying inspection stopped.
The Sidecar answers N to that request. The client falls back and keeps its
Kerberos authentication, which travels as those tagged messages. Postgres
agrees:
pgjdbc, and therefore DBeaver, defaults gssEncMode to allow, which skips
the request. The exposure comes from psql and anything else built on
libpq.N, not E. Both decline, and pgjdbc closes and reopens the
TCP connection on E, which doubles each login in the audit trail.
The audit trail names the actor
pgwire sends itsuser parameter in cleartext in the StartupMessage, so the
relay records it:
AuthenticationOk, because Postgres validated the ticket against
that exact name. Statements only flow after authentication succeeds, so a
statement attributed to a principal carries a verified one. A session showing a
principal and no statements is a login that failed.
Terminating the client’s TLS
pgwire negotiates TLS in-band: the client sends an 8-byteSSLRequest and waits
for a one-byte answer before any handshake. A plain TLS listener sees a sentinel
where it expects a ClientHello and fails.
Two ways to handle it.
The Sidecar terminates. Set downstream_tls on the lane. Envoy stays a plain
tcp_proxy, and the Sidecar answers both the GSS request and the TLS request in
the right order, so Kerberos and TLS coexist with no client flags:
envoy-contrib image and
envoy.filters.network.postgres_proxy with terminate_ssl, layered over a
starttls transport socket. Envoy marks that filter work-in-progress and
documents it as unhardened. It also reacts to a GSSENCRequest by treating the
session as encrypted and stops terminating from that point, so Kerberos
clients on that lane must set gssencmode=disable.
Either way, clients need channel_binding=disable when the Sidecar terminates the
upstream TLS. The Sidecar strips SCRAM-SHA-256-PLUS from the server’s offer,
because the client cannot bind to a channel it did not see, and a client on TLS
reads that missing mechanism as a downgrade attack.
Masking
Both database lanes mask responses, by mechanisms suited to their framing. Postgres rebuilds its length-prefixedDataRow frames. MSSQL reassembles the TDS
token stream across packets, rewrites ROW and NBCROW values, and lays fresh
packets over the result, because a longer value has nowhere to go in the
original framing.
NVARCHAR, NCHAR, VARCHAR, CHAR, their MAX (PLP) forms,
and TEXT/NTEXT. A column type the codec cannot measure (SQL_VARIANT, XML,
UDT) stops the rewriting for that connection. Guessing a length would
desynchronize the client.
Statements and policy carry on; only masking steps aside, and whatever was
already rewritten stays rewritten.
Local testing
Three compose stacks underdeploy/docker-compose/envoy-stack run the whole
flow. The first two put a Samba AD DC, a database holding a keytab, a client
holding a ticket and Envoy in front of it. The third drops Envoy and Kerberos
to isolate one variable, the encryption negotiation.
Limits
- MSSQL statement extraction covers
sp_executesql.sp_prepare,sp_prepexecand the cursor family yield no statement, which is how JDBC and .NET send prepared statements.EXEC('DELETE ...')classifies as a call rather than a delete. - MSSQL logins need a directory. SQL Server resolves
DOMAIN\userto a SID over LDAP before it will store a login, so a keytab alone is not enough. Postgres compares the principal againstpg_authid, a local table. - The MSSQL upstream hop is plaintext unless the server speaks TDS 8.0.
upstream_tlsperforms a TLS-on-connect handshake, and SQL Server on Linux offers no strict-encryption setting. - Prepared statements undercount in the audit trail. pgjdbc stops sending
ParseafterprepareThresholdexecutions, so repeat runs of one prepared statement leave fewer records than executions. Policy still evaluates at parse time. - Extended Protection breaks the topology. A channel-binding token ties the authenticator to the client’s TLS channel. The database then compares that token against the channel Envoy terminated, and rejects the login. EPA’s server side is Windows-only and off by default.