How to match on StartTLS for proxying Postgres?
coolaj86 opened this issue Β· 11 comments
Update
psql
will send 00 00 00 08 04 d2 16 2f
(SSLRequest) and wait for either
N
(no - plaintext)Y
(yes - version 2) (deprecated)- or
S
(ssl - version 3)
If S
is sent, it will immediately begin a standard TLS connection.
After TLS is terminated, of if N
(plaintext) is sent, it will send 00 00 xx xx 00 03 00 00
- where xx are Little-Endian length bytes (up to 4k at least) and 03 means "pg version 3", followed by the plaintext db connection information (username, dbname, application name, character encoding).
If sslmode=disable
in the client, it will skip the SSLRequest handshake and go straight to plain text, as if N
had been sent, or TLS had been terminated.
Original
The first packet from psql
is 00 00 00 08 04 d2 16 2f
, which I believe is related to StartTLS.
openssl s_client -starttls postgres
- https://www.postgresql.org/docs/current/protocol-message-formats.html
- https://www.tzeejay.com/blog/2022/06/golang-postgresql-check-certificates/
Either way, it doesn't send any SNI or ALPN information until later on in the handshake.
Is there a way that I could route based on the first 8 bytes?
I almost got around this by using sclient
for TLS and piping the psql
connection through with sslmode=disable
, but I think my HTTP matcher is causing it to hang waiting for a \r\n
that never comes.
Matching of PostgreSQL connections was recently merged in #186. Anything beyond matching and simple proxying/teeing will require custom handlers or matchers to be developed. My previous research indicates each database has its own packet structure, so each requires its own matching logic:
I can't come up with a UX-friendly way to match arbitrary bytes
@mohammed90 #186 is not actually detecting Postgres. It's detecting the StartTLS byte (SMTP, SHTTP) it is detecting SSLRequest
I think the "Version 3" byte refers to SSLv3, not pgsql v3. Nope, it's SSLRequest pg v3.
The 2f
byte refers to SSL/TLS whereas a 30
in that position would refer to GSSAPI (competing standard at the time which postgres also adopted). I'm not sure with all the specifics yet, but it may be a bitmask that signals either protocol.
See: traefik/traefik#9929
I can't come up with a UX-friendly way to match arbitrary bytes
How about:
{
"match": [
{
"bytes": {
"values": [
{
"offset": 0,
"value": "0x0000000804d2162f"
},
{
"offset": 0,
"value": "0x0000000804d21630"
}
]
}
}
]
}
The way that HAProxy does it is similar to that.
I believe what's supposed to happen is that Caddy should send a "hello" / "accepted" style packet back, then the client will send SNI + ALPN like a normal TLS connection.
Then caddy would reconstruct the StartTLS byte to the correct server, eat the response, and forward the true TLS connection on from that point.
I'm digging in a little bit to try to figure out more...
Looks like I was wrong. The packet is a postgres-specific SSLRequest
: https://svn.nmap.org/nmap/nselib/sslcert.lua
postgres_prepare_tls_without_reconnect = function(host, port)
-- http://www.postgresql.org/docs/devel/static/protocol-message-formats.html
-- 80877103 is "SSLRequest" in v2 and v3 of Postgres protocol
local s, resp = comm.opencon(host, port, string.pack(">I4I4", 8, 80877103))
if not s then
return false, ("Failed to connect to Postgres server: %s"):format(resp)
end
-- v2 has "Y", v3 has "S"
if string.match(resp, "^[SY]") then
starttls_supported(host, port, true)
return true, s
elseif string.match(resp, "^N") then
starttls_supported(host, port, false)
return false, "Postgres server does not support SSL"
end
return false, "Unknown response from Postgres server"
end,
So Caddy would have to respond with the byte S
, after which the SNI information should come.
Is there currently a way to modify that matcher to send the S
back so that it can get the rest of the information to match on?
Verified:
Terminal 1
printf 'S' | nc -l localhost 54321 | hexyl
Terminal 2
psql 'postgres://postgres:postgres@localhost:54321/postgres?sslmode=require'
Result (Terminal 1)
ββββββββββ¬ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ¬βββββββββ¬βββββββββ
β00000000β 00 00 00 08 04 d2 16 2f β 16 03 01 01 38 01 00 01 βββββ’β’Γβ’/ββ’β’β’β’8β’ββ’β
β00000010β 34 03 03 91 93 fa f9 ce β 9d 1c 06 d6 03 cf 6d be β4β’β’ΓΓΓΓΓβΓβ’β’Γβ’ΓmΓβ
β00000020β 4e 72 e3 ea 30 d4 14 3d β 6c 7b 3e 65 2a ea 63 cf βNrΓΓ0Γβ’=βl{>e*ΓcΓβ
β00000030β cc 71 bd 20 2e c2 b3 00 β 6f 19 cc b5 ee 59 5a 1f βΓqΓ .ΓΓββoβ’ΓΓΓYZβ’β
β00000040β e3 81 33 66 0a e4 f8 cd β 80 b0 b5 8e 7e 77 d2 3c βΓΓ3f_ΓΓΓβΓΓΓΓ~wΓ<β
β00000050β 48 48 2c 9e 00 3e 13 02 β 13 03 13 01 c0 2c c0 30 βHH,Γβ>β’β’ββ’β’β’β’Γ,Γ0β
β00000060β 00 9f cc a9 cc a8 cc aa β c0 2b c0 2f 00 9e c0 24 ββΓΓΓΓΓΓΓβΓ+Γ/βΓΓ$β
β00000070β c0 28 00 6b c0 23 c0 27 β 00 67 c0 0a c0 14 00 39 βΓ(βkΓ#Γ'ββgΓ_Γβ’β9β
β00000080β c0 09 c0 13 00 33 00 9d β 00 9c 00 3d 00 3c 00 35 βΓ_Γβ’β3βΓββΓβ=β<β5β
β00000090β 00 2f 00 ff 01 00 00 ad β 00 00 00 0e 00 0c 00 00 ββ/βΓβ’ββΓβββββ’β_βββ
β000000a0β 09 6c 6f 63 61 6c 68 6f β 73 74 00 0b 00 04 03 00 β_localhoβstββ’ββ’β’ββ
β000000b0β 01 02 00 0a 00 16 00 14 β 00 1d 00 17 00 1e 00 19 ββ’β’β_ββ’ββ’βββ’ββ’ββ’ββ’β
β000000c0β 00 18 01 00 01 01 01 02 β 01 03 01 04 00 23 00 00 βββ’β’ββ’β’β’β’ββ’β’β’β’β#βββ
β000000d0β 00 16 00 00 00 17 00 00 β 00 0d 00 30 00 2e 04 03 βββ’ββββ’ββββ_β0β.β’β’β
β000000e0β 05 03 06 03 08 07 08 08 β 08 1a 08 1b 08 1c 08 09 ββ’β’β’β’β’β’β’β’ββ’β’β’β’β’β’β’_β
β000000f0β 08 0a 08 0b 08 04 08 05 β 08 06 04 01 05 01 06 01 ββ’_β’β’β’β’β’β’ββ’β’β’β’β’β’β’β’β
β00000100β 03 03 03 01 03 02 04 02 β 05 02 06 02 00 2b 00 05 ββ’β’β’β’β’β’β’β’ββ’β’β’β’β+ββ’β
β00000110β 04 03 04 03 03 00 2d 00 β 02 01 01 00 33 00 26 00 ββ’β’β’β’β’β-βββ’β’β’β3β&ββ
β00000120β 24 00 1d 00 20 92 9c 91 β b3 48 c7 a8 fa 1e 1e d9 β$ββ’β ΓΓΓβΓHΓΓΓβ’β’Γβ
β00000130β 8e ff 94 51 5f 2b 93 5c β 2b 0d 9c 7d 46 e2 08 3b βΓΓΓQ_+Γ\β+_Γ}FΓβ’;β
β00000140β ba ba 78 d9 73 β βΓΓxΓs β β
ββββββββββ΄ββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββ΄βββββββββ΄βββββββββ
Comparing that to the output of curl https://localhost:54321/hello
:
ββββββββββ¬ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ¬βββββββββ¬βββββββββ
β00000000β 16 03 01 01 3a 01 00 01 β 36 03 03 6f c4 fa 4d 5c ββ’β’β’β’:β’ββ’β6β’β’oΓΓM\β
β00000010β 1c 91 f0 f1 85 98 b2 26 β bc 27 95 01 7b 8d ef 0e ββ’ΓΓΓΓΓΓ&βΓ'Γβ’{ΓΓβ’β
β00000020β 8b 33 92 1e 89 98 55 4c β 4b 2e e0 20 db b7 1a 80 βΓ3Γβ’ΓΓULβK.Γ ΓΓβ’Γβ
β00000030β 0b 73 ea 87 38 17 f5 78 β 01 9f 80 41 e9 1b 15 b1 ββ’sΓΓ8β’Γxββ’ΓΓAΓβ’β’Γβ
β00000040β e0 a4 c5 a6 81 7e 4f 24 β 97 9d 2e 78 00 62 13 03 βΓΓΓΓΓ~O$βΓΓ.xβbβ’β’β
β00000050β 13 02 13 01 cc a9 cc a8 β cc aa c0 30 c0 2c c0 28 ββ’β’β’β’ΓΓΓΓβΓΓΓ0Γ,Γ(β
β00000060β c0 24 c0 14 c0 0a 00 9f β 00 6b 00 39 ff 85 00 c4 βΓ$Γβ’Γ_βΓββkβ9ΓΓβΓβ
β00000070β 00 88 00 81 00 9d 00 3d β 00 35 00 c0 00 84 c0 2f ββΓβΓβΓβ=ββ5βΓβΓΓ/β
β00000080β c0 2b c0 27 c0 23 c0 13 β c0 09 00 9e 00 67 00 33 βΓ+Γ'Γ#Γβ’βΓ_βΓβgβ3β
β00000090β 00 be 00 45 00 9c 00 3c β 00 2f 00 ba 00 41 c0 11 ββΓβEβΓβ<ββ/βΓβAΓβ’β
β000000a0β c0 07 00 05 00 04 c0 12 β c0 08 00 16 00 0a 00 ff βΓβ’ββ’ββ’Γβ’βΓβ’ββ’β_βΓβ
β000000b0β 01 00 00 8b 00 2b 00 09 β 08 03 04 03 03 03 02 03 ββ’ββΓβ+β_ββ’β’β’β’β’β’β’β’β
β000000c0β 01 00 33 00 26 00 24 00 β 1d 00 20 3e ad 97 0e a7 ββ’β3β&β$βββ’β >ΓΓβ’Γβ
β000000d0β a5 8a 6f b6 99 66 5c 1f β b2 d7 64 42 25 1e b1 ee βΓΓoΓΓf\β’βΓΓdB%β’ΓΓβ
β000000e0β 65 2b 46 ba c3 47 1c e1 β 65 fc 4b 00 00 00 0e 00 βe+FΓΓGβ’ΓβeΓKββββ’ββ
β000000f0β 0c 00 00 09 6c 6f 63 61 β 6c 68 6f 73 74 00 0b 00 β_ββ_locaβlhostββ’ββ
β00000100β 02 01 00 00 0a 00 0a 00 β 08 00 1d 00 17 00 18 00 ββ’β’ββ_β_βββ’ββ’ββ’ββ’ββ
β00000110β 19 00 0d 00 18 00 16 08 β 06 06 01 06 03 08 05 05 ββ’β_ββ’ββ’β’ββ’β’β’β’β’β’β’β’β
β00000120β 01 05 03 08 04 04 01 04 β 03 02 01 02 03 00 10 00 ββ’β’β’β’β’β’β’β’ββ’β’β’β’β’ββ’ββ
β00000130β 0e 00 0c 02 68 32 08 68 β 74 74 70 2f 31 2e 31 ββ’β_β’h2β’hβttp/1.1 β
ββββββββββ΄ββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββ΄βββββββββ΄βββββββββ
That very much appears that as soon as the character S
is sent, the standard TLS connection resumes. The next 16 bytes are almost identical in both cases, aside from perhaps some size or version bytes being slightly different, and you can see the SNI localhost
in both messages.
Thanks for digging into the SSL side of Postgres - it's certainly more complex and so I started by addressing the non-SSL side of things, for which I have #188 in progess, however it looks like there may be some hope from your comments.
What is currently unclear to me is how Caddy L4 can match across multiple requests, I see some use of cx.Context
and caddy.Context
which is probably the way to go.
For other details on the Postgres protocol I have found a great resource in https://github.com/rueian/pgbroker
@metafeather Awesome, thanks. We'll probably be making some improvements to the matching algorithm here soon: #192 -- just FYI. I don't know, maybe it won't affect your plugin (much).
FYI: I've submitted a request to Postgres to add --tls
and --alpn
options of some sort to the client - in which case the plaintext matching that caddy is already equipped to handle would be sufficient:
- https://www.postgresql.org/message-id/ECNyobMWPeoCd4yj_5J0RsDL1yKC9MbbBwOGCYHgcts7v0BW_-znGIoxcvfzUsf3yKvUB6Lef22OBMZnJyZ-0T2U1qaVflQqEGO0RFHp1PE%3D%40proton.me
- https://www.postgresql.org/message-id/y3hCpl3ALJQPlIn8aKG19aiYbNM_HbchVTOqlwm2Y9OE-sWmtre-Cljlt9Jd_yYsv5S3mDNG-T5OXXfU8GgDrdu2MjTBEcWl23_NUesj8i8%3D%40proton.me
I probably didn't submit it to the right place, but perhaps it will get the conversation started.
Speak of the Zeitgeist!
This discussed a little over a year ago, and the commits are beginning to land.