mholt/caddy-l4

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.

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:

https://caddy.community/t/need-some-help-with-enabling-lets-encrypt-certificate-on-layer-4-module/18628/11

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:

I probably didn't submit it to the right place, but perhaps it will get the conversation started.

Speak of the Zeitgeist!

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=d39a49c1e459804831302807c724fa6512e90cf0

This discussed a little over a year ago, and the commits are beginning to land.