diff --git a/.travis.yml b/.travis.yml index 9789ec03..37fa4f44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,4 @@ -sudo: required - language: c compiler: gcc @@ -51,6 +49,7 @@ install: - export PATH=$OPENRESTY_PREFIX/nginx/sbin:$LUAROCKS_PREFIX/bin:$PATH - sudo luarocks install luacheck > build.log 2>&1 || (cat build.log && exit 1) - sudo luarocks install lua-resty-worker-events > build.log 2>&1 || (cat build.log && exit 1) + - sudo luarocks install penlight > build.log 2>&1 || (cat build.log && exit 1) - luarocks --version - nginx -V diff --git a/lib/resty/healthcheck.lua b/lib/resty/healthcheck.lua index 739301b1..9358cfe8 100644 --- a/lib/resty/healthcheck.lua +++ b/lib/resty/healthcheck.lua @@ -38,6 +38,7 @@ local resty_lock = require ("resty.lock") local re_find = ngx.re.find local bit = require("bit") local ngx_now = ngx.now +local ssl = require("ngx.ssl") -- constants local EVENT_SOURCE_PREFIX = "lua-resty-healthcheck" @@ -828,14 +829,23 @@ function checker:run_single_check(ip, port, hostname, hostheader) end if self.checks.active.type == "https" then - local session - session, err = sock:sslhandshake(nil, hostname, + local session, err + if self.ssl_cert and self.ssl_key then + session, err = sock:tlshandshake({ + verify = self.checks.active.https_verify_certificate, + client_cert = self.ssl_cert, + client_priv_key = self.ssl_key + }) + else + session, err = sock:sslhandshake(nil, hostname, self.checks.active.https_verify_certificate) + end if not session then sock:close() self:log(ERR, "failed SSL handshake with '", hostname, " (", ip, ":", port, ")': ", err) return self:report_tcp_failure(ip, port, hostname, "connect", "active") end + end local path = self.checks.active.http_path @@ -1274,6 +1284,8 @@ end -- * `name`: name of the health checker -- * `shm_name`: the name of the `lua_shared_dict` specified in the Nginx configuration to use -- * `checks.active.type`: "http", "https" or "tcp" (default is "http") +-- * `ssl_cert`: certificate for mTLS connections (string or parsed object) +-- * `ssl_key`: key for mTLS connections (string or parsed object) -- * `checks.active.timeout`: socket timeout for active checks (in seconds) -- * `checks.active.concurrency`: number of targets to check concurrently -- * `checks.active.http_path`: path to use in `GET` HTTP request to run on active checks @@ -1339,6 +1351,22 @@ function _M.new(opts) self.shm = ngx.shared[tostring(opts.shm_name)] assert(self.shm, ("no shm found by name '%s'"):format(opts.shm_name)) + -- load certificate and key + if opts.ssl_cert and opts.ssl_key then + if type(opts.ssl_cert) == "cdata" then + self.ssl_cert = opts.ssl_cert + else + self.ssl_cert = assert(ssl.parse_pem_cert(opts.ssl_cert)) + end + + if type(opts.ssl_key) == "cdata" then + self.ssl_key = opts.ssl_key + else + self.ssl_key = assert(ssl.parse_pem_priv_key(opts.ssl_key)) + end + + end + -- other properties self.targets = nil -- list of targets, initially loaded, maintained by events self.events = nil -- hash table with supported events (prevent magic strings) diff --git a/lua-resty-healthcheck-scm-1.rockspec b/lua-resty-healthcheck-scm-1.rockspec index dc85bfdc..cca3e690 100644 --- a/lua-resty-healthcheck-scm-1.rockspec +++ b/lua-resty-healthcheck-scm-1.rockspec @@ -15,6 +15,7 @@ description = { } dependencies = { "lua-resty-worker-events == 0.3.1", + "penlight == 1.7.0", } build = { type = "builtin", diff --git a/t/17-mtls.t b/t/17-mtls.t new file mode 100644 index 00000000..21166d64 --- /dev/null +++ b/t/17-mtls.t @@ -0,0 +1,128 @@ +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +workers(1); + +plan tests => repeat_each() * 4; + +my $pwd = cwd(); + +our $HttpConfig = qq{ + lua_package_path "$pwd/lib/?.lua;;"; + lua_shared_dict test_shm 8m; + lua_shared_dict my_worker_events 8m; +}; + +run_tests(); + +__DATA__ + +=== TEST 1: configure a MTLS probe +--- http_config eval +qq{ + $::HttpConfig +} +--- config + location = /t { + content_by_lua_block { + local we = require "resty.worker.events" + assert(we.configure{ shm = "my_worker_events", interval = 0.1 }) + + local pl_file = require "pl.file" + local cert = pl_file.read("t/util/cert.pem", true) + local key = pl_file.read("t/util/key.pem", true) + + local healthcheck = require("resty.healthcheck") + local checker = healthcheck.new({ + name = "testing_mtls", + shm_name = "test_shm", + type = "http", + ssl_cert = cert, + ssl_key = key, + checks = { + active = { + http_path = "/status", + healthy = { + interval = 999, -- we don't want active checks + successes = 3, + }, + unhealthy = { + interval = 999, -- we don't want active checks + tcp_failures = 3, + http_failures = 3, + } + }, + passive = { + healthy = { + successes = 3, + }, + unhealthy = { + tcp_failures = 3, + http_failures = 3, + } + } + } + }) + ngx.say(checker ~= nil) -- true + } + } +--- request +GET /t +--- response_body +true + + +=== TEST 2: configure a MTLS probe with parsed cert/key +--- http_config eval +qq{ + $::HttpConfig +} +--- config + location = /t { + content_by_lua_block { + local we = require "resty.worker.events" + assert(we.configure{ shm = "my_worker_events", interval = 0.1 }) + + local pl_file = require "pl.file" + local ssl = require "ngx.ssl" + local cert = ssl.parse_pem_cert(pl_file.read("t/util/cert.pem", true)) + local key = ssl.parse_pem_priv_key(pl_file.read("t/util/key.pem", true)) + + local healthcheck = require("resty.healthcheck") + local checker = healthcheck.new({ + name = "testing_mtls", + shm_name = "test_shm", + type = "http", + ssl_cert = cert, + ssl_key = key, + checks = { + active = { + http_path = "/status", + healthy = { + interval = 999, -- we don't want active checks + successes = 3, + }, + unhealthy = { + interval = 999, -- we don't want active checks + tcp_failures = 3, + http_failures = 3, + } + }, + passive = { + healthy = { + successes = 3, + }, + unhealthy = { + tcp_failures = 3, + http_failures = 3, + } + } + } + }) + ngx.say(checker ~= nil) -- true + } + } +--- request +GET /t +--- response_body +true diff --git a/t/util/cert.pem b/t/util/cert.pem new file mode 100644 index 00000000..2df6a75a --- /dev/null +++ b/t/util/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUWWntedJ1yLAJE2baK/Mg06osmGAwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UECgwJS29uZyBJbmMuMB4XDTIwMDQyMzIwMjcwMFoXDTMwMDQy +MTIwMjcwMFowFDESMBAGA1UECgwJS29uZyBJbmMuMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAvVBrEH34MzwKlkBapiNyXr9huSShuojy+7i/01BSFng3 +1TiejXJ3pEjykZqt7ENkZ6+BTYUdb9klK221yXiSyX71x97O0WHHuhH/m4XwGiIH +YPBHdg+ExdMRflXgwtlW3of2hTWxkPkPQDPhoSQVMc5DkU7EOgrTxkv1rUWVAed4 +gSK4IT2AkhKwOSkewZANj2bnK5Evf71ACyJd7IQbJAIYoKBwRJAUXJMA7XAreIB+ +nEr9whNYTklhB4aEa2wtOQuiQubIMJzdOryEX5nufH+tL4p1QKhRPFAqqtJ2Czgw +YZY/v9IrThl19r0nL7FIvxFDNIMeOamJxDLQqsh9NwIDAQABo1MwUTAdBgNVHQ4E +FgQU9t6YAdQ5mOXeqvptN5l3yYZGibEwHwYDVR0jBBgwFoAU9t6YAdQ5mOXeqvpt +N5l3yYZGibEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhi83 +aXsfJGqr9Zb1guWxbI8uKoG6o88ptXjV2c6dJnxXag0A/Rj+bX2bcPkN2kvQksNl +MBUQlniOydZfsBUAoC0V7yyGUv9eO2RIeFnnNpRXNu+n+Kg2bvgvu8BKNNNOASZv ++Vmzvo9lbfhS9MNAxYk9eTiPNUZ3zn2RfFyT6YWWJbRjk//EAlchyud3XGug9/hw +c05dtzWEYT8GdzMd+Y1/2kR5r/CapSj7GEqL5T3+zDIfjbhTokV7WBrw6og2avoZ +vzrF8xWucry5/2mKQbRxMyCtKYUKTcoLzF4HrNQCETm0n9qUODrHER7Wit9fQFZX +1GEA3BkX2tsbIVVaig== +-----END CERTIFICATE----- diff --git a/t/util/key.pem b/t/util/key.pem new file mode 100644 index 00000000..ae945f44 --- /dev/null +++ b/t/util/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9UGsQffgzPAqW +QFqmI3Jev2G5JKG6iPL7uL/TUFIWeDfVOJ6NcnekSPKRmq3sQ2Rnr4FNhR1v2SUr +bbXJeJLJfvXH3s7RYce6Ef+bhfAaIgdg8Ed2D4TF0xF+VeDC2Vbeh/aFNbGQ+Q9A +M+GhJBUxzkORTsQ6CtPGS/WtRZUB53iBIrghPYCSErA5KR7BkA2PZucrkS9/vUAL +Il3shBskAhigoHBEkBRckwDtcCt4gH6cSv3CE1hOSWEHhoRrbC05C6JC5sgwnN06 +vIRfme58f60vinVAqFE8UCqq0nYLODBhlj+/0itOGXX2vScvsUi/EUM0gx45qYnE +MtCqyH03AgMBAAECggEAA1hWa/Yt2onnDfyZHXJm5PGwwlq5WNhuorADA7LZoHgD +VIspkgpBvu9jCduX0yLltUdOm5YMjRtjIr9PhP3SaikKIrv3H5AAvXLv90mIko2j +X70fJiDkEbLHDlpqHEdG16vDWVs3hf5AnLvN8tD2ZujkHL8tjHEAiPJyptsh5OSw +XaltCD67U940XXJ89x0zFZ/3RoRk78wX3ELz7/dY0cMnslMavON+LYTq9hQZyVmm +nOhZICWerKjax4t5f9PZ/zM6IhEVrUhw2WrC31tgRo+ITCIA/nkKid8vNhkiLVdw +jTyAYDLgYW7K8/zVrzmV9TOr3CaZHLQxnF/LMpIEAQKBgQDjnA/G4g2mDD7lsqU1 +N3it87v2VBnZPFNW6L17Qig+2BDTXg1kadFBlp8qtEJI+H5axVSmzsrlmATJVhUK +iYOQwiEsQnt4tGmWZI268NAIUtv0TX0i9yscsezmvGABMcyBCF7ZwFhUfhy0pn1t +kzmbYN4AjYdcisCnSusoMD92NwKBgQDU7YVNuieMIZCIuSxG61N1+ZyX3Ul5l6KU +m1xw1PZvugqXnQlOLV/4Iaz86Vvlt2aDqTWO/iv4LU7ixNdhRtxFIU/b2a8DzDOw +ijhzMGRJqJOdi1NfciiIWHyrjRmGbhCgm784vqV7qbQomiIsjgnDvjoZkossZMiJ +63vs7huxAQKBgQDiQjT8w6JFuk6cD+Zi7G2unmfvCtNXO7ys3Fffu3g+YJL5SrmN +ZBN8W7qFvQNXfo48tYTc/Rx8941qh4QLIYAD2rcXRE9xQgbkVbj+aHykiZnVVWJb +69CTidux0vist1BPxH5lf+tOsr7eZdKxpnTRnI2Thx1URSoWI0d4f93WKQKBgBXn +kW0bl3HtCgdmtU1ebCmY0ik1VJezp8AN84aQAgIga3KJbymhtVu7ayZhg1iwc1Vc +FOxu7WsMji75/QY+2e4qrSJ61GxZl3+z2HbRJaAGPZlZeew5vD26jKjBTTztGbzM +CPH3euKr5KLAqH9Y5VxDt4pl7vdULuUxWoBXRnYBAoGAHIFMYiCdXETtrFHKVTzc +vm4P24PnsNHoDTGMXPeRYRKF2+3VEJrwp1Q3fue4Go4zFB8I6nhNVIbh4dIHxFab +hyxZvGWGUgRvTvD4VYn/YHVoSf2/xNZ0r/S2LKomp+jwoWKfukbCoDjAOWvnK5iD +o41Tn0yhzBdnrYguKznGR3g= +-----END PRIVATE KEY-----