From 7cf86da0528d02517fa5748874dd1fffbb51ac03 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:44:37 -0500 Subject: [PATCH 01/21] Generate new certificates and keys for kbn-dev-utils Added Kibana certificate/keypair, and also added P12 formats for both Kibana and Elasticsearch. Removed obsolete dev certs/keys, and changed files and tests where appropriate. --- packages/kbn-dev-utils/certs/README.md | 68 ++++++++++++++++++ packages/kbn-dev-utils/certs/ca.crt | 45 +++++++----- .../kbn-dev-utils/certs/elasticsearch.crt | 43 ++++++----- .../kbn-dev-utils/certs/elasticsearch.key | 50 ++++++------- .../kbn-dev-utils/certs/elasticsearch.p12 | Bin 0 -> 3501 bytes .../certs/elasticsearch_emptypassword.p12 | Bin 0 -> 3333 bytes .../certs/elasticsearch_nopassword.p12 | Bin 0 -> 3543 bytes packages/kbn-dev-utils/certs/kibana.crt | 29 ++++++++ packages/kbn-dev-utils/certs/kibana.key | 27 +++++++ packages/kbn-dev-utils/certs/kibana.p12 | Bin 0 -> 3463 bytes packages/kbn-dev-utils/src/certs.ts | 11 +++ packages/kbn-dev-utils/src/index.ts | 14 +++- src/cli/serve/serve.js | 2 + test/server_integration/http/ssl/config.js | 7 +- .../http/ssl_redirect/config.js | 7 +- x-pack/test/pki_api_integration/config.ts | 6 +- .../pki_api_integration/fixtures/README.md | 17 +++-- .../pki_api_integration/fixtures/es_ca.key | 27 ------- .../fixtures/first_client.p12 | Bin 3467 -> 3467 bytes .../fixtures/second_client.p12 | Bin 3469 -> 3477 bytes .../fixtures/idp_metadata.xml | 38 +++++----- .../fixtures/saml_tools.ts | 3 +- 22 files changed, 275 insertions(+), 119 deletions(-) create mode 100644 packages/kbn-dev-utils/certs/README.md mode change 100755 => 100644 packages/kbn-dev-utils/certs/ca.crt mode change 100755 => 100644 packages/kbn-dev-utils/certs/elasticsearch.crt mode change 100755 => 100644 packages/kbn-dev-utils/certs/elasticsearch.key create mode 100644 packages/kbn-dev-utils/certs/elasticsearch.p12 create mode 100644 packages/kbn-dev-utils/certs/elasticsearch_emptypassword.p12 create mode 100644 packages/kbn-dev-utils/certs/elasticsearch_nopassword.p12 create mode 100644 packages/kbn-dev-utils/certs/kibana.crt create mode 100644 packages/kbn-dev-utils/certs/kibana.key create mode 100644 packages/kbn-dev-utils/certs/kibana.p12 delete mode 100644 x-pack/test/pki_api_integration/fixtures/es_ca.key diff --git a/packages/kbn-dev-utils/certs/README.md b/packages/kbn-dev-utils/certs/README.md new file mode 100644 index 0000000000000..49762c6d61aa5 --- /dev/null +++ b/packages/kbn-dev-utils/certs/README.md @@ -0,0 +1,68 @@ +# Development certificates + +Kibana includes several development certificates to enable easy setup of TLS-encrypted communications with Elasticsearch. + +_Note: these certificates should **never** be used in production._ + +## Certificate information + +Certificates and keys are provided in multiple formats. These can be used by other packages to set up a new Elastic Stack with Kibana and Elasticsearch. The Certificate Authority (CA) private key is intentionally omitted from this package. + +### PEM + +* `ca.crt` -- A [PEM-formatted](https://tools.ietf.org/html/rfc1421) [X.509](https://tools.ietf.org/html/rfc5280) certificate that is used as a CA. +* `elasticsearch.crt` -- A PEM-formatted X.509 certificate and public key for Elasticsearch. +* `elasticsearch.key` -- A PEM-formatted [PKCS #1](https://tools.ietf.org/html/rfc8017) private key for Elasticsearch. +* `kibana.crt` -- A PEM-formatted X.509 certificate and public key for Kibana. +* `kibana.key` -- A PEM-formatted PKCS #1 private key for Kibana. + +### PKCS #12 + +* `elasticsearch.p12` -- A [PKCS #12](https://tools.ietf.org/html/rfc7292) encrypted key store / trust store that contains `ca.crt`, `elasticsearch.crt`, and a [PKCS #8](https://tools.ietf.org/html/rfc5208) encrypted version of `elasticsearch.key`. +* `kibana.p12` -- A PKCS #12 encrypted key store / trust store that contains `ca.crt`, `kibana.crt`, and a PKCS #8 encrypted version of `kibana.key`. + +The password used for both of these is "storepass". Other copies are also provided for testing purposes: + +* `elasticsearch_emptypassword.p12` -- The same PKCS #12 key store, encrypted with an empty password. +* `elasticsearch_nopassword.p12` -- The same PKCS #12 key store, not encrypted with a password. + +## Certificate generation + +[Elasticsearch cert-util](https://www.elastic.co/guide/en/elasticsearch/reference/current/certutil.html) and [OpenSSL](https://www.openssl.org/) were used to generate these certificates. The following commands were used from the root directory of Elasticsearch: + +``` +# Generate the PKCS #12 keystore for a CA, valid for 50 years +bin/elasticsearch-certutil ca -days 18250 --pass castorepass + +# Generate the PKCS #12 keystore for Elasticsearch and sign it with the CA +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name elasticsearch --dns localhost --pass storepass + +# Generate the PKCS #12 keystore for Kibana and sign it with the CA +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name kibana --dns localhost --pass storepass + +# Copy the PKCS #12 keystore for Elasticsearch with an empty password +openssl pkcs12 -in elasticsearch.p12 -nodes -passin pass:"storepass" -passout pass:"" | openssl pkcs12 -export -out elasticsearch_emptypassword.p12 -passout pass:"" + +# Manually create "elasticsearch_nopassword.p12" -- this can be done on macOS by importing the P12 key store into the Keychain and exporting it again + +# Extract the PEM-formatted X.509 certificate for the CA +openssl pkcs12 -in elasticsearch.p12 -out ca.crt -cacerts -passin pass:"storepass" -passout pass: + +# Extract the PEM-formatted PKCS #8 encrypted private key for Elasticsearch +openssl pkcs12 -in elasticsearch.p12 -out elasticsearch-encrypted.key -nocerts -passin pass:"storepass" -passout pass:"keypass" + +# Decrypt the private key for Elasticsearch and convert it to a PEM-formatted PKCS #1 private key +openssl rsa -in elasticsearch-encrypted.key -out elasticsearch.key -passin pass:keypass + +# Extract the PEM-formatted X.509 certificate for Elasticsearch +openssl pkcs12 -in elasticsearch.p12 -out elasticsearch.crt -clcerts -passin pass:"storepass" -passout pass: + +# Extract the PEM-formatted PKCS #8 encrypted private key for Kibana +openssl pkcs12 -in kibana.p12 -out kibana-encrypted.key -nocerts -passin pass:"storepass" -passout pass:"keypass" + +# Decrypt the private key for Kibana and convert it to a PEM-formatted PKCS #1 private key +openssl rsa -in kibana-encrypted.key -out kibana.key -passin pass:keypass + +# Extract the PEM-formatted X.509 certificate for Kibana +openssl pkcs12 -in kibana.p12 -out kibana.crt -clcerts -passin pass:"storepass" -passout pass: +``` diff --git a/packages/kbn-dev-utils/certs/ca.crt b/packages/kbn-dev-utils/certs/ca.crt old mode 100755 new mode 100644 index 3e964823c5086..217935b8d83f6 --- a/packages/kbn-dev-utils/certs/ca.crt +++ b/packages/kbn-dev-utils/certs/ca.crt @@ -1,20 +1,29 @@ +Bag Attributes + friendlyName: elasticsearch + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37 +Key Attributes: +Bag Attributes + friendlyName: ca + 2.16.840.1.113894.746875.1.1: +subject=/CN=Elastic Certificate Tool Autogenerated CA +issuer=/CN=Elastic Certificate Tool Autogenerated CA -----BEGIN CERTIFICATE----- -MIIDSjCCAjKgAwIBAgIVAOgxLlE1RMGl2fYgTKDznvDL2vboMA0GCSqGSIb3DQEB -CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu -ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzQ0OFoXDTIyMDcxMDE3MzQ0OFowNDEyMDAG -A1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNImKp/A9l++Ac7U5lvHOA -+fYRb8p7AgdfKBMB0v3bo+bpHjkbkf3vYHjo1xJSg5ls6EPK+Do4owkAgKJdrznI -5/efJOjgA+ylH4rgAfrRIQmiFEWZnAv86vJ+Iq83mfkPELb4dvXCi7AFQkzoM/rY -Lbi97xha5bA2SEmpYp7VhBTM9zWy+q9Tm5odPO8u2n75GpIM2RwipaXlL0ink+06 -/oweQJoivaDgpDOmUXCFPmpV3VCdhUGxDQPyG0upQkF+NbQoei4RmluPEmVz4S7I -TFLWjX7LeZVP63bJkcCgiq6Hm97kDtr9EYlPKhHm7UMWzhNzHbfvySMDzqAJC0KX -AgMBAAGjUzBRMB0GA1UdDgQWBBRKqaaQ/+jT+ipPLJe7qekp1N/zizAfBgNVHSME -GDAWgBRKqaaQ/+jT+ipPLJe7qekp1N/zizAPBgNVHRMBAf8EBTADAQH/MA0GCSqG -SIb3DQEBCwUAA4IBAQA7Gcq8h8yDXvepfKUAcTTMCBZkI+g3qE1gfRwjW7587CIj -xnrzEqANU+Q1lv7IeQ158HiduDUMZfnvpuNwkf0HkqnRWb57RwfVdCAlAeZmzipq -5ZJWlIW4dbmk57nGLg4fCszedi0uSGytZ2/BUdpWyC0fAM97h7Agtr4xGGKMEL67 -uB55ijt61V62HZ5wWXWNO9m+wfmdnt+YQViQJHtpYz1oOmWhY3dpitZLfWs1sLLD -w3CZOhmWX7+P7+HlCkSBF4swzHOCI3THyX61NbLxju8VkTAjwbZPq4EOnVKnO6kr -RdwQVnzKnqG5fxfSGknNahy0pOhJHZlGLwECRlgF +MIIDSzCCAjOgAwIBAgIUW0brhEtYK3tUBYlXnUa+AMmAX6kwDQYJKoZIhvcNAQEL +BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l +cmF0ZWQgQ0EwIBcNMTkxMjI3MTcwMjMyWhgPMjA2OTEyMTQxNzAyMzJaMDQxMjAw +BgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAplO5m5Xy8xERyA0/G5SM +Nu2QXkfS+m7ZTFjSmtwqX7BI1I6ISI4Yw8QxzcIgSbEGlSqb7baeT+A/1JQj0gZN +KOnKbazl+ujVRJpsfpt5iUsnQyVPheGekcHkB+9WkZPgZ1oGRENr/4Eb1VImQf+Y +yo/FUj8X939tYW0fficAqYKv8/4NWpBUbeop8wsBtkz738QKlmPkMwC4FbuF2/bN +vNuzQuRbGMVmPeyivZJRfDAMKExoXjCCLmbShdg4dUHsUjVeWQZ6s4vbims+8qF9 +b4bseayScQNNU3hc5mkfhEhSM0KB0lDpSvoCxuXvXzb6bOk7xIdYo+O4vHUhvSkQ +mwIDAQABo1MwUTAdBgNVHQ4EFgQUGu0mDnvDRnBdNBG8DxwPdWArB0kwHwYDVR0j +BBgwFoAUGu0mDnvDRnBdNBG8DxwPdWArB0kwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEASv/FYOwWGnQreH8ulcVupGeZj25dIjZiuKfJmslH8QN/ +pVCIzAxNZjGjCpKxbJoCu5U9USaBylbhigeBJEq4wmYTs/WPu4uYMgDj0MILuHin +RQqgEVG0uADGEgH2nnk8DeY8gQvGpJRQGlXNK8pb+pCsy6F8k/svGOeBND9osHfU +CVEo5nXjfq6JCFt6hPx7kl4h3/j3C4wNy/Dv/QINdpPsl6CnF17Q9R9d60WFv42/ +pkl7W1hszCG9foNJOJabuWfVoPkvKQjoCvPitZt/hCaFZAW49PmAVhK+DAohQ91l +TZhDmYqHoXNiRDQiUT68OS7RlfKgNpr/vMTZXDxpmw== -----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/certs/elasticsearch.crt b/packages/kbn-dev-utils/certs/elasticsearch.crt old mode 100755 new mode 100644 index b30e11e9bbce1..87ba02019903f --- a/packages/kbn-dev-utils/certs/elasticsearch.crt +++ b/packages/kbn-dev-utils/certs/elasticsearch.crt @@ -1,20 +1,29 @@ +Bag Attributes + friendlyName: elasticsearch + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37 +Key Attributes: +Bag Attributes + friendlyName: elasticsearch + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37 +subject=/CN=elasticsearch +issuer=/CN=Elastic Certificate Tool Autogenerated CA -----BEGIN CERTIFICATE----- -MIIDRDCCAiygAwIBAgIVAI8V1fwvXKykKtp5k0cLpTOtY+DVMA0GCSqGSIb3DQEB +MIIDQDCCAiigAwIBAgIVAI93OQE6tZffPyzenSg3ljE3JJBzMA0GCSqGSIb3DQEB CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu -ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzUxOFoXDTIyMDcxMDE3MzUxOFowGDEWMBQG -A1UEAxMNZWxhc3RpY3NlYXJjaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBALW+8gV6m6wYmTZmrXzNWKElE+ePkkikCviNfuWonWqxgAoWpAwAx2FvdhP3 -UDFbe38ydJX4oDgXeC25vdIR6z2uqzx+GXSNSybO7luuOUYQOP4Xf5Cj3zzXXMyu -nY1nZTVsChI9jAMz4cZZdUd04f4r4TBNxrFCcVR0uec5RGRXuP8rSQd9AbYFUVYf -jJeLb24asghb2Ku+c2JGvMqPEXFWFGOXFhUoIbRjCJNTDcr1ZXPof3+fO1l6HmhT -QBSqC4IZL8XqANltDT4tCQDD8L9+ckWJD8MP3wPkPUGZId2gLu++hrb9YfiP2upq -N/f3P7l5Fcisw1iwQC4+DGMTyfcCAwEAAaNpMGcwHQYDVR0OBBYEFGuiGk8HLpG2 -MyA24/J+GwxT32ikMB8GA1UdIwQYMBaAFEqpppD/6NP6Kk8sl7up6SnU3/OLMBoG -A1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB -CwUAA4IBAQB8yfY0edAgq2KnJNWyl8NpHNfqtM27+/LR2V8OxVwxV1hc4ZilczLu -CXeqP9uqBVjcck6fvLrjy4LhSG0V05j51UMJ1FjFVTBuhlrDcd3j8848yWrmyz8z -vPYYY2vIN9d1NsBgufULwliBT4UJchsYE8xT5ayAzGHKCTlzHGHMTPzYjwac8nbT -nd2u+6h0OQOJn6K4v+RfXtN4EA8ZUrYxUkqHNS3cFB5sxH7JQGi25XJc5MfxyCwY -YOukxbN85ew861N6oVd+W+nGJu8WOLU88/uvCv+dLhnAlnnIOLqvmrD5m7gFsFO9 -Z7Xz/U1SbNipWy9OLOhqq2Ja59j8p9e5 +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDMxN1oYDzIwNjkxMjE0MTcwMzE3WjAYMRYw +FAYDVQQDEw1lbGFzdGljc2VhcmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2EkPfvE3ZNMjHCAQZhpImoXBCIN6KavvJSbVHRtLzAXB4wxige+vFQWb +4umqPeEeVH7FvrsRqn24tUgGIkag9p9AOwYxfcT3vwNqcK/EztIlYFs72pmYg7Ez +s6+qLc/YSLOT3aMoHKDHE93z1jYIDGccyjGbv9NsdgCbLHD0TQuqm+7pKy1MZoJm +0qn4KYw4kXakVNWlxm5GIwr8uqU/w4phrikcOOWqRzsxByoQajypLOA4eD/uWnI2 +zGyPQy7Bkxojiy1ss0CVlrl8fJgcjC4PONpm1ibUSX3SoZ8PopPThR6gvvwoQolR +rYu4+D+rsX7q/ldA6vBOiHBD8r4QoQIDAQABo2MwYTAdBgNVHQ4EFgQUSlIMCYYd +e72A0rUqaCkjVPkGPIwwHwYDVR0jBBgwFoAUGu0mDnvDRnBdNBG8DxwPdWArB0kw +FAYDVR0RBA0wC4IJbG9jYWxob3N0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQAD +ggEBAImbzBVAEjiLRsNDLP7QAl0k7lVmfQRFz5G95ZTAUSUgbqqymDvry47yInFF +3o12TuI1GxK5zHzi+qzpJLyrnGwGK5JBR+VGxIBBKFVcFh1WNGKV6kSO/zBzO7PO +4Jw4G7By/ImWvS0RBhBUQ9XbQZN3WcVkVVV8UQw5Y7JoKtM+fzyEKXKRCTsvgH+h +3+fUBgqwal2Mz4KPH57Jrtk209dtn7tnQxHTNLo0niHyEcfrpuG3YFqTwekr+5FF +FniIcYHPGjag1WzLIdyhe88FFpuav19mlCaxBACc7t97v+euSVUWnsKpy4dLydpv +NxJiI9eWbJZ7f5VM7o64pm7U1cU= -----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/certs/elasticsearch.key b/packages/kbn-dev-utils/certs/elasticsearch.key old mode 100755 new mode 100644 index 1013ce3971246..9ae4e314630d1 --- a/packages/kbn-dev-utils/certs/elasticsearch.key +++ b/packages/kbn-dev-utils/certs/elasticsearch.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/packages/kbn-dev-utils/certs/elasticsearch.p12 b/packages/kbn-dev-utils/certs/elasticsearch.p12 new file mode 100644 index 0000000000000000000000000000000000000000..02a9183cd8a50e166b7b0500caf215a0a8dcd6b6 GIT binary patch literal 3501 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 L$;zsL0Kk6%be@sh literal 0 HcmV?d00001 diff --git a/packages/kbn-dev-utils/certs/kibana.crt b/packages/kbn-dev-utils/certs/kibana.crt new file mode 100644 index 0000000000000..1c83be587bff9 --- /dev/null +++ b/packages/kbn-dev-utils/certs/kibana.crt @@ -0,0 +1,29 @@ +Bag Attributes + friendlyName: kibana + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 32 32 33 30 33 39 +Key Attributes: +Bag Attributes + friendlyName: kibana + localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 32 32 33 30 33 39 +subject=/CN=kibana +issuer=/CN=Elastic Certificate Tool Autogenerated CA +-----BEGIN CERTIFICATE----- +MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w +DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ +wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU +FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q +OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ +s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU +vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T +BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz +V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE +DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/ +z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv ++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX +TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy +b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk +cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O +eOUsdwn1yDKHRxDHyA== +-----END CERTIFICATE----- diff --git a/packages/kbn-dev-utils/certs/kibana.key b/packages/kbn-dev-utils/certs/kibana.key new file mode 100644 index 0000000000000..4a4e6b4cb8c36 --- /dev/null +++ b/packages/kbn-dev-utils/certs/kibana.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAkMGGG0LW0QSieLjWXNviIEVPuzTS9tWXm0JRCDICu4wKjjK7 +Ftm+QwE8+NRXfhkBVBVGGI8KPj0I+rCR7ITiaP4DW99h68nbzCO2D73r5LM5LhBg +n18UNw8NkUwAi4A9EDgpMB7590zH9JaJaFf3jxlmmQ2GWl4t2uQ3VovxxRaCcrIB +dCVdn+l8ioSZBmhHybPaVl9uvcx/10LxjceARIpiaWAfIeCZw2DxSIkHao/rJdm/ +pEtYR/ZC+YAQDF4xVL3DZ1/j5sk48iaxqO+ZNJLvrKQX9WzMLkLd7kpjPk0CF5Lx +Q5ktAzlREq6QORAtUwVBkc+qPyPPF8eDLne+9QIDAQABAoIBAHl9suxWYKz00te3 +alJtSZAEHDLm1tjL034/XnseXiTCGGnYMiWvgnwCIgZFUVlH61GCuV4LT3GFEHA2 +mYKE1PGBn5gQF8MpnAvtPPRhVgaQVUFQBYg86F59h8mWnC545sciG4+DsA/apUem +wJSOn/u+Odni/AwEV0ALolZFBhl+0rccSr+6paJnzJ7QNiIn6EWbgb0n9WXqkhap +TqoPclBHm0ObeBI6lNyfvBZ8HB3hyjWZInNCaAs9DnkNPh4evuttUn/KlOPOVn9r +xz2UYsmVW6E+yPXUpSYkFQN9aaPF6alOz8PIfF8Wit7pmZMmInluGcwi/us9+ZTN +8gNvpoECgYEA0KC7XEoXRsBTN4kPznkGftvj1dtgB35W/HxXNouArQQjCbLhqcsA +jqaK0f+stYzSWZXGsKl9yQU9KA7u/wCHmLep70l7WsYYUKdkhWouK0HU5MeeLwB0 +N4ekQOQuQGqelqMo7IG2hQhTYD9PB4F3G0Sz1FgdObfuGPKfvNFVjckCgYEAsaAA +IY/TpRBWeWZfyXrnkp3atOPzkdpjb6cfT8Kib9bIECXr7ULUxA5QANX05ofodhsW +3+7iW5wicyZ1VNVEsPRL0aw7YUbNpBvob8faBUZ2KEdKQr42IfVOo7TQnvVXtumR +UE+dNvWUL2PbL0wMxD1XbMSmOze/wF8X2CeyDc0CgYBQnLqol2xVBz1gaRJ1emgb +HoXzfVemrZeY6cadKdwnfkC3n6n4fJsTg6CCMiOe5vHkca4bVvJmeSK/Vr3cRG0g +gl8kOaVzVrXQfE2oC3YZes9zMvqZOLivODcsZ77DXy82D4dhk2FeF/B3cR7tTIYk +QDCoLP/l7H8QnrdAMza2mQKBgDODwuX475ncviehUEB/26+DBo4V2ms/mj0kjAk2 +2qNy+DzuspjyHADsYbmMU+WUHxA51Q2HG7ET/E3HJpo+7BgiEecye1pADZ391hCt +Nob3I4eU/W2T+uEoYvFJnIOthg3veYyAOolY+ewwmr4B4WX8oGFUOx3Lklo5ehHf +mV01AoGBAI/c6OoHdcqQsZxKlxDNLyB2bTbowAcccoZIOjkC5fkkbsmMDLfScBfW +Q4YYJsmJBdrWNvo7jCl17Mcc4Is3RlmHDrItRkaZj+ehqAN3ejrnPLdgYeW/5XDK +e7yBj7oJd4oKZc59jVytdHvo5R8K0QohAv9gQEZ/tdypX+xWe+5E +-----END RSA PRIVATE KEY----- diff --git a/packages/kbn-dev-utils/certs/kibana.p12 b/packages/kbn-dev-utils/certs/kibana.p12 new file mode 100644 index 0000000000000000000000000000000000000000..06bbd23881290b8952db722041a5da7553546560 GIT binary patch literal 3463 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 literal 0 HcmV?d00001 diff --git a/packages/kbn-dev-utils/src/certs.ts b/packages/kbn-dev-utils/src/certs.ts index 0d340e4e8c906..f72e3ee547b5c 100644 --- a/packages/kbn-dev-utils/src/certs.ts +++ b/packages/kbn-dev-utils/src/certs.ts @@ -22,3 +22,14 @@ import { resolve } from 'path'; export const CA_CERT_PATH = resolve(__dirname, '../certs/ca.crt'); export const ES_KEY_PATH = resolve(__dirname, '../certs/elasticsearch.key'); export const ES_CERT_PATH = resolve(__dirname, '../certs/elasticsearch.crt'); +export const ES_P12_PATH = resolve(__dirname, '../certs/elasticsearch.p12'); +export const ES_P12_PASSWORD = 'storepass'; +export const ES_EMPTYPASSWORD_P12_PATH = resolve( + __dirname, + '../certs/elasticsearch_emptypassword.p12' +); +export const ES_NOPASSWORD_P12_PATH = resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); +export const KBN_KEY_PATH = resolve(__dirname, '../certs/kibana.key'); +export const KBN_CERT_PATH = resolve(__dirname, '../certs/kibana.crt'); +export const KBN_P12_PATH = resolve(__dirname, '../certs/kibana.p12'); +export const KBN_P12_PASSWORD = 'storepass'; diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 7bd22d73df8d5..e05f03eb344ca 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -25,7 +25,19 @@ export { ToolingLogCollectingWriter, } from './tooling_log'; export { createAbsolutePathSerializer } from './serializers'; -export { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs'; +export { + CA_CERT_PATH, + ES_KEY_PATH, + ES_CERT_PATH, + ES_P12_PATH, + ES_P12_PASSWORD, + ES_EMPTYPASSWORD_P12_PATH, + ES_NOPASSWORD_P12_PATH, + KBN_KEY_PATH, + KBN_CERT_PATH, + KBN_P12_PATH, + KBN_P12_PASSWORD, +} from './certs'; export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './constants'; export { KbnClient } from './kbn_client'; diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 976eac7f95da6..ecd8fb96ed3a3 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -119,6 +119,8 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { }); set('server.ssl.enabled', true); + // TODO: change this cert/key to KBN_CERT_PATH and KBN_KEY_PATH from '@kbn/dev-utils'; will require some work to avoid breaking + // functional tests. Once that is done, the existing test cert/key at DEV_SSL_CERT_PATH and DEV_SSL_KEY_PATH can be deleted. set('server.ssl.certificate', DEV_SSL_CERT_PATH); set('server.ssl.key', DEV_SSL_KEY_PATH); set('elasticsearch.hosts', elasticsearchHosts); diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index 1cf4cdf6064c1..4ce4c6002df24 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -17,6 +17,8 @@ * under the License. */ +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; + export default async function({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); @@ -39,8 +41,9 @@ export default async function({ readConfigFile }) { serverArgs: [ ...httpConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.key=${require.resolve('../../../dev_certs/server.key')}`, - `--server.ssl.certificate=${require.resolve('../../../dev_certs/server.crt')}`, + `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, // this is needed for the test runner to trust the server certificate + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, ], }, }; diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 36e68eaf8e345..56f8c5bf3f908 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -17,6 +17,8 @@ * under the License. */ +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; + import { KibanaSupertestProvider } from '../../services'; export default async function({ readConfigFile }) { @@ -54,8 +56,9 @@ export default async function({ readConfigFile }) { serverArgs: [ ...httpConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.key=${require.resolve('../../../dev_certs/server.key')}`, - `--server.ssl.certificate=${require.resolve('../../../dev_certs/server.crt')}`, + `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, // this is needed for the test runner to trust the server certificate + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, `--server.ssl.redirectHttpFromPort=${redirectPort}`, ], }, diff --git a/x-pack/test/pki_api_integration/config.ts b/x-pack/test/pki_api_integration/config.ts index 50b41ad251827..a445b3d4943b0 100644 --- a/x-pack/test/pki_api_integration/config.ts +++ b/x-pack/test/pki_api_integration/config.ts @@ -7,7 +7,7 @@ import { resolve } from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; // @ts-ignore -import { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from '@kbn/dev-utils'; +import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { services } from './services'; export default async function({ readConfigFile }: FtrConfigProviderContext) { @@ -54,8 +54,8 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.key=${ES_KEY_PATH}`, - `--server.ssl.certificate=${ES_CERT_PATH}`, + `--server.ssl.key=${KBN_KEY_PATH}`, + `--server.ssl.certificate=${KBN_CERT_PATH}`, `--server.ssl.certificateAuthorities=${JSON.stringify([ CA_CERT_PATH, resolve(__dirname, './fixtures/kibana_ca.crt'), diff --git a/x-pack/test/pki_api_integration/fixtures/README.md b/x-pack/test/pki_api_integration/fixtures/README.md index 0fcbc76183b48..ac2be482c6e33 100644 --- a/x-pack/test/pki_api_integration/fixtures/README.md +++ b/x-pack/test/pki_api_integration/fixtures/README.md @@ -1,7 +1,16 @@ # PKI Fixtures -* `es_ca.key` - the CA key used to sign certificates from @kbn/dev-utils that are used and trusted by test Elasticsearch server. -* `first_client.p12` and `second_client.p12` - the client certificate bundles signed by `es_ca.key` and hence trusted by -both test Kibana and Elasticsearch servers. +* `first_client.p12` and `second_client.p12` - the client certificate bundles signed by the Elastic Stack CA (in `kbn-dev-utils`) +and hence trusted by both test Kibana and Elasticsearch servers. * `untrusted_client.p12` - the client certificate bundle trusted by test Kibana server, but not test Elasticsearch test server. -* `kibana_ca.crt` and `kibana_ca.key` - the CA certificate and key trusted by test Kibana server only. \ No newline at end of file +* `kibana_ca.crt` and `kibana_ca.key` - the CA certificate and key trusted by test Kibana server only. + +The `first_client.p12` and `second_client.p12` files were generated the same time as the other certificates in `kbn-dev-utils`, using the +following commands: + +``` +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name first_client --pass "" +bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name second_client --pass "" +``` + +If that CA is ever changed, these two files must be regenerated. diff --git a/x-pack/test/pki_api_integration/fixtures/es_ca.key b/x-pack/test/pki_api_integration/fixtures/es_ca.key deleted file mode 100644 index 5428f86851e5a..0000000000000 --- a/x-pack/test/pki_api_integration/fixtures/es_ca.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAjSJiqfwPZfvgHO1OZbxzgPn2EW/KewIHXygTAdL926Pm6R45 -G5H972B46NcSUoOZbOhDyvg6OKMJAICiXa85yOf3nyTo4APspR+K4AH60SEJohRF -mZwL/OryfiKvN5n5DxC2+Hb1wouwBUJM6DP62C24ve8YWuWwNkhJqWKe1YQUzPc1 -svqvU5uaHTzvLtp++RqSDNkcIqWl5S9Ip5PtOv6MHkCaIr2g4KQzplFwhT5qVd1Q -nYVBsQ0D8htLqUJBfjW0KHouEZpbjxJlc+EuyExS1o1+y3mVT+t2yZHAoIquh5ve -5A7a/RGJTyoR5u1DFs4Tcx2378kjA86gCQtClwIDAQABAoIBAFTOGKMzxrztQJmh -Lr6LIoyZpnaLygtoCK3xEprCAbB9KD9j3cTnUMMKIR0oPuY+FW8Pkczgo3ts2/fl -U6sfo4VJfc2vDA+vy/7cmUJJbkFDrNorfDb1QW7UbqnEhazPZIzc6lUahkpETZyb -XkMZGN3Ve3EFvojAA8ZaYYjarb52HRddLPZJ7c8ZiHfJ1jHNIvx6dIQ6CJVuovBJ -OGbbSAK8MjUtOI2XzWNHgUqGHcjVDFysuAac3ckK14TaN4KVNRl+usAMkZwqSM5u -j/ATFL9hx7nkzh3KWPsuOLMoLX7JN81z0YtT52wTxJoSiZKk/u91JHZ3NcrsOSPS -oLvVkyECgYEA16qtXvtmboAbqeuXf0nF+7QD0b+MdaRFIacqTG0LpEgY9Tjgs9Pn -6z44tHABWPVkRLNQZiky99MAq4Ci354Bk9dmylCw9ADH78VGmKWklbQEr1rw4dqm -DHTj9NQ79SyTdiasQjnnxCilWkrO6ZUqD8og4DT5MhzfxO/ZND8arGsCgYEAp4df -oI5lwlc1n9X/G9RQAKwNM5un8RmReleUVemjkcvWwvZVEjV0Gcc1WtjB+77Y5B9B -CM3laURDGrAgX5VS/I2jb0xqBNUr8XccSkDQAP9UuVPZgxpS+8d0R3fxVzniHWwR -WC2dW/Is40i/6+7AkFXhkiFiqxkvSg4pWHPazYUCgYB/gP7C+urSRZcVXJ3SuXD9 -oK3pYc/O9XGRtd0CFi4d0CpBQIFIj+27XKv1sYp6Z4oCO+k6nPzvG6Z3vrOMdUQF -fgHddttHRvbtwLo+ISAvCaEDc0aaoMQu9SSYaKmSB+qenbqV5NorVMR9n2C5JGEb -uKq7I1Z41C1Pp2XIx84jRQKBgQCjKvfZsjesZDJnfg9dtJlDPlARXt7gte16gkiI -sOnOfAGtnCzZclSlMuBlnk65enVXIpW+FIQH1iOhn7+4OQE92FpBceSk1ldZdJCK -RbwR7J5Bb0igJ4iBkA9R+KGIOmlgDLyL7MmiHyrXKCk9iynkqrDsGjY2vW3QrCBa -9WQ73QKBgQDAYZzplO4TPoPK9AnxoW/HpSwGEO7Fb8fLyPg94CvHn4QBCFJUKuTn -hBp/TJgF6CjQWQMr2FKVFF33Ow7+Qa96YGvmYlEjR/71D4Rlprj5JJpuO154DI3I -YIMNTjvwEQEI+YamMarKsz0Kq+I1EYSAf6bQ4H2PgxDxwTXaLkl0RA== ------END RSA PRIVATE KEY----- diff --git a/x-pack/test/pki_api_integration/fixtures/first_client.p12 b/x-pack/test/pki_api_integration/fixtures/first_client.p12 index 62da80d9ab80efbedd24edf322acbc1aecc38ef8..9d838199e8392672bfff64155616ac536acb4621 100644 GIT binary patch delta 3295 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 diff --git a/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml b/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml index 132faffd41667..a890fe812987b 100644 --- a/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml +++ b/x-pack/test/saml_api_integration/fixtures/idp_metadata.xml @@ -6,25 +6,25 @@ - - MIIDMDCCAhgCCQCkOD7fnHiQrTANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB - VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 - cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwIBcNMTYwMTE5MjExNjQ5WhgP - MjA4NDAyMDYyMTE2NDlaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 - YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMT - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALR63SnR - LW/Dgl0Vuy8gB6KjWwvajRWeXNgvlf6LX56oNsUFREHBLQlC2uT5R26F3tOqCDbs - MFNoyDjMBinXRRFJJ2Sokue7GSsGvBv41LMTHnO/MeCCbEOqghJS/QI89cV+u+Aw - 9U+v426KAlCa1sGuE2+3/JvqdBQyheiukmGLiJ0OofpfgpYuFmKi2uYBKU3qzjUx - D01wQ4rCpq5nEnksGhgBeBDnheYmmDsj/wDvnz1exK/WprvTiHQ5MwuIQ4OybwgV - WDF+zv8PXrObrsZvD/ulrjh1cakvnCe2kDYEKMRiHUDesHS2jNJkBUe+FJo4/E3U - pFoYOtdoBe69BIUCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAQhjF91G0R662XJJ7 - jGMudA9VbRVCow8s68I/GWPZmpKxPAxwz0xiv1eFIoiP416LX9amnx3yAmUoN4Wr - Cq0jsgyT1AOiSCdxkvYsqQG3SFVVt5BDLjThH66Vxi7Bach6SyATa1NG588mg7n9 - pPJ4A1rcj+5kZuwnd52kfVLP+535lylwMyoyJa2AskieRPLNSzus2eUDTR6F+9Mb - eLOwp5rMl2nNYfLXUCSqEeC6uPu0yq6Tu0N0SjThfKndd2NU1fk3zyOjxyCIhGPe - G8VhrPY4lkJ9EE9Tuq095jwd1+q9fYzlKZWhOmg+IcOwUMgbgeWpeZTAhUIZAnia - 4UH6NA== + + MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w +DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ +wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU +FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q +OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ +s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU +vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T +BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz +V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE +DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/ +z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv ++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX +TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy +b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk +cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O +eOUsdwn1yDKHRxDHyA== diff --git a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts index f8862e6fb209d..b7b94b8eeb17a 100644 --- a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts +++ b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts @@ -12,6 +12,7 @@ import zlib from 'zlib'; import { promisify } from 'util'; import { parseString } from 'xml2js'; import { SignedXml } from 'xml-crypto'; +import { KBN_KEY_PATH } from '@kbn/dev-utils'; /** * @file Defines a set of tools that allow us to parse and generate various SAML XML messages. @@ -24,7 +25,7 @@ const inflateRawAsync = promisify(zlib.inflateRaw); const deflateRawAsync = promisify(zlib.deflateRaw); const parseStringAsync = promisify(parseString); -const signingKey = fs.readFileSync(require.resolve('../../../../test/dev_certs/server.key')); +const signingKey = fs.readFileSync(KBN_KEY_PATH); const signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; export async function getSAMLRequestId(urlWithSAMLRequestId: string) { From 3224ee1cf656851563f888d9efdece6779e815b9 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:45:13 -0500 Subject: [PATCH 02/21] Change certificate/keypair that are used for kbn-es --- packages/kbn-es/src/cluster.js | 8 ++++---- .../src/integration_tests/__fixtures__/es_bin.js | 2 ++ .../kbn-es/src/integration_tests/cluster.test.js | 14 +++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 665f80e3802e3..ceb4a5b6aece1 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -35,7 +35,7 @@ const { createCliError } = require('./errors'); const { promisify } = require('util'); const treeKillAsync = promisify(require('tree-kill')); const { parseSettings, SettingsFilter } = require('./settings'); -const { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); +const { CA_CERT_PATH, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils'); const readFile = util.promisify(fs.readFile); // listen to data on stream until map returns anything but undefined @@ -261,9 +261,9 @@ exports.Cluster = class Cluster { const esArgs = [].concat(options.esArgs || []); if (this._ssl) { esArgs.push('xpack.security.http.ssl.enabled=true'); - esArgs.push(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); - esArgs.push(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); - esArgs.push(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + esArgs.push(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`); + esArgs.push(`xpack.security.http.ssl.keystore.type=PKCS12`); + esArgs.push(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`); } const args = parseSettings(extractConfigFiles(esArgs, installPath, { log: this._log }), { diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js index d3181a748ffbb..d374abe5db068 100644 --- a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js +++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js @@ -34,6 +34,8 @@ if (!start) { let serverUrl; const server = createServer( { + // Note: the integration uses the ES_P12_PATH, but that keystore contains + // the same key/cert as ES_KEY_PATH and ES_CERT_PATH key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined, cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined, }, diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index dd570e27e3282..dfbc04477bd40 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -17,7 +17,7 @@ * under the License. */ -const { ToolingLog, CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils'); +const { ToolingLog, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils'); const execa = require('execa'); const { Cluster } = require('../cluster'); const { installSource, installSnapshot, installArchive } = require('../install'); @@ -252,9 +252,9 @@ describe('#start(installPath)', () => { const config = extractConfigFiles.mock.calls[0][0]; expect(config).toContain('xpack.security.http.ssl.enabled=true'); - expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.type=PKCS12`); + expect(config).toContain(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`); }); it(`doesn't setup SSL when disabled`, async () => { @@ -319,9 +319,9 @@ describe('#run()', () => { const config = extractConfigFiles.mock.calls[0][0]; expect(config).toContain('xpack.security.http.ssl.enabled=true'); - expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`); - expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`); + expect(config).toContain(`xpack.security.http.ssl.keystore.type=PKCS12`); + expect(config).toContain(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`); }); it(`doesn't setup SSL when disabled`, async () => { From 5a3837afbb9698283d61beb6db3a4d5ce4c87397 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:45:20 -0500 Subject: [PATCH 03/21] Add crypto utils and node-forge module Not positive that this code should remain here, open to suggestions. --- package.json | 2 + renovate.json5 | 8 ++ src/core/utils/crypto/index.ts | 20 ++++ src/core/utils/crypto/pkcs12.test.ts | 141 ++++++++++++++++++++++++++ src/core/utils/crypto/pkcs12.ts | 144 +++++++++++++++++++++++++++ src/core/utils/index.ts | 1 + yarn.lock | 12 +++ 7 files changed, 328 insertions(+) create mode 100644 src/core/utils/crypto/index.ts create mode 100644 src/core/utils/crypto/pkcs12.test.ts create mode 100644 src/core/utils/crypto/pkcs12.ts diff --git a/package.json b/package.json index 651ffb60d7b88..cdf62b9c34056 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "@kbn/ui-framework": "1.0.0", "@types/json-stable-stringify": "^1.0.32", "@types/lodash.clonedeep": "^4.5.4", + "@types/node-forge": "^0.9.0", "@types/react-grid-layout": "^0.16.7", "@types/recompose": "^0.30.5", "JSONStream": "1.3.5", @@ -214,6 +215,7 @@ "mustache": "2.3.2", "ngreact": "0.5.1", "node-fetch": "1.7.3", + "node-forge": "^0.9.1", "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", diff --git a/renovate.json5 b/renovate.json5 index ecc9b3b2ceb62..f505da16d5202 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -297,6 +297,14 @@ '@types/lodash.clonedeep', ], }, + { + groupSlug: 'node-forge', + groupName: 'node-forge related packages', + packageNames: [ + 'node-forge', + '@types/node-forge', + ], + }, { groupSlug: 'bluebird', groupName: 'bluebird related packages', diff --git a/src/core/utils/crypto/index.ts b/src/core/utils/crypto/index.ts new file mode 100644 index 0000000000000..9a36682cc4ecb --- /dev/null +++ b/src/core/utils/crypto/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { Pkcs12ReadResult, readPkcs12Keystore, readPkcs12Truststore } from './pkcs12'; diff --git a/src/core/utils/crypto/pkcs12.test.ts b/src/core/utils/crypto/pkcs12.test.ts new file mode 100644 index 0000000000000..f775d37af7764 --- /dev/null +++ b/src/core/utils/crypto/pkcs12.test.ts @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + CA_CERT_PATH, + ES_KEY_PATH, + ES_CERT_PATH, + ES_P12_PATH, + ES_P12_PASSWORD, + ES_EMPTYPASSWORD_P12_PATH, + ES_NOPASSWORD_P12_PATH, +} from '@kbn/dev-utils'; +import { readFileSync } from 'fs'; + +import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from './pkcs12'; + +const readPem = (file: string) => { + const raw = readFileSync(file, 'utf8'); + // strip bag attributes that are included from a previous PKCS #12 export + return raw.substr(raw.indexOf('-----BEGIN')).trim(); +}; + +describe('#readPkcs12Keystore', () => { + describe('Succeeds when the correct password is used', () => { + let pkcs12ReadResult: Pkcs12ReadResult; + + it('Reads the keystore without error', () => { + // this is expensive, just do it once + pkcs12ReadResult = readPkcs12Keystore(ES_P12_PATH, ES_P12_PASSWORD); + }); + + it('Extracts the PEM key', () => { + const pemKey = readPem(ES_KEY_PATH); + expect(pkcs12ReadResult.key).toEqual(pemKey); + }); + + it('Extracts the PEM instance certificate', () => { + const pemCert = readPem(ES_CERT_PATH); + expect(pkcs12ReadResult.cert).toEqual(pemCert); + }); + + it('Extracts the PEM CA certificate', () => { + const pemCA = readPem(CA_CERT_PATH); + expect(pkcs12ReadResult.ca).toEqual([pemCA]); + }); + }); + + describe('Succeeds on a key store with an empty password', () => { + let pkcs12ReadResult: Pkcs12ReadResult; + + it('Reads the keystore without error', () => { + // this is expensive, just do it once + pkcs12ReadResult = readPkcs12Keystore(ES_EMPTYPASSWORD_P12_PATH, ''); + }); + + it('Extracts the PEM key', () => { + const pemKey = readPem(ES_KEY_PATH); + expect(pkcs12ReadResult.key).toEqual(pemKey); + }); + + it('Extracts the PEM instance certificate', () => { + const pemCert = readPem(ES_CERT_PATH); + expect(pkcs12ReadResult.cert).toEqual(pemCert); + }); + + it('Extracts the PEM CA certificate', () => { + const pemCA = readPem(CA_CERT_PATH); + expect(pkcs12ReadResult.ca).toEqual([pemCA]); + }); + }); + + describe('Succeeds on a key store with no password', () => { + let pkcs12ReadResult: Pkcs12ReadResult; + + it('Reads the keystore without error', () => { + // this is expensive, just do it once + pkcs12ReadResult = readPkcs12Keystore(ES_NOPASSWORD_P12_PATH); + }); + + it('Extracts the PEM key', () => { + const pemKey = readPem(ES_KEY_PATH); + expect(pkcs12ReadResult.key).toEqual(pemKey); + }); + + it('Extracts the PEM instance certificate', () => { + const pemCert = readPem(ES_CERT_PATH); + expect(pkcs12ReadResult.cert).toEqual(pemCert); + }); + + it('Extracts the PEM CA certificate', () => { + const pemCA = readPem(CA_CERT_PATH); + expect(pkcs12ReadResult.ca).toEqual([pemCA]); + }); + }); + + describe('Throws when an invalid password is used', () => { + const expectError = (password?: string) => { + expect(() => readPkcs12Keystore(ES_P12_PATH, password)).toThrowError( + 'PKCS#12 MAC could not be verified. Invalid password?' + ); + }; + + it('Incorrect password', () => { + expectError('incorrect'); + }); + + it('Empty password', () => { + expectError(''); + }); + + it('Undefined password', () => { + expectError(); + }); + }); +}); + +describe('#readPkcs12Truststore', () => { + it('reads all certificates into one CA array and discards any keys', () => { + const ca = readPkcs12Truststore(ES_P12_PATH, ES_P12_PASSWORD); + const pemCert = readPem(ES_CERT_PATH); + const pemCA = readPem(CA_CERT_PATH); + + expect(ca).toEqual([pemCert, pemCA]); + }); +}); diff --git a/src/core/utils/crypto/pkcs12.ts b/src/core/utils/crypto/pkcs12.ts new file mode 100644 index 0000000000000..ad600e905e705 --- /dev/null +++ b/src/core/utils/crypto/pkcs12.ts @@ -0,0 +1,144 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { util, asn1, pkcs12, pki } from 'node-forge'; +import { readFileSync } from 'fs'; + +export interface Pkcs12ReadResult { + ca?: string[]; + cert?: string; + key?: string; +} + +/** + * Reads a private key and certificate chain from a PKCS12 key store. + * + * @remarks + * The PKCS12 key store may contain the following: + * - 0 or more certificates contained in a `certBag` (OID + * 1.2.840.113549.1.12.10.1.3); the first is treated as an instance + * certificate, and any others are treated as CA certificates + * - 0 or 1 private keys contained in a `keyBag` (OID + * 1.2.840.113549.1.12.10.1.1) or a `pkcs8ShroudedKeyBag` (OID + * 1.2.840.113549.1.12.10.1.2) + * + * Any other PKCS12 bags are ignored. + * + * @privateRemarks + * This intentionally does not allow for a separate key store password and + * private key password. In conventional implementations, these two values + * are expected to be identical, so we do not support other configurations. + * + * @param path The file path of the PKCS12 key store + * @param password The optional password of the key store and private key; + * if there is no password, this may be an empty string or `undefined`, + * depending on how the key store was generated. + * @returns the parsed private key and certificate(s) in PEM format + */ +export const readPkcs12Keystore = (path: string, password?: string): Pkcs12ReadResult => { + const p12base64 = readFileSync(path, 'base64'); + const p12Der = util.decode64(p12base64); + const p12Asn1 = asn1.fromDer(p12Der); + const p12 = pkcs12.pkcs12FromAsn1(p12Asn1, password); + const { ca, cert } = getCerts(p12); + const key = getKey(p12); + return { ca, cert, key }; +}; + +/** + * Reads a certificate chain from a PKCS12 trust store. + * + * @remarks + * The PKCS12 trust store may contain the following: + * - 0 or more certificates contained in a `certBag` (OID + * 1.2.840.113549.1.12.10.1.3); all are treated as CA certificates + * + * Any other PKCS12 bags are ignored. + * + * @param path The file path of the PKCS12 trust store + * @param password The optional password of the trust store; if there is + * no password, this may be an empty string or `undefined`, depending on + * how the trust store was generated. + * @returns the parsed certificate(s) in PEM format + */ +export const readPkcs12Truststore = (path: string, password?: string): string[] | undefined => { + const p12base64 = readFileSync(path, 'base64'); + const p12Der = util.decode64(p12base64); + const p12Asn1 = asn1.fromDer(p12Der); + const p12 = pkcs12.pkcs12FromAsn1(p12Asn1, password); + const { ca } = getCerts(p12, true); + return ca; +}; + +const getCerts = (p12: pkcs12.Pkcs12Pfx, combineBags: boolean = false) => { + // OID 1.2.840.113549.1.12.10.1.3 (certBag) + const bags = getBags(p12, pki.oids.certBag); + let ca: string[] | undefined; + let cert: string | undefined; + if (bags && bags.length) { + if (!combineBags) { + const certBag = bags.shift(); + if (certBag) cert = convertCert(certBag); + } + if (bags.length) { + ca = bags.map(convertCert).filter(x => x !== undefined) as string[]; + } + } + return { ca, cert }; +}; + +const convertCert = (bag: pkcs12.Bag | undefined) => { + if (bag) { + const cert = bag.cert; + if (cert) { + const pem = pki.certificateToPem(cert); + return reformatPem(pem); + } + } + return undefined; +}; + +const getKey = (p12: pkcs12.Pkcs12Pfx) => { + // OID 1.2.840.113549.1.12.10.1.1 (keyBag) || OID 1.2.840.113549.1.12.10.1.2 (pkcs8ShroudedKeyBag) + const bags = getBags(p12, pki.oids.keyBag) || getBags(p12, pki.oids.pkcs8ShroudedKeyBag); + if (bags && bags.length) { + if (bags.length > 1) { + throw new Error(`Keystore contains multiple private keys.`); + } + const key = bags[0].key; + if (key) { + const pem = pki.privateKeyToPem(key); + return reformatPem(pem); + } + } + return undefined; +}; + +const getBags = (p12: pkcs12.Pkcs12Pfx, bagType: string) => { + const bagObj = p12.getBags({ bagType }); + const bags = bagObj[bagType]; + if (bags && bags.length) { + return bags; + } + return undefined; +}; + +const reformatPem = (pem: string) => { + return pem.replace(/\r\n/g, '\n').trim(); +}; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index b51cc4ef56410..a29bdba818461 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -19,6 +19,7 @@ export * from './assert_never'; export * from './context'; +export * from './crypto'; export * from './deep_freeze'; export * from './get'; export * from './map_to_object'; diff --git a/yarn.lock b/yarn.lock index 7528db731e587..5a7b825baede4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3897,6 +3897,13 @@ dependencies: "@types/node" "*" +"@types/node-forge@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.0.tgz#e9f678ec09283f9f35cb8de6c01f86be9278ac08" + integrity sha512-J00+BIHJOfagO1Qs67Jp5CZO3VkFxY8YKMt44oBhXr+3ZYNnl8wv/vtcJyPjuH0QZ+q7+5nnc6o/YH91ZJy2pQ== + dependencies: + "@types/node" "*" + "@types/node-jose@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/node-jose/-/node-jose-1.1.0.tgz#26e1d234b41a39035482443ef35414bf34ba5d8b" @@ -20329,6 +20336,11 @@ node-forge@^0.7.6: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== +node-forge@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" + integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== + node-gyp@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" From e4a77dd0ef68ad8e228455166ad6c11c9d7f05d8 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:45:26 -0500 Subject: [PATCH 04/21] Migrated elasticsearch username deprecation The Kibana Platform supports configuration deprecations now, so we can remove the custom deprecation logging that we had previously. --- .../deprecation/core_deprecations.test.ts | 31 +++++++++++++++++++ .../config/deprecation/core_deprecations.ts | 11 +++++++ .../elasticsearch/elasticsearch_config.ts | 9 ------ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index b40dbdc1b6651..7851522ec899f 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -208,4 +208,35 @@ describe('core deprecations', () => { ).toEqual([`worker-src blob:`]); }); }); + + describe('elasticsearchUsernameDeprecation', () => { + it('logs a warning if elasticsearch.username is set to "elastic"', () => { + const { messages } = applyCoreDeprecations({ + elasticsearch: { + username: 'elastic', + }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting elasticsearch.username to \\"elastic\\" is deprecated. You should use the \\"kibana\\" user instead.", + ] + `); + }); + + it('does not log a warning if elasticsearch.username is set to something besides "elastic"', () => { + const { messages } = applyCoreDeprecations({ + elasticsearch: { + username: 'otheruser', + }, + }); + expect(messages).toHaveLength(0); + }); + + it('does not log a warning if elasticsearch.username is unset', () => { + const { messages } = applyCoreDeprecations({ + elasticsearch: {}, + }); + expect(messages).toHaveLength(0); + }); + }); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 6a401ec6625a2..515baa4d382ba 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -91,6 +91,16 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; +const elasticsearchUsernameDeprecation: ConfigDeprecation = (settings, _fromPath, log) => { + const username: string | undefined = get(settings, 'elasticsearch.username'); + if (username === 'elastic') { + log( + `Setting elasticsearch.username to "elastic" is deprecated. You should use the "kibana" user instead.` + ); + } + return settings; +}; + export const coreDeprecationProvider: ConfigDeprecationProvider = ({ unusedFromRoot, renameFromRoot, @@ -111,4 +121,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ dataPathDeprecation, rewriteBasePathDeprecation, cspRulesDeprecation, + elasticsearchUsernameDeprecation, ]; diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 8d92b12ae4a77..4330d7769ef1e 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -212,14 +212,5 @@ export class ElasticsearchConfig { ...rawConfig.ssl, certificateAuthorities, }; - - if (this.username === 'elastic' && log !== undefined) { - // logger is optional / not used during tests - // TODO: logger can be removed when issue #40255 is resolved to support deprecations in NP config service - log.warn( - `Setting the elasticsearch username to "elastic" is deprecated. You should use the "kibana" user instead.`, - { tags: ['deprecation'] } - ); - } } } From b7ea8a6f3d0c94b007b6d6ee60c5adef6e501c22 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:45:33 -0500 Subject: [PATCH 05/21] Require logger for ElasticsearchConfig Logger was previously optional to avoid changing tests. Now it is mandatory and the tests have been changed to mock the logger. --- .../elasticsearch_config.test.ts | 31 ++++++++++++------- .../elasticsearch/elasticsearch_config.ts | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 1d919639abe5f..ce9616ed3fe4e 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -17,10 +17,19 @@ * under the License. */ -import { ElasticsearchConfig, config } from './elasticsearch_config'; +import { ElasticsearchConfig, config, ElasticsearchConfigType } from './elasticsearch_config'; +import { loggingServiceMock } from '../mocks'; +import { Logger } from '../logging'; + +const createElasticsearchConfig = (rawConfig: ElasticsearchConfigType, log?: Logger) => { + if (!log) { + log = loggingServiceMock.create().get('config'); + } + return new ElasticsearchConfig(rawConfig, log); +}; test('set correct defaults', () => { - const configValue = new ElasticsearchConfig(config.schema.validate({})); + const configValue = createElasticsearchConfig(config.schema.validate({})); expect(configValue).toMatchInlineSnapshot(` ElasticsearchConfig { "apiVersion": "master", @@ -52,17 +61,17 @@ test('set correct defaults', () => { }); test('#hosts accepts both string and array of strings', () => { - let configValue = new ElasticsearchConfig( + let configValue = createElasticsearchConfig( config.schema.validate({ hosts: 'http://some.host:1234' }) ); expect(configValue.hosts).toEqual(['http://some.host:1234']); - configValue = new ElasticsearchConfig( + configValue = createElasticsearchConfig( config.schema.validate({ hosts: ['http://some.host:1234'] }) ); expect(configValue.hosts).toEqual(['http://some.host:1234']); - configValue = new ElasticsearchConfig( + configValue = createElasticsearchConfig( config.schema.validate({ hosts: ['http://some.host:1234', 'https://some.another.host'], }) @@ -71,17 +80,17 @@ test('#hosts accepts both string and array of strings', () => { }); test('#requestHeadersWhitelist accepts both string and array of strings', () => { - let configValue = new ElasticsearchConfig( + let configValue = createElasticsearchConfig( config.schema.validate({ requestHeadersWhitelist: 'token' }) ); expect(configValue.requestHeadersWhitelist).toEqual(['token']); - configValue = new ElasticsearchConfig( + configValue = createElasticsearchConfig( config.schema.validate({ requestHeadersWhitelist: ['token'] }) ); expect(configValue.requestHeadersWhitelist).toEqual(['token']); - configValue = new ElasticsearchConfig( + configValue = createElasticsearchConfig( config.schema.validate({ requestHeadersWhitelist: ['token', 'X-Forwarded-Proto'], }) @@ -90,17 +99,17 @@ test('#requestHeadersWhitelist accepts both string and array of strings', () => }); test('#ssl.certificateAuthorities accepts both string and array of strings', () => { - let configValue = new ElasticsearchConfig( + let configValue = createElasticsearchConfig( config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } }) ); expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); - configValue = new ElasticsearchConfig( + configValue = createElasticsearchConfig( config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } }) ); expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); - configValue = new ElasticsearchConfig( + configValue = createElasticsearchConfig( config.schema.validate({ ssl: { certificateAuthorities: ['some-path', 'another-path'] }, }) diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 4330d7769ef1e..b2735ee84b3db 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -183,7 +183,7 @@ export class ElasticsearchConfig { */ public readonly customHeaders: ElasticsearchConfigType['customHeaders']; - constructor(rawConfig: ElasticsearchConfigType, log?: Logger) { + constructor(rawConfig: ElasticsearchConfigType, log: Logger) { this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch; this.apiVersion = rawConfig.apiVersion; this.logQueries = rawConfig.logQueries; From 76d1441535a4bacc960f8bfb4bc308f246d8366f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:45:40 -0500 Subject: [PATCH 06/21] Change Elasticsearch clients to read certs/keys upon config parse --- .../elasticsearch_client_config.test.ts | 21 ++- .../elasticsearch_client_config.ts | 13 +- ....ts => elasticsearch_config.test.mocks.ts} | 0 .../elasticsearch_config.test.ts | 126 +++++++++++++++--- .../elasticsearch/elasticsearch_config.ts | 48 ++++++- .../elasticsearch_service.test.ts | 4 + .../__tests__/elasticsearch_proxy_config.js | 39 +++--- .../console/server/__tests__/fixtures/ca.crt | 1 - .../server/__tests__/fixtures/cert.crt | 1 - .../server/__tests__/fixtures/cert.key | 1 - .../console/server/__tests__/proxy_config.js | 33 ++--- .../server/elasticsearch_proxy_config.ts | 23 +--- .../console/server/proxy_config.js | 7 +- 13 files changed, 204 insertions(+), 113 deletions(-) rename src/core/server/elasticsearch/{elasticsearch_client_config.test.mocks.ts => elasticsearch_config.test.mocks.ts} (100%) delete mode 100644 src/legacy/core_plugins/console/server/__tests__/fixtures/ca.crt delete mode 100644 src/legacy/core_plugins/console/server/__tests__/fixtures/cert.crt delete mode 100644 src/legacy/core_plugins/console/server/__tests__/fixtures/cert.key diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts index 64fb41cb3e4e5..20c10459e0e8a 100644 --- a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts @@ -17,8 +17,6 @@ * under the License. */ -import { mockReadFileSync } from './elasticsearch_client_config.test.mocks'; - import { duration } from 'moment'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { @@ -66,8 +64,6 @@ Object { }); test('parses fully specified config', () => { - mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); - const elasticsearchConfig: ElasticsearchClientConfig = { apiVersion: 'v7.0.0', customHeaders: { xsrf: 'something' }, @@ -87,9 +83,9 @@ test('parses fully specified config', () => { sniffInterval: 11223344, ssl: { verificationMode: 'certificate', - certificateAuthorities: ['ca-path-1', 'ca-path-2'], - certificate: 'certificate-path', - key: 'key-path', + certificateAuthorities: ['content-of-ca-path-1', 'content-of-ca-path-2'], + certificate: 'content-of-certificate-path', + key: 'content-of-key-path', keyPassphrase: 'key-pass', alwaysPresentCertificate: true, }, @@ -497,6 +493,7 @@ Object { "sniffOnConnectionFault": true, "sniffOnStart": true, "ssl": Object { + "ca": undefined, "rejectUnauthorized": false, }, } @@ -541,6 +538,7 @@ Object { "sniffOnConnectionFault": true, "sniffOnStart": true, "ssl": Object { + "ca": undefined, "checkServerIdentity": [Function], "rejectUnauthorized": true, }, @@ -581,6 +579,7 @@ Object { "sniffOnConnectionFault": true, "sniffOnStart": true, "ssl": Object { + "ca": undefined, "rejectUnauthorized": true, }, } @@ -606,8 +605,6 @@ Object { }); test('#ignoreCertAndKey = true', () => { - mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); - expect( parseElasticsearchClientConfig( { @@ -620,9 +617,9 @@ Object { requestHeadersWhitelist: [], ssl: { verificationMode: 'certificate', - certificateAuthorities: ['ca-path'], - certificate: 'certificate-path', - key: 'key-path', + certificateAuthorities: ['content-of-ca-path'], + certificate: 'content-of-certificate-path', + key: 'content-of-key-path', keyPassphrase: 'key-pass', alwaysPresentCertificate: true, }, diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.ts b/src/core/server/elasticsearch/elasticsearch_client_config.ts index dcc09f711abbe..287d835c40351 100644 --- a/src/core/server/elasticsearch/elasticsearch_client_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_client_config.ts @@ -18,7 +18,6 @@ */ import { ConfigOptions } from 'elasticsearch'; -import { readFileSync } from 'fs'; import { cloneDeep } from 'lodash'; import { Duration } from 'moment'; import { checkServerIdentity } from 'tls'; @@ -165,18 +164,12 @@ export function parseElasticsearchClientConfig( throw new Error(`Unknown ssl verificationMode: ${verificationMode}`); } - const readFile = (file: string) => readFileSync(file, 'utf8'); - if ( - config.ssl.certificateAuthorities !== undefined && - config.ssl.certificateAuthorities.length > 0 - ) { - esClientConfig.ssl.ca = config.ssl.certificateAuthorities.map(readFile); - } + esClientConfig.ssl.ca = config.ssl.certificateAuthorities; // Add client certificate and key if required by elasticsearch if (!ignoreCertAndKey && config.ssl.certificate && config.ssl.key) { - esClientConfig.ssl.cert = readFile(config.ssl.certificate); - esClientConfig.ssl.key = readFile(config.ssl.key); + esClientConfig.ssl.cert = config.ssl.certificate; + esClientConfig.ssl.key = config.ssl.key; esClientConfig.ssl.passphrase = config.ssl.keyPassphrase; } diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts similarity index 100% rename from src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts rename to src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index ce9616ed3fe4e..3a06f46e715ea 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -17,6 +17,8 @@ * under the License. */ +import { mockReadFileSync } from './elasticsearch_config.test.mocks'; + import { ElasticsearchConfig, config, ElasticsearchConfigType } from './elasticsearch_config'; import { loggingServiceMock } from '../mocks'; import { Logger } from '../logging'; @@ -52,7 +54,10 @@ test('set correct defaults', () => { "sniffOnStart": false, "ssl": Object { "alwaysPresentCertificate": false, + "certificate": undefined, "certificateAuthorities": undefined, + "key": undefined, + "keyPassphrase": undefined, "verificationMode": "full", }, "username": undefined, @@ -98,23 +103,114 @@ test('#requestHeadersWhitelist accepts both string and array of strings', () => expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']); }); -test('#ssl.certificateAuthorities accepts both string and array of strings', () => { - let configValue = createElasticsearchConfig( - config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } }) - ); - expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); +describe('reads files', () => { + beforeEach(() => { + mockReadFileSync.mockReset(); + mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + }); - configValue = createElasticsearchConfig( - config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } }) - ); - expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']); + it('reads certificate authorities when ssl.certificateAuthorities is specified', () => { + let configValue = createElasticsearchConfig( + config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); - configValue = createElasticsearchConfig( - config.schema.validate({ - ssl: { certificateAuthorities: ['some-path', 'another-path'] }, - }) - ); - expect(configValue.ssl.certificateAuthorities).toEqual(['some-path', 'another-path']); + mockReadFileSync.mockClear(); + configValue = createElasticsearchConfig( + config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = createElasticsearchConfig( + config.schema.validate({ + ssl: { certificateAuthorities: ['some-path', 'another-path'] }, + }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path', + 'content-of-another-path', + ]); + }); + + it('reads a private key when ssl.key is specified', () => { + const configValue = createElasticsearchConfig( + config.schema.validate({ ssl: { key: 'some-path' } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path'); + }); + + it('reads a certificate when ssl.certificate is specified', () => { + const configValue = createElasticsearchConfig( + config.schema.validate({ ssl: { certificate: 'some-path' } }) + ); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificate).toEqual('content-of-some-path'); + }); +}); + +describe('throws when config is invalid', () => { + beforeAll(() => { + const realFs = jest.requireActual('fs'); + mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + }); + + it('throws if key is invalid', () => { + const value = { ssl: { key: '/invalid/key' } }; + expect(() => + createElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/key'"` + ); + }); + + it('throws if certificate is invalid', () => { + const value = { ssl: { certificate: '/invalid/cert' } }; + expect(() => + createElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/cert'"` + ); + }); + + it('throws if certificateAuthorities is invalid', () => { + const value = { ssl: { certificateAuthorities: '/invalid/ca' } }; + expect(() => + createElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot(`"ENOENT: no such file or directory, open '/invalid/ca'"`); + }); +}); + +describe('logs warnings', () => { + let logger: ReturnType; + let log: Logger; + + beforeAll(() => { + mockReadFileSync.mockResolvedValue('foo'); + }); + + beforeEach(() => { + logger = loggingServiceMock.create(); + log = logger.get('config'); + }); + + it('warns if ssl.key is set and ssl.certificate is not', () => { + createElasticsearchConfig(config.schema.validate({ ssl: { key: 'some-path' } }), log); + expect(loggingServiceMock.collect(logger).warn[0][0]).toMatchInlineSnapshot( + `"Detected a key without a certificate; mutual TLS authentication is disabled."` + ); + }); + + it('warns if ssl.certificate is set and ssl.key is not', () => { + createElasticsearchConfig(config.schema.validate({ ssl: { certificate: 'some-path' } }), log); + expect(loggingServiceMock.collect(logger).warn[0][0]).toMatchInlineSnapshot( + `"Detected a certificate without a key; mutual TLS authentication is disabled."` + ); + }); }); test('#username throws if equal to "elastic", only while running from source', () => { diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index b2735ee84b3db..4c758f56d75e5 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -19,6 +19,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Duration } from 'moment'; +import { readFileSync } from 'fs'; import { Logger } from '../logging'; const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); @@ -202,15 +203,50 @@ export class ElasticsearchConfig { this.password = rawConfig.password; this.customHeaders = rawConfig.customHeaders; - const certificateAuthorities = Array.isArray(rawConfig.ssl.certificateAuthorities) - ? rawConfig.ssl.certificateAuthorities - : typeof rawConfig.ssl.certificateAuthorities === 'string' - ? [rawConfig.ssl.certificateAuthorities] - : undefined; + const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; + + let key: string | undefined; + let keyPassphrase: string | undefined; + let certificate: string | undefined; + let certificateAuthorities: string[] | undefined; + + if (rawConfig.ssl.key) { + key = readFile(rawConfig.ssl.key); + keyPassphrase = rawConfig.ssl.keyPassphrase; + } + if (rawConfig.ssl.certificate) { + certificate = readFile(rawConfig.ssl.certificate); + } + + const ca = rawConfig.ssl.certificateAuthorities; + if (ca) { + const parsed = []; + const paths = Array.isArray(ca) ? ca : [ca]; + if (paths.length > 0) { + for (const path of paths) { + parsed.push(readFile(path)); + } + certificateAuthorities = parsed; + } + } + + if (key && !certificate) { + log.warn(`Detected a key without a certificate; mutual TLS authentication is disabled.`); + } else if (certificate && !key) { + log.warn(`Detected a certificate without a key; mutual TLS authentication is disabled.`); + } this.ssl = { - ...rawConfig.ssl, + alwaysPresentCertificate, + key, + keyPassphrase, + certificate, certificateAuthorities, + verificationMode, }; } } + +const readFile = (file: string) => { + return readFileSync(file, 'utf8'); +}; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 6c4a1f263bc71..61f06744bd7f7 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -174,7 +174,11 @@ Object { undefined, ], "ssl": Object { + "alwaysPresentCertificate": undefined, + "certificate": undefined, "certificateAuthorities": undefined, + "key": undefined, + "keyPassphrase": undefined, "verificationMode": "none", }, } diff --git a/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js b/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js index 2942e76f27628..ec7d256975b27 100644 --- a/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js +++ b/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js @@ -19,14 +19,10 @@ import expect from '@kbn/expect'; import moment from 'moment'; -import fs from 'fs'; -import { promisify } from 'bluebird'; import { getElasticsearchProxyConfig } from '../elasticsearch_proxy_config'; import https from 'https'; import http from 'http'; -const readFileAsync = promisify(fs.readFile, fs); - const getDefaultElasticsearchConfig = () => { return { hosts: ['http://localhost:9200', 'http://192.168.1.1:1234'], @@ -124,10 +120,10 @@ describe('plugins/console', function() { it(`sets ca when certificateAuthorities are specified`, function() { const { agent } = getElasticsearchProxyConfig({ ...config, - ssl: { ...config.ssl, certificateAuthorities: [__dirname + '/fixtures/ca.crt'] }, + ssl: { ...config.ssl, certificateAuthorities: ['content-of-some-path'] }, }); - expect(agent.options.ca).to.contain('test ca certificate\n'); + expect(agent.options.ca).to.contain('content-of-some-path'); }); describe('when alwaysPresentCertificate is false', () => { @@ -137,8 +133,8 @@ describe('plugins/console', function() { ssl: { ...config.ssl, alwaysPresentCertificate: false, - certificate: __dirname + '/fixtures/cert.crt', - key: __dirname + '/fixtures/cert.key', + certificate: 'content-of-some-path', + key: 'content-of-another-path', }, }); @@ -152,8 +148,8 @@ describe('plugins/console', function() { ssl: { ...config.ssl, alwaysPresentCertificate: false, - certificate: __dirname + '/fixtures/cert.crt', - key: __dirname + '/fixtures/cert.key', + certificate: 'content-of-some-path', + key: 'content-of-another-path', keyPassphrase: 'secret', }, }); @@ -163,22 +159,19 @@ describe('plugins/console', function() { }); describe('when alwaysPresentCertificate is true', () => { - it(`sets cert and key when certificate and key paths are specified`, async function() { - const certificatePath = __dirname + '/fixtures/cert.crt'; - const keyPath = __dirname + '/fixtures/cert.key'; - + it(`sets cert and key when certificate and key are specified`, async function() { const { agent } = getElasticsearchProxyConfig({ ...config, ssl: { ...config.ssl, alwaysPresentCertificate: true, - certificate: certificatePath, - key: keyPath, + certificate: 'content-of-some-path', + key: 'content-of-another-path', }, }); - expect(agent.options.cert).to.be(await readFileAsync(certificatePath, 'utf8')); - expect(agent.options.key).to.be(await readFileAsync(keyPath, 'utf8')); + expect(agent.options.cert).to.be('content-of-some-path'); + expect(agent.options.key).to.be('content-of-another-path'); }); it(`sets passphrase when certificate, key and keyPassphrase are specified`, function() { @@ -187,8 +180,8 @@ describe('plugins/console', function() { ssl: { ...config.ssl, alwaysPresentCertificate: true, - certificate: __dirname + '/fixtures/cert.crt', - key: __dirname + '/fixtures/cert.key', + certificate: 'content-of-some-path', + key: 'content-of-another-path', keyPassphrase: 'secret', }, }); @@ -197,13 +190,12 @@ describe('plugins/console', function() { }); it(`doesn't set cert when only certificate path is specified`, async function() { - const certificatePath = __dirname + '/fixtures/cert.crt'; const { agent } = getElasticsearchProxyConfig({ ...config, ssl: { ...config.ssl, alwaysPresentCertificate: true, - certificate: certificatePath, + certificate: 'content-of-some-path', key: undefined, }, }); @@ -213,14 +205,13 @@ describe('plugins/console', function() { }); it(`doesn't set key when only key path is specified`, async function() { - const keyPath = __dirname + '/fixtures/cert.key'; const { agent } = getElasticsearchProxyConfig({ ...config, ssl: { ...config.ssl, alwaysPresentCertificate: true, certificate: undefined, - key: keyPath, + key: 'content-of-some-path', }, }); diff --git a/src/legacy/core_plugins/console/server/__tests__/fixtures/ca.crt b/src/legacy/core_plugins/console/server/__tests__/fixtures/ca.crt deleted file mode 100644 index 075fdd038daff..0000000000000 --- a/src/legacy/core_plugins/console/server/__tests__/fixtures/ca.crt +++ /dev/null @@ -1 +0,0 @@ -test ca certificate diff --git a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.crt b/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.crt deleted file mode 100644 index 360cdfaaaa5a9..0000000000000 --- a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.crt +++ /dev/null @@ -1 +0,0 @@ -test certificate diff --git a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.key b/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.key deleted file mode 100644 index 04d3bfef24188..0000000000000 --- a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.key +++ /dev/null @@ -1 +0,0 @@ -test key diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_config.js b/src/legacy/core_plugins/console/server/__tests__/proxy_config.js index 821fb8bef64d5..2a221aa261167 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_config.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_config.js @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import fs from 'fs'; import https, { Agent as HttpsAgent } from 'https'; import { parse as parseUrl } from 'url'; @@ -36,14 +35,6 @@ const parsedGoogle = parseUrl('https://google.com/search'); const parsedLocalEs = parseUrl('https://localhost:5601/search'); describe('ProxyConfig', function() { - beforeEach(function() { - sinon.stub(fs, 'readFileSync').callsFake(path => ({ path })); - }); - - afterEach(function() { - fs.readFileSync.restore(); - }); - describe('constructor', function() { beforeEach(function() { sinon.stub(https, 'Agent'); @@ -56,7 +47,7 @@ describe('ProxyConfig', function() { it('uses ca to create sslAgent', function() { const config = new ProxyConfig({ ssl: { - ca: ['path/to/ca'], + ca: ['content-of-some-path'], }, }); @@ -64,7 +55,7 @@ describe('ProxyConfig', function() { sinon.assert.calledOnce(https.Agent); const sslAgentOpts = https.Agent.firstCall.args[0]; expect(sslAgentOpts).to.eql({ - ca: [{ path: 'path/to/ca' }], + ca: ['content-of-some-path'], cert: undefined, key: undefined, rejectUnauthorized: true, @@ -74,8 +65,8 @@ describe('ProxyConfig', function() { it('uses cert, and key to create sslAgent', function() { const config = new ProxyConfig({ ssl: { - cert: 'path/to/cert', - key: 'path/to/key', + cert: 'content-of-some-path', + key: 'content-of-another-path', }, }); @@ -84,8 +75,8 @@ describe('ProxyConfig', function() { const sslAgentOpts = https.Agent.firstCall.args[0]; expect(sslAgentOpts).to.eql({ ca: undefined, - cert: { path: 'path/to/cert' }, - key: { path: 'path/to/key' }, + cert: 'content-of-some-path', + key: 'content-of-another-path', rejectUnauthorized: true, }); }); @@ -93,9 +84,9 @@ describe('ProxyConfig', function() { it('uses ca, cert, and key to create sslAgent', function() { const config = new ProxyConfig({ ssl: { - ca: ['path/to/ca'], - cert: 'path/to/cert', - key: 'path/to/key', + ca: ['content-of-some-path'], + cert: 'content-of-another-path', + key: 'content-of-yet-another-path', rejectUnauthorized: true, }, }); @@ -104,9 +95,9 @@ describe('ProxyConfig', function() { sinon.assert.calledOnce(https.Agent); const sslAgentOpts = https.Agent.firstCall.args[0]; expect(sslAgentOpts).to.eql({ - ca: [{ path: 'path/to/ca' }], - cert: { path: 'path/to/cert' }, - key: { path: 'path/to/key' }, + ca: ['content-of-some-path'], + cert: 'content-of-another-path', + key: 'content-of-yet-another-path', rejectUnauthorized: true, }); }); diff --git a/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts b/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts index 9e7c814aa57a4..5f44a524e9cc8 100644 --- a/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts +++ b/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts @@ -18,13 +18,10 @@ */ import _ from 'lodash'; -import { readFileSync } from 'fs'; import http from 'http'; import https from 'https'; import url from 'url'; -const readFile = (file: string) => readFileSync(file, 'utf8'); - const createAgent = (legacyConfig: any) => { const target = url.parse(_.head(legacyConfig.hosts)); if (!/^https/.test(target.protocol || '')) return new http.Agent(); @@ -49,22 +46,12 @@ const createAgent = (legacyConfig: any) => { throw new Error(`Unknown ssl verificationMode: ${verificationMode}`); } - if ( - legacyConfig.ssl && - Array.isArray(legacyConfig.ssl.certificateAuthorities) && - legacyConfig.ssl.certificateAuthorities.length > 0 - ) { - agentOptions.ca = legacyConfig.ssl.certificateAuthorities.map(readFile); - } + agentOptions.ca = legacyConfig.ssl?.certificateAuthorities; - if ( - legacyConfig.ssl && - legacyConfig.ssl.alwaysPresentCertificate && - legacyConfig.ssl.certificate && - legacyConfig.ssl.key - ) { - agentOptions.cert = readFile(legacyConfig.ssl.certificate); - agentOptions.key = readFile(legacyConfig.ssl.key); + const ignoreCertAndKey = !legacyConfig.ssl?.alwaysPresentCertificate; + if (!ignoreCertAndKey && legacyConfig.ssl?.certificate && legacyConfig.ssl?.key) { + agentOptions.cert = legacyConfig.ssl.certificate; + agentOptions.key = legacyConfig.ssl.key; agentOptions.passphrase = legacyConfig.ssl.keyPassphrase; } diff --git a/src/legacy/core_plugins/console/server/proxy_config.js b/src/legacy/core_plugins/console/server/proxy_config.js index 44a2769a46eb3..b8c1b11f9a0d3 100644 --- a/src/legacy/core_plugins/console/server/proxy_config.js +++ b/src/legacy/core_plugins/console/server/proxy_config.js @@ -20,7 +20,6 @@ import { values } from 'lodash'; import { format as formatUrl } from 'url'; import { Agent as HttpsAgent } from 'https'; -import { readFileSync } from 'fs'; import { WildcardMatcher } from './wildcard_matcher'; @@ -63,9 +62,9 @@ export class ProxyConfig { this.verifySsl = ssl.verify; const sslAgentOpts = { - ca: ssl.ca && ssl.ca.map(ca => readFileSync(ca)), - cert: ssl.cert && readFileSync(ssl.cert), - key: ssl.key && readFileSync(ssl.key), + ca: ssl.ca, + cert: ssl.cert, + key: ssl.key, }; if (values(sslAgentOpts).filter(Boolean).length) { From 7212b668d8d73e42fb3011425731de288f5fc69e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:45:47 -0500 Subject: [PATCH 07/21] scripts/check_core_api_changes.js --accept This test started failing until I accepted these changes. --- .../server/kibana-plugin-server.kibanaresponsefactory.md | 8 ++++---- src/core/server/server.api.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md index 85874f5ec16ba..2e496aa0c46fc 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md @@ -10,7 +10,7 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom ```typescript kibanaResponseFactory: { - custom: | Buffer | Stream | { + custom: | { message: string | Error; attributes?: Record | undefined; } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; @@ -21,9 +21,9 @@ kibanaResponseFactory: { conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; - redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; - ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; - accepted: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + redirected: (options: RedirectResponseOptions) => KibanaResponse>; + ok: (options?: HttpResponseOptions) => KibanaResponse>; + accepted: (options?: HttpResponseOptions) => KibanaResponse>; noContent: (options?: HttpResponseOptions) => KibanaResponse; } ``` diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 48293eda44d33..7ad15da4ac21f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -885,7 +885,7 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory; // @public export const kibanaResponseFactory: { - custom: | Buffer | Stream | { + custom: | { message: string | Error; attributes?: Record | undefined; } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; @@ -896,9 +896,9 @@ export const kibanaResponseFactory: { conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; - redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; - ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; - accepted: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; + redirected: (options: RedirectResponseOptions) => KibanaResponse>; + ok: (options?: HttpResponseOptions) => KibanaResponse>; + accepted: (options?: HttpResponseOptions) => KibanaResponse>; noContent: (options?: HttpResponseOptions) => KibanaResponse; }; From 78177d87c1cba2f9a0989a60ddbb5df137900f33 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:45:59 -0500 Subject: [PATCH 08/21] Change Elasticsearch clients to support P12 key stores and trust stores --- .../elasticsearch_config.test.mocks.ts | 7 ++ .../elasticsearch_config.test.ts | 98 ++++++++++++++++++- .../elasticsearch/elasticsearch_config.ts | 91 +++++++++++++---- .../resources/bin/kibana-docker | 4 + 4 files changed, 178 insertions(+), 22 deletions(-) diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts index f6c6079822cb5..d908fdbfd2e80 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts @@ -19,3 +19,10 @@ export const mockReadFileSync = jest.fn(); jest.mock('fs', () => ({ readFileSync: mockReadFileSync })); + +export const mockReadPkcs12Keystore = jest.fn(); +export const mockReadPkcs12Truststore = jest.fn(); +jest.mock('../../utils', () => ({ + readPkcs12Keystore: mockReadPkcs12Keystore, + readPkcs12Truststore: mockReadPkcs12Truststore, +})); diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 3a06f46e715ea..025c703d75304 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -17,7 +17,11 @@ * under the License. */ -import { mockReadFileSync } from './elasticsearch_config.test.mocks'; +import { + mockReadFileSync, + mockReadPkcs12Keystore, + mockReadPkcs12Truststore, +} from './elasticsearch_config.test.mocks'; import { ElasticsearchConfig, config, ElasticsearchConfigType } from './elasticsearch_config'; import { loggingServiceMock } from '../mocks'; @@ -107,6 +111,30 @@ describe('reads files', () => { beforeEach(() => { mockReadFileSync.mockReset(); mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + mockReadPkcs12Keystore.mockReset(); + mockReadPkcs12Keystore.mockImplementation((path: string) => ({ + key: `content-of-${path}.key`, + cert: `content-of-${path}.cert`, + ca: [`content-of-${path}.ca`], + })); + mockReadPkcs12Truststore.mockReset(); + mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); + }); + + it('reads certificate authorities when ssl.keystore.path is specified', () => { + const configValue = createElasticsearchConfig( + config.schema.validate({ ssl: { keystore: { path: 'some-path' } } }) + ); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']); + }); + + it('reads certificate authorities when ssl.truststore.path is specified', () => { + const configValue = createElasticsearchConfig( + config.schema.validate({ ssl: { truststore: { path: 'some-path' } } }) + ); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']); }); it('reads certificate authorities when ssl.certificateAuthorities is specified', () => { @@ -136,6 +164,35 @@ describe('reads files', () => { ]); }); + it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => { + const configValue = createElasticsearchConfig( + config.schema.validate({ + ssl: { + keystore: { path: 'some-path' }, + truststore: { path: 'another-path' }, + certificateAuthorities: 'yet-another-path', + }, + }) + ); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.ssl.certificateAuthorities).toEqual([ + 'content-of-some-path.ca', + 'content-of-another-path', + 'content-of-yet-another-path', + ]); + }); + + it('reads a private key and certificate when ssl.keystore.path is specified', () => { + const configValue = createElasticsearchConfig( + config.schema.validate({ ssl: { keystore: { path: 'some-path' } } }) + ); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.ssl.key).toEqual('content-of-some-path.key'); + expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert'); + }); + it('reads a private key when ssl.key is specified', () => { const configValue = createElasticsearchConfig( config.schema.validate({ ssl: { key: 'some-path' } }) @@ -157,6 +214,13 @@ describe('throws when config is invalid', () => { beforeAll(() => { const realFs = jest.requireActual('fs'); mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + const utils = jest.requireActual('../../utils'); + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Keystore(path, password) + ); + mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Truststore(path, password) + ); }); it('throws if key is invalid', () => { @@ -183,6 +247,38 @@ describe('throws when config is invalid', () => { createElasticsearchConfig(config.schema.validate(value)) ).toThrowErrorMatchingInlineSnapshot(`"ENOENT: no such file or directory, open '/invalid/ca'"`); }); + + it('throws if keystore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/keystore' } } }; + expect(() => + createElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/keystore'"` + ); + }); + + it('throws if truststore path is invalid', () => { + const value = { ssl: { keystore: { path: '/invalid/truststore' } } }; + expect(() => + createElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/truststore'"` + ); + }); + + it('throws if key and keystore.path are both specified', () => { + const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } }; + expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[ssl]: cannot use [key] when [keystore.path] is specified"` + ); + }); + + it('throws if certificate and keystore.path are both specified', () => { + const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } }; + expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[ssl]: cannot use [certificate] when [keystore.path] is specified"` + ); + }); }); describe('logs warnings', () => { diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 4c758f56d75e5..2910298413d57 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -20,6 +20,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { Duration } from 'moment'; import { readFileSync } from 'fs'; +import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils'; import { Logger } from '../logging'; const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); @@ -68,19 +69,39 @@ export const config = { pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }), startupTimeout: schema.duration({ defaultValue: '5s' }), logQueries: schema.boolean({ defaultValue: false }), - ssl: schema.object({ - verificationMode: schema.oneOf( - [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], - { defaultValue: 'full' } - ), - certificateAuthorities: schema.maybe( - schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })]) - ), - certificate: schema.maybe(schema.string()), - key: schema.maybe(schema.string()), - keyPassphrase: schema.maybe(schema.string()), - alwaysPresentCertificate: schema.boolean({ defaultValue: false }), - }), + ssl: schema.object( + { + verificationMode: schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ), + certificateAuthorities: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })]) + ), + certificate: schema.maybe(schema.string()), + key: schema.maybe(schema.string()), + keyPassphrase: schema.maybe(schema.string()), + keystore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), + truststore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), + alwaysPresentCertificate: schema.boolean({ defaultValue: false }), + }, + { + validate: rawConfig => { + if (rawConfig.key && rawConfig.keystore.path) { + return 'cannot use [key] when [keystore.path] is specified'; + } + if (rawConfig.certificate && rawConfig.keystore.path) { + return 'cannot use [certificate] when [keystore.path] is specified'; + } + }, + } + ), apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), ignoreVersionMismatch: schema.boolean({ defaultValue: false }), @@ -174,7 +195,7 @@ export class ElasticsearchConfig { */ public readonly ssl: Pick< SslConfigSchema, - Exclude + Exclude > & { certificateAuthorities?: string[] }; /** @@ -209,24 +230,52 @@ export class ElasticsearchConfig { let keyPassphrase: string | undefined; let certificate: string | undefined; let certificateAuthorities: string[] | undefined; + const addCAs = (ca: string[] | undefined) => { + if (ca && ca.length) { + certificateAuthorities = certificateAuthorities ? certificateAuthorities.concat(ca) : ca; + } + }; - if (rawConfig.ssl.key) { - key = readFile(rawConfig.ssl.key); - keyPassphrase = rawConfig.ssl.keyPassphrase; + if (rawConfig.ssl.keystore?.path) { + const { key: k, cert, ca } = readPkcs12Keystore( + rawConfig.ssl.keystore.path, + rawConfig.ssl.keystore.password + ); + if (!k && !cert) { + log.warn( + `Did not find key or certificate in keystore; mutual TLS authentication is disabled.` + ); + } + key = k; + certificate = cert; + addCAs(ca); + } else { + if (rawConfig.ssl.key) { + key = readFile(rawConfig.ssl.key); + keyPassphrase = rawConfig.ssl.keyPassphrase; + } + if (rawConfig.ssl.certificate) { + certificate = readFile(rawConfig.ssl.certificate); + } } - if (rawConfig.ssl.certificate) { - certificate = readFile(rawConfig.ssl.certificate); + + if (rawConfig.ssl.truststore?.path) { + const ca = readPkcs12Truststore( + rawConfig.ssl.truststore.path, + rawConfig.ssl.truststore.password + ); + addCAs(ca); } const ca = rawConfig.ssl.certificateAuthorities; if (ca) { - const parsed = []; + const parsed: string[] = []; const paths = Array.isArray(ca) ? ca : [ca]; if (paths.length > 0) { for (const path of paths) { parsed.push(readFile(path)); } - certificateAuthorities = parsed; + addCAs(parsed); } } diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index d1734e836d983..bf626dfe02ad7 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -34,6 +34,10 @@ kibana_vars=( elasticsearch.ssl.certificateAuthorities elasticsearch.ssl.key elasticsearch.ssl.keyPassphrase + elasticsearch.ssl.keystore.path + elasticsearch.ssl.keystore.password + elasticsearch.ssl.truststore.path + elasticsearch.ssl.truststore.password elasticsearch.ssl.verificationMode elasticsearch.startupTimeout elasticsearch.username From 73660b69bf4dc6a101c0f3f90cb3d322ef9f322c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:46:09 -0500 Subject: [PATCH 09/21] Split out ssl_config unit tests from http_config unit tests These never should have been grouped together. Split them out to improve maintainability and reduce clutter. --- .../__snapshots__/http_config.test.ts.snap | 18 -- src/core/server/http/http_config.test.ts | 220 +----------------- src/core/server/http/ssl_config.test.ts | 194 +++++++++++++++ 3 files changed, 204 insertions(+), 228 deletions(-) create mode 100644 src/core/server/http/ssl_config.test.ts diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 6c690f9da70c3..597d09b538870 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -81,24 +81,6 @@ exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = ` exports[`throws if invalid hostname 1`] = `"[host]: value is [asdf$%^] but it must be a valid hostname (see RFC 1123)."`; -exports[`with TLS should accept known protocols\` 1`] = ` -"[ssl.supportedProtocols.0]: types that failed validation: -- [ssl.supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500] -- [ssl.supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] -- [ssl.supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" -`; - -exports[`with TLS should accept known protocols\` 2`] = ` -"[ssl.supportedProtocols.3]: types that failed validation: -- [ssl.supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500] -- [ssl.supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] -- [ssl.supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" -`; - -exports[`with TLS throws if TLS is enabled but \`certificate\` is not specified 1`] = `"[ssl]: must specify [certificate] and [key] when ssl is enabled"`; - -exports[`with TLS throws if TLS is enabled but \`key\` is not specified 1`] = `"[ssl]: must specify [certificate] and [key] when ssl is enabled"`; - exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`; exports[`with compression accepts valid referrer whitelist 1`] = ` diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index 9b6fab8f3daec..7a0bdde124c0d 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -18,9 +18,7 @@ */ import uuid from 'uuid'; -import { config, HttpConfig } from '.'; -import { Env } from '../config'; -import { getEnvOptions } from '../config/__mocks__/env'; +import { config } from '.'; const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost']; const invalidHostname = 'asdf$%^'; @@ -87,28 +85,6 @@ test('accepts only valid uuids for server.uuid', () => { }); describe('with TLS', () => { - test('throws if TLS is enabled but `key` is not specified', () => { - const httpSchema = config.schema; - const obj = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); - }); - - test('throws if TLS is enabled but `certificate` is not specified', () => { - const httpSchema = config.schema; - const obj = { - ssl: { - enabled: true, - key: '/path/to/key', - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); - }); - test('throws if TLS is enabled but `redirectHttpFromPort` is equal to `port`', () => { const httpSchema = config.schema; const obj = { @@ -122,192 +98,16 @@ describe('with TLS', () => { }; expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot(); }); +}); - test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => { - const httpSchema = config.schema; - const obj = { - port: 1234, - ssl: { - enabled: false, - clientAuthentication: 'optional', - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"[ssl]: must enable ssl to use [clientAuthentication]"` - ); - }); - - test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => { - const httpSchema = config.schema; - const obj = { - port: 1234, - ssl: { - enabled: false, - clientAuthentication: 'required', - }, - }; - expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"[ssl]: must enable ssl to use [clientAuthentication]"` - ); - }); - - test('can specify `none` for [clientAuthentication] if ssl is not enabled', () => { - const obj = { - ssl: { - enabled: false, - clientAuthentication: 'none', - }, - }; - - const configValue = config.schema.validate(obj); - expect(configValue.ssl.clientAuthentication).toBe('none'); - }); - - test('can specify single `certificateAuthority` as a string', () => { - const obj = { - ssl: { - certificate: '/path/to/certificate', - certificateAuthorities: '/authority/', - enabled: true, - key: '/path/to/key', - }, - }; - - const configValue = config.schema.validate(obj); - expect(configValue.ssl.certificateAuthorities).toBe('/authority/'); - }); - - test('can specify socket timeouts', () => { - const obj = { - keepaliveTimeout: 1e5, - socketTimeout: 5e5, - }; - const { keepaliveTimeout, socketTimeout } = config.schema.validate(obj); - expect(keepaliveTimeout).toBe(1e5); - expect(socketTimeout).toBe(5e5); - }); - - test('can specify several `certificateAuthorities`', () => { - const obj = { - ssl: { - certificate: '/path/to/certificate', - certificateAuthorities: ['/authority/1', '/authority/2'], - enabled: true, - key: '/path/to/key', - }, - }; - - const configValue = config.schema.validate(obj); - expect(configValue.ssl.certificateAuthorities).toEqual(['/authority/1', '/authority/2']); - }); - - test('accepts known protocols`', () => { - const httpSchema = config.schema; - const singleKnownProtocol = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1'], - }, - }; - - const allKnownProtocols = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'], - }, - }; - - const singleKnownProtocolConfig = httpSchema.validate(singleKnownProtocol); - expect(singleKnownProtocolConfig.ssl.supportedProtocols).toEqual(['TLSv1']); - - const allKnownProtocolsConfig = httpSchema.validate(allKnownProtocols); - expect(allKnownProtocolsConfig.ssl.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']); - }); - - test('should accept known protocols`', () => { - const httpSchema = config.schema; - - const singleUnknownProtocol = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['SOMEv100500'], - }, - }; - - const allKnownWithOneUnknownProtocols = { - ssl: { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'], - }, - }; - - expect(() => httpSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingSnapshot(); - expect(() => - httpSchema.validate(allKnownWithOneUnknownProtocols) - ).toThrowErrorMatchingSnapshot(); - }); - - test('HttpConfig instance should properly interpret `none` client authentication', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'none', - }, - }), - {} as any, - Env.createDefault(getEnvOptions()) - ); - - expect(httpConfig.ssl.requestCert).toBe(false); - expect(httpConfig.ssl.rejectUnauthorized).toBe(false); - }); - - test('HttpConfig instance should properly interpret `optional` client authentication', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'optional', - }, - }), - {} as any, - Env.createDefault(getEnvOptions()) - ); - - expect(httpConfig.ssl.requestCert).toBe(true); - expect(httpConfig.ssl.rejectUnauthorized).toBe(false); - }); - - test('HttpConfig instance should properly interpret `required` client authentication', () => { - const httpConfig = new HttpConfig( - config.schema.validate({ - ssl: { - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'required', - }, - }), - {} as any, - Env.createDefault(getEnvOptions()) - ); - - expect(httpConfig.ssl.requestCert).toBe(true); - expect(httpConfig.ssl.rejectUnauthorized).toBe(true); - }); +test('can specify socket timeouts', () => { + const obj = { + keepaliveTimeout: 1e5, + socketTimeout: 5e5, + }; + const { keepaliveTimeout, socketTimeout } = config.schema.validate(obj); + expect(keepaliveTimeout).toBe(1e5); + expect(socketTimeout).toBe(5e5); }); describe('with compression', () => { diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts new file mode 100644 index 0000000000000..3d00175eb6291 --- /dev/null +++ b/src/core/server/http/ssl_config.test.ts @@ -0,0 +1,194 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { sslSchema, SslConfig } from './ssl_config'; + +describe('throws when config is invalid', () => { + test('throws if TLS is enabled but `key` is not specified', () => { + const obj = { + certificate: '/path/to/certificate', + enabled: true, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] when ssl is enabled"` + ); + }); + + test('throws if TLS is enabled but `certificate` is not specified', () => { + const obj = { + enabled: true, + key: '/path/to/key', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] when ssl is enabled"` + ); + }); + + test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => { + const obj = { + enabled: false, + clientAuthentication: 'optional', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must enable ssl to use [clientAuthentication]"` + ); + }); + + test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => { + const obj = { + enabled: false, + clientAuthentication: 'required', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must enable ssl to use [clientAuthentication]"` + ); + }); +}); + +test('can specify single `certificateAuthority` as a string', () => { + const obj = { + certificate: '/path/to/certificate', + certificateAuthorities: '/authority/', + enabled: true, + key: '/path/to/key', + }; + + const configValue = sslSchema.validate(obj); + expect(configValue.certificateAuthorities).toBe('/authority/'); +}); + +test('can specify several `certificateAuthorities`', () => { + const obj = { + certificate: '/path/to/certificate', + certificateAuthorities: ['/authority/1', '/authority/2'], + enabled: true, + key: '/path/to/key', + }; + + const configValue = sslSchema.validate(obj); + expect(configValue.certificateAuthorities).toEqual(['/authority/1', '/authority/2']); +}); + +describe('#supportedProtocols', () => { + test('accepts known protocols`', () => { + const singleKnownProtocol = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1'], + }; + + const allKnownProtocols = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'], + }; + + const singleKnownProtocolConfig = sslSchema.validate(singleKnownProtocol); + expect(singleKnownProtocolConfig.supportedProtocols).toEqual(['TLSv1']); + + const allKnownProtocolsConfig = sslSchema.validate(allKnownProtocols); + expect(allKnownProtocolsConfig.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']); + }); + + test('rejects unknown protocols`', () => { + const singleUnknownProtocol = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['SOMEv100500'], + }; + + const allKnownWithOneUnknownProtocols = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'], + }; + + expect(() => sslSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingInlineSnapshot(` +"[supportedProtocols.0]: types that failed validation: +- [supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500] +- [supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] +- [supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" +`); + expect(() => sslSchema.validate(allKnownWithOneUnknownProtocols)) + .toThrowErrorMatchingInlineSnapshot(` +"[supportedProtocols.3]: types that failed validation: +- [supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500] +- [supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] +- [supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" +`); + }); +}); + +describe('#clientAuthentication', () => { + test('can specify `none` client authentication when ssl is not enabled', () => { + const obj = { + enabled: false, + clientAuthentication: 'none', + }; + + const configValue = sslSchema.validate(obj); + expect(configValue.clientAuthentication).toBe('none'); + }); + + test('should properly interpret `none` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'none', + }) + ); + + expect(sslConfig.requestCert).toBe(false); + expect(sslConfig.rejectUnauthorized).toBe(false); + }); + + test('should properly interpret `optional` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'optional', + }) + ); + + expect(sslConfig.requestCert).toBe(true); + expect(sslConfig.rejectUnauthorized).toBe(false); + }); + + test('should properly interpret `required` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'required', + }) + ); + + expect(sslConfig.requestCert).toBe(true); + expect(sslConfig.rejectUnauthorized).toBe(true); + }); +}); From afdc378ca4755395d829dcc49969d13da630a77e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:46:15 -0500 Subject: [PATCH 10/21] Change HTTP servers to read certs/keys upon config parse This includes: * HTTPS server * HTTPS redirect server * BasePath proxy server --- src/core/server/http/http_server.test.ts | 19 ++-- src/core/server/http/http_tools.ts | 9 +- src/core/server/http/ssl_config.test.mocks.ts | 23 +++++ src/core/server/http/ssl_config.test.ts | 86 ++++++++++++++----- src/core/server/http/ssl_config.ts | 35 +++++--- 5 files changed, 124 insertions(+), 48 deletions(-) create mode 100644 src/core/server/http/ssl_config.test.mocks.ts diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index df357aeaf2731..df7b4b5af4267 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -18,11 +18,7 @@ */ import { Server } from 'http'; - -jest.mock('fs', () => ({ - readFileSync: jest.fn(), -})); - +import { readFileSync } from 'fs'; import supertest from 'supertest'; import { ByteSizeValue, schema } from '@kbn/config-schema'; @@ -39,6 +35,7 @@ import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; import { Readable } from 'stream'; import { RequestHandlerContext } from 'kibana/server'; +import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; const cookieOptions = { name: 'sid', @@ -55,6 +52,14 @@ const loggingService = loggingServiceMock.create(); const logger = loggingService.get(); const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); +let certificate: string; +let key: string; + +beforeAll(() => { + certificate = readFileSync(KBN_CERT_PATH, 'utf8'); + key = readFileSync(KBN_KEY_PATH, 'utf8'); +}); + beforeEach(() => { config = { host: '127.0.0.1', @@ -68,10 +73,10 @@ beforeEach(() => { ...config, ssl: { enabled: true, - certificate: '/certificate', + certificate, cipherSuites: ['cipherSuite'], getSecureOptions: () => 0, - key: '/key', + key, redirectHttpFromPort: config.port + 1, }, } as HttpConfig; diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 22468a5b252f4..747fd5a10b168 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -17,7 +17,6 @@ * under the License. */ -import { readFileSync } from 'fs'; import { Lifecycle, Request, ResponseToolkit, Server, ServerOptions, Util } from 'hapi'; import Hoek from 'hoek'; import { ServerOptions as TLSOptions } from 'https'; @@ -66,14 +65,12 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { // TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of // `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`. const tlsOptions: TLSOptions = { - ca: - config.ssl.certificateAuthorities && - config.ssl.certificateAuthorities.map(caFilePath => readFileSync(caFilePath)), - cert: readFileSync(ssl.certificate!), + ca: ssl.certificateAuthorities, + cert: ssl.certificate, ciphers: config.ssl.cipherSuites.join(':'), // We use the server's cipher order rather than the client's to prevent the BEAST attack. honorCipherOrder: true, - key: readFileSync(ssl.key!), + key: ssl.key, passphrase: ssl.keyPassphrase, secureOptions: ssl.getSecureOptions(), requestCert: ssl.requestCert, diff --git a/src/core/server/http/ssl_config.test.mocks.ts b/src/core/server/http/ssl_config.test.mocks.ts new file mode 100644 index 0000000000000..632626e38e0ed --- /dev/null +++ b/src/core/server/http/ssl_config.test.mocks.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const mockReadFileSync = jest.fn(); +jest.mock('fs', () => { + return { readFileSync: mockReadFileSync }; +}); diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts index 3d00175eb6291..dd727db52712d 100644 --- a/src/core/server/http/ssl_config.test.ts +++ b/src/core/server/http/ssl_config.test.ts @@ -17,10 +17,46 @@ * under the License. */ +import { mockReadFileSync } from './ssl_config.test.mocks'; + import { sslSchema, SslConfig } from './ssl_config'; +const createConfig = (obj: any) => new SslConfig(sslSchema.validate(obj)); + +beforeEach(() => { + mockReadFileSync.mockReset(); + mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); +}); + describe('throws when config is invalid', () => { - test('throws if TLS is enabled but `key` is not specified', () => { + beforeEach(() => { + const realFs = jest.requireActual('fs'); + mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + }); + + test('throws if `key` is invalid', () => { + const obj = { key: '/invalid/key', certificate: '/valid/certificate' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/key'"` + ); + }); + + test('throws if `certificate` is invalid', () => { + mockReadFileSync.mockImplementationOnce((path: string) => `content-of-${path}`); + const obj = { key: '/valid/key', certificate: '/invalid/certificate' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/certificate'"` + ); + }); + + test('throws if `certificateAuthorities` is invalid', () => { + const obj = { certificateAuthorities: '/invalid/ca' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/ca'"` + ); + }); + + test('throws if TLS is enabled but `certificate` is specified and `key` is not', () => { const obj = { certificate: '/path/to/certificate', enabled: true, @@ -30,7 +66,7 @@ describe('throws when config is invalid', () => { ); }); - test('throws if TLS is enabled but `certificate` is not specified', () => { + test('throws if TLS is enabled but `key` is specified and `certificate` is not', () => { const obj = { enabled: true, key: '/path/to/key', @@ -61,28 +97,32 @@ describe('throws when config is invalid', () => { }); }); -test('can specify single `certificateAuthority` as a string', () => { - const obj = { - certificate: '/path/to/certificate', - certificateAuthorities: '/authority/', - enabled: true, - key: '/path/to/key', - }; - - const configValue = sslSchema.validate(obj); - expect(configValue.certificateAuthorities).toBe('/authority/'); -}); - -test('can specify several `certificateAuthorities`', () => { - const obj = { - certificate: '/path/to/certificate', - certificateAuthorities: ['/authority/1', '/authority/2'], - enabled: true, - key: '/path/to/key', - }; +describe('reads files', () => { + it('reads certificate authorities when `certificateAuthorities` is specified', () => { + let configValue = createConfig({ certificateAuthorities: 'some-path' }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = createConfig({ certificateAuthorities: ['some-path'] }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = createConfig({ certificateAuthorities: ['some-path', 'another-path'] }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.certificateAuthorities).toEqual([ + 'content-of-some-path', + 'content-of-another-path', + ]); + }); - const configValue = sslSchema.validate(obj); - expect(configValue.certificateAuthorities).toEqual(['/authority/1', '/authority/2']); + it('reads a private key and certificate when `key` and `certificate` are specified', () => { + const configValue = createConfig({ key: 'some-path', certificate: 'another-path' }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.key).toEqual('content-of-some-path'); + expect(configValue.certificate).toEqual('content-of-another-path'); + }); }); describe('#supportedProtocols', () => { diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts index 55d6ebff93ce7..267833cd33f72 100644 --- a/src/core/server/http/ssl_config.ts +++ b/src/core/server/http/ssl_config.ts @@ -19,6 +19,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import crypto from 'crypto'; +import { readFileSync } from 'fs'; // `crypto` type definitions doesn't currently include `crypto.constants`, see // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fa5baf1733f49cf26228a4e509914572c1b74adf/types/node/v6/index.d.ts#L3412 @@ -88,14 +89,28 @@ export class SslConfig { constructor(config: SslConfigType) { this.enabled = config.enabled; this.redirectHttpFromPort = config.redirectHttpFromPort; - this.key = config.key; - this.certificate = config.certificate; - this.certificateAuthorities = this.initCertificateAuthorities(config.certificateAuthorities); - this.keyPassphrase = config.keyPassphrase; this.cipherSuites = config.cipherSuites; this.supportedProtocols = config.supportedProtocols; this.requestCert = config.clientAuthentication !== 'none'; this.rejectUnauthorized = config.clientAuthentication === 'required'; + + if (config.key && config.certificate) { + this.key = readFile(config.key); + this.keyPassphrase = config.keyPassphrase; + this.certificate = readFile(config.certificate); + } + + const ca = config.certificateAuthorities; + if (ca) { + const parsed: string[] = []; + const paths = Array.isArray(ca) ? ca : [ca]; + if (paths.length > 0) { + for (const path of paths) { + parsed.push(readFile(path)); + } + this.certificateAuthorities = parsed; + } + } } /** @@ -117,12 +132,8 @@ export class SslConfig { : secureOptions | secureOption; // eslint-disable-line no-bitwise }, 0); } - - private initCertificateAuthorities(certificateAuthorities?: string[] | string) { - if (certificateAuthorities === undefined || Array.isArray(certificateAuthorities)) { - return certificateAuthorities; - } - - return [certificateAuthorities]; - } } + +const readFile = (file: string) => { + return readFileSync(file, 'utf8'); +}; From eb557e0ad7fda23f485764f69a395f7430e83644 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:46:21 -0500 Subject: [PATCH 11/21] Change HTTP servers to support P12 key stores and trust stores --- src/cli/serve/serve.js | 2 + .../__snapshots__/http_config.test.ts.snap | 2 + src/core/server/http/ssl_config.test.mocks.ts | 7 + src/core/server/http/ssl_config.test.ts | 129 +++++++++++++++++- src/core/server/http/ssl_config.ts | 48 ++++++- .../resources/bin/kibana-docker | 4 + .../http/ssl_with_p12/config.js | 50 +++++++ .../http/ssl_with_p12/index.js | 28 ++++ 8 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 test/server_integration/http/ssl_with_p12/config.js create mode 100644 test/server_integration/http/ssl_with_p12/index.js diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index ecd8fb96ed3a3..78adb9d242ed5 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -102,6 +102,8 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { } ensureNotDefined('server.ssl.certificate'); ensureNotDefined('server.ssl.key'); + ensureNotDefined('server.ssl.keystore.path'); + ensureNotDefined('server.ssl.truststore.path'); ensureNotDefined('elasticsearch.ssl.certificateAuthorities'); const elasticsearchHosts = ( diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index 597d09b538870..b4eb7d4dbdf9e 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -65,10 +65,12 @@ Object { ], "clientAuthentication": "none", "enabled": false, + "keystore": Object {}, "supportedProtocols": Array [ "TLSv1.1", "TLSv1.2", ], + "truststore": Object {}, }, } `; diff --git a/src/core/server/http/ssl_config.test.mocks.ts b/src/core/server/http/ssl_config.test.mocks.ts index 632626e38e0ed..ab98c3a27920c 100644 --- a/src/core/server/http/ssl_config.test.mocks.ts +++ b/src/core/server/http/ssl_config.test.mocks.ts @@ -21,3 +21,10 @@ export const mockReadFileSync = jest.fn(); jest.mock('fs', () => { return { readFileSync: mockReadFileSync }; }); + +export const mockReadPkcs12Keystore = jest.fn(); +export const mockReadPkcs12Truststore = jest.fn(); +jest.mock('../../utils', () => ({ + readPkcs12Keystore: mockReadPkcs12Keystore, + readPkcs12Truststore: mockReadPkcs12Truststore, +})); diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts index dd727db52712d..dcfb621326f90 100644 --- a/src/core/server/http/ssl_config.test.ts +++ b/src/core/server/http/ssl_config.test.ts @@ -17,7 +17,11 @@ * under the License. */ -import { mockReadFileSync } from './ssl_config.test.mocks'; +import { + mockReadFileSync, + mockReadPkcs12Keystore, + mockReadPkcs12Truststore, +} from './ssl_config.test.mocks'; import { sslSchema, SslConfig } from './ssl_config'; @@ -26,12 +30,27 @@ const createConfig = (obj: any) => new SslConfig(sslSchema.validate(obj)); beforeEach(() => { mockReadFileSync.mockReset(); mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + mockReadPkcs12Keystore.mockReset(); + mockReadPkcs12Keystore.mockImplementation((path: string) => ({ + key: `content-of-${path}.key`, + cert: `content-of-${path}.cert`, + ca: [`content-of-${path}.ca`], + })); + mockReadPkcs12Truststore.mockReset(); + mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); }); describe('throws when config is invalid', () => { beforeEach(() => { const realFs = jest.requireActual('fs'); mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + const utils = jest.requireActual('../../utils'); + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Keystore(path, password) + ); + mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Truststore(path, password) + ); }); test('throws if `key` is invalid', () => { @@ -56,13 +75,73 @@ describe('throws when config is invalid', () => { ); }); + test('throws if `keystore.path` is invalid', () => { + const obj = { keystore: { path: '/invalid/keystore' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/keystore'"` + ); + }); + + test('throws if `keystore.path` does not contain a private key', () => { + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ + key: undefined, + certificate: 'foo', + })); + const obj = { keystore: { path: 'some-path' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"Did not find private key in keystore at [keystore.path]."` + ); + }); + + test('throws if `keystore.path` does not contain a certificate', () => { + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ + key: 'foo', + certificate: undefined, + })); + const obj = { keystore: { path: 'some-path' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"Did not find certificate in keystore at [keystore.path]."` + ); + }); + + test('throws if `truststore.path` is invalid', () => { + const obj = { truststore: { path: '/invalid/truststore' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/truststore'"` + ); + }); + + test('throws if both `key` and `keystore.path` are specified', () => { + const obj = { + key: '/path/to/key', + keystore: { + path: 'path/to/keystore', + }, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"cannot use [key] when [keystore.path] is specified"` + ); + }); + + test('throws if both `certificate` and `keystore.path` are specified', () => { + const obj = { + certificate: '/path/to/certificate', + keystore: { + path: 'path/to/keystore', + }, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"cannot use [certificate] when [keystore.path] is specified"` + ); + }); + test('throws if TLS is enabled but `certificate` is specified and `key` is not', () => { const obj = { certificate: '/path/to/certificate', enabled: true, }; expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"must specify [certificate] and [key] when ssl is enabled"` + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` ); }); @@ -72,7 +151,16 @@ describe('throws when config is invalid', () => { key: '/path/to/key', }; expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"must specify [certificate] and [key] when ssl is enabled"` + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` + ); + }); + + test('throws if TLS is enabled but `key`, `certificate`, and `keystore.path` are not specified', () => { + const obj = { + enabled: true, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` ); }); @@ -98,6 +186,18 @@ describe('throws when config is invalid', () => { }); describe('reads files', () => { + it('reads certificate authorities when `keystore.path` is specified', () => { + const configValue = createConfig({ keystore: { path: 'some-path' } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path.ca']); + }); + + it('reads certificate authorities when `truststore.path` is specified', () => { + const configValue = createConfig({ truststore: { path: 'some-path' } }); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + }); + it('reads certificate authorities when `certificateAuthorities` is specified', () => { let configValue = createConfig({ certificateAuthorities: 'some-path' }); expect(mockReadFileSync).toHaveBeenCalledTimes(1); @@ -117,6 +217,29 @@ describe('reads files', () => { ]); }); + it('reads certificate authorities when `keystore.path`, `truststore.path`, and `certificateAuthorities` are specified', () => { + const configValue = createConfig({ + keystore: { path: 'some-path' }, + truststore: { path: 'another-path' }, + certificateAuthorities: 'yet-another-path', + }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual([ + 'content-of-some-path.ca', + 'content-of-another-path', + 'content-of-yet-another-path', + ]); + }); + + it('reads a private key and certificate when `keystore.path` is specified', () => { + const configValue = createConfig({ keystore: { path: 'some-path' } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.key).toEqual('content-of-some-path.key'); + expect(configValue.certificate).toEqual('content-of-some-path.cert'); + }); + it('reads a private key and certificate when `key` and `certificate` are specified', () => { const configValue = createConfig({ key: 'some-path', certificate: 'another-path' }); expect(mockReadFileSync).toHaveBeenCalledTimes(2); diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts index 267833cd33f72..5ef81b5bc3956 100644 --- a/src/core/server/http/ssl_config.ts +++ b/src/core/server/http/ssl_config.ts @@ -20,6 +20,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import crypto from 'crypto'; import { readFileSync } from 'fs'; +import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils'; // `crypto` type definitions doesn't currently include `crypto.constants`, see // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fa5baf1733f49cf26228a4e509914572c1b74adf/types/node/v6/index.d.ts#L3412 @@ -45,6 +46,14 @@ export const sslSchema = schema.object( }), key: schema.maybe(schema.string()), keyPassphrase: schema.maybe(schema.string()), + keystore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), + truststore: schema.object({ + path: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + }), redirectHttpFromPort: schema.maybe(schema.number()), supportedProtocols: schema.arrayOf( schema.oneOf([schema.literal('TLSv1'), schema.literal('TLSv1.1'), schema.literal('TLSv1.2')]), @@ -57,8 +66,16 @@ export const sslSchema = schema.object( }, { validate: ssl => { - if (ssl.enabled && (!ssl.key || !ssl.certificate)) { - return 'must specify [certificate] and [key] when ssl is enabled'; + if (ssl.key && ssl.keystore.path) { + return 'cannot use [key] when [keystore.path] is specified'; + } + + if (ssl.certificate && ssl.keystore.path) { + return 'cannot use [certificate] when [keystore.path] is specified'; + } + + if (ssl.enabled && (!ssl.key || !ssl.certificate) && !ssl.keystore.path) { + return 'must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled'; } if (!ssl.enabled && ssl.clientAuthentication !== 'none') { @@ -94,12 +111,35 @@ export class SslConfig { this.requestCert = config.clientAuthentication !== 'none'; this.rejectUnauthorized = config.clientAuthentication === 'required'; - if (config.key && config.certificate) { + const addCAs = (ca: string[] | undefined) => { + if (ca && ca.length) { + this.certificateAuthorities = this.certificateAuthorities + ? this.certificateAuthorities.concat(ca) + : ca; + } + }; + + if (config.keystore?.path) { + const { key, cert, ca } = readPkcs12Keystore(config.keystore.path, config.keystore.password); + if (!key) { + throw new Error(`Did not find private key in keystore at [keystore.path].`); + } else if (!cert) { + throw new Error(`Did not find certificate in keystore at [keystore.path].`); + } + this.key = key; + this.certificate = cert; + addCAs(ca); + } else if (config.key && config.certificate) { this.key = readFile(config.key); this.keyPassphrase = config.keyPassphrase; this.certificate = readFile(config.certificate); } + if (config.truststore?.path) { + const ca = readPkcs12Truststore(config.truststore.path, config.truststore.password); + addCAs(ca); + } + const ca = config.certificateAuthorities; if (ca) { const parsed: string[] = []; @@ -108,7 +148,7 @@ export class SslConfig { for (const path of paths) { parsed.push(readFile(path)); } - this.certificateAuthorities = parsed; + addCAs(parsed); } } } diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index bf626dfe02ad7..31a5a4c13a4be 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -77,6 +77,10 @@ kibana_vars=( server.ssl.enabled server.ssl.key server.ssl.keyPassphrase + server.ssl.keystore.path + server.ssl.keystore.password + server.ssl.truststore.path + server.ssl.truststore.password server.ssl.redirectHttpFromPort server.ssl.supportedProtocols server.xsrf.whitelist diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js new file mode 100644 index 0000000000000..caf8a1c857b27 --- /dev/null +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KBN_P12_PATH, KBN_P12_PASSWORD, CA_CERT_PATH } from '@kbn/dev-utils'; + +export default async function({ readConfigFile }) { + const httpConfig = await readConfigFile(require.resolve('../../config')); + + return { + testFiles: [require.resolve('./')], + services: httpConfig.get('services'), + servers: { + ...httpConfig.get('servers'), + kibana: { + ...httpConfig.get('servers.kibana'), + protocol: 'https', + }, + }, + junit: { + reportName: 'Http SSL Integration Tests', + }, + esTestCluster: httpConfig.get('esTestCluster'), + kbnTestServer: { + ...httpConfig.get('kbnTestServer'), + serverArgs: [ + ...httpConfig.get('kbnTestServer.serverArgs'), + '--server.ssl.enabled=true', + `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, // this is needed for the test runner to trust the server certificate + `--server.ssl.keystore.path=${KBN_P12_PATH}`, + `--server.ssl.keystore.password=${KBN_P12_PASSWORD}`, + ], + }, + }; +} diff --git a/test/server_integration/http/ssl_with_p12/index.js b/test/server_integration/http/ssl_with_p12/index.js new file mode 100644 index 0000000000000..25e28f12ceaaa --- /dev/null +++ b/test/server_integration/http/ssl_with_p12/index.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('kibana server with ssl', () => { + it('handles requests using ssl', async () => { + await supertest.get('/').expect(302); + }); + }); +} From c4d5163d6705f220ff0873d4c47e149525a146a9 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:46:29 -0500 Subject: [PATCH 12/21] Update documentation --- docs/settings/security-settings.asciidoc | 2 +- docs/setup/settings.asciidoc | 112 ++++++++++----- .../monitoring/monitoring-kibana.asciidoc | 12 +- .../security/authentication/index.asciidoc | 3 + .../securing-communications/index.asciidoc | 130 ++++++++---------- x-pack/legacy/plugins/security/README.md | 6 +- 6 files changed, 153 insertions(+), 112 deletions(-) diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index d6dd4378da1b7..16d68a7759f77 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -45,7 +45,7 @@ if this setting isn't the same for all instances of {kib}. `xpack.security.secureCookies`:: Sets the `secure` flag of the session cookie. The default value is `false`. It -is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set +is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 01e6bd51ea50b..eb4c67e4a65f8 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -82,31 +82,48 @@ Elasticsearch nodes on startup. `elasticsearch.sniffOnConnectionFault:`:: *Default: false* Update the list of Elasticsearch nodes immediately following a connection fault. -`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls -whether to always present the certificate specified by -`elasticsearch.ssl.certificate` when requested. This applies to all requests to -Elasticsearch, including requests that are proxied for end-users. Setting this -to `true` when Elasticsearch is using certificates to authenticate users can -lead to proxied requests for end-users being executed as the identity tied to -the configured certificate. - -`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Optional -settings that provide the paths to the PEM-format SSL certificate and key files. -These files are used to verify the identity of Kibana to Elasticsearch and are -required when `xpack.security.http.ssl.client_authentication` in Elasticsearch is -set to `required`. - -`elasticsearch.ssl.certificateAuthorities:`:: Optional setting that enables you -to specify a list of paths to the PEM file for the certificate authority for -your Elasticsearch instance. - -`elasticsearch.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt -the private key. This value is optional as the key may not be encrypted. - -`elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the -verification of certificates presented by Elasticsearch. Valid values are `none`, -`certificate`, and `full`. `full` performs hostname verification, and -`certificate` does not. +`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls whether to always present the certificate specified by +`elasticsearch.ssl.certificate` or `elasticsearch.ssl.keystore.path` when requested. This applies to all requests to Elasticsearch, +including requests that are proxied for end-users. Setting this to `true` when Elasticsearch is using certificates to authenticate users can +lead to proxied requests for end-users being executed as the identity tied to the configured certificate. + +`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. +When `xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required`, the certificate and key are used to prove +Kibana's identity when it makes an outbound request to your Elasticsearch cluster. + +NOTE: These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. + +`elasticsearch.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. This may consist of a root certificate +authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This is used to establish trust when +Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via +`elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. + +`elasticsearch.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via +`elasticsearch.ssl.key`. This value is optional, as the key may not be encrypted. + +`elasticsearch.ssl.keystore.path:`:: Path to a PKCS #12 file -- this should contain an X.509 certificate with its private key. When +`xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required`, the certificate and key are used to prove Kibana's +identity when it makes an outbound request to your Elasticsearch cluster. If the file contains any additional certificates, those will be +used as a trusted certificate chain for your Elasticsearch cluster. This is used to establish trust when Kibana creates an SSL connection +with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via +`elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. + +NOTE: This setting cannot be used in conjunction with `elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. + +`elasticsearch.ssl.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has +no password, leave this unset. If your key store has an empty password, set this to `""`. + +`elasticsearch.ssl.truststore.path:`:: Path to a PKCS #12 trust store -- this should contain one or more X.509 certificates. This may +consist of a root certificate authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for your +Elasticsearch cluster. This is used to establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to +this setting, trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.keystore.path`. + +`elasticsearch.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password, +leave this unset. If your trust store has an empty password, set this to `""`. + +`elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the verification of certificates presented by Elasticsearch. Valid values +are `none`, `certificate`, and `full`. `full` performs hostname verification, and `certificate` does not. This is only used when traffic to +Elasticsearch is encrypted -- this is specified by using the HTTPS protocol in `elasticsearch.hosts`. `elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying. @@ -325,11 +342,16 @@ default is `true`. `server.socketTimeout:`:: *Default: "120000"* The number of milliseconds to wait before closing an inactive socket. -`server.ssl.certificate:` and `server.ssl.key:`:: Paths to the PEM-format SSL -certificate and SSL key files, respectively. +`server.ssl.certificate:` and `server.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. These are used +when enabling SSL for inbound requests from web browsers to the Kibana server. -`server.ssl.certificateAuthorities:`:: List of paths to PEM encoded certificate -files that should be trusted. +NOTE: These settings cannot be used in conjunction with `server.ssl.keystore.path`. + +`server.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. This may consist of a root certificate authority +(CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This is used when a web browser creates an SSL +connection with the Kibana server; the certificate chain is sent to the browser along with the end-entity certificate to establish trust. +This is also used to determine if client certificates should be trusted when PKI authentication is enabled. In addition to this setting, +trusted certificates may be specified via `server.ssl.keystore.path` and/or `server.ssl.truststore.path`. `server.ssl.cipherSuites:`:: *Default: ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA*. Details on the format, and the valid options, are available via the @@ -339,12 +361,34 @@ https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenS connections. Valid values are `required`, `optional`, and `none`. `required` forces a client to present a certificate, while `optional` requests a client certificate but the client is not required to present one. -`server.ssl.enabled:`:: *Default: "false"* Enables SSL for outgoing requests -from the Kibana server to the browser. When set to `true`, -`server.ssl.certificate` and `server.ssl.key` are required. +`server.ssl.enabled:`:: *Default: "false"* Enables SSL for inbound requests from the browser to the Kibana server. When set to `true`, a +certificate and private key must be provided. These can be specified via `server.ssl.keystore.path`, or the combination of +`server.ssl.certificate` and `server.ssl.key`. + +`server.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via `server.ssl.key`. This value +is optional, as the key may not be encrypted. + +`server.ssl.keystore.path:`:: Path to a PKCS #12 file -- this should contain an X.509 certificate with its private key. These are used when +enabling SSL for inbound requests from web browsers to the Kibana server. If the file contains any additional certificates, those will be +used as a trusted certificate chain for Kibana. This is used when a web browser creates an SSL connection with the Kibana server; the +certificate chain is sent to the browser along with the end-entity certificate to establish trust. This is also used to determine if client +certificates should be trusted when PKI authentication is enabled. In addition to this setting, trusted certificates may be specified via +`server.ssl.certificateAuthorities` and/or `server.ssl.truststore.path`. + +NOTE: This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key`. + +`server.ssl.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has no +password, leave this unset. If your key store has an empty password, set this to `""`. + +`server.ssl.truststore.path:`:: Path to a PKCS #12 trust store -- this should contain one or more X.509 certificates. This may consist of a +root certificate authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This is used when a +web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the end-entity +certificate to establish trust. This is also used to determine if client certificates should be trusted when PKI authentication is enabled. +In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or +`server.ssl.keystore.path`. -`server.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the -private key. This value is optional as the key may not be encrypted. +`server.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password, leave +this unset. If your trust store has an empty password, set this to `""`. `server.ssl.redirectHttpFromPort:`:: Kibana will bind to this port and redirect all http requests to https over the port configured as `server.port`. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index d7af0d5c420a1..b5d263aed8346 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -96,7 +96,8 @@ used when {kib} sends monitoring data to the production cluster. .. Configure {kib} to encrypt communications between the {kib} server and the production cluster. This set up involves generating a server certificate and setting `server.ssl.*` and `elasticsearch.ssl.certificateAuthorities` settings -in the `kibana.yml` file on the {kib} server. For example: +in the `kibana.yml` file on the {kib} server. For example, using a PEM-formatted +certificate and private key: + -- [source,yaml] @@ -105,14 +106,19 @@ server.ssl.key: /path/to/your/server.key server.ssl.certificate: /path/to/your/server.crt -------------------------------------------------------------------------------- -If you are using your own certificate authority to sign certificates, specify -the location of the PEM file in the `kibana.yml` file: +If you are using your own certificate authority (CA) to sign certificates, +specify the location of the PEM file in the `kibana.yml` file: [source,yaml] -------------------------------------------------------------------------------- elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem -------------------------------------------------------------------------------- +NOTE: Alternatively, the PKCS #12 format can be used for the Kibana certificate +and key, along with any included CA certificates, by setting +`server.ssl.keystore.path`. If your CA certificate chain is in a separate trust +store, you can also use `server.ssl.truststore.path`. + For more information, see <>. -- diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 2e2aaf688e8b6..ce0f1f8c6545b 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -67,6 +67,9 @@ server.ssl.clientAuthentication: required xpack.security.authc.providers: [pki] -------------------------------------------------------------------------------- +NOTE: Trusted CAs can also be specified in a PKCS #12 keystore bundled with your Kibana server certificate/key using +`server.ssl.keystore.path`, or in a separate trust store using `server.ssl.truststore.path`. + PKI support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both PKI and Basic authentication for the same {kib} instance: [source,yaml] diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc index 6917a48909c7b..f1eaa38ac0e04 100644 --- a/docs/user/security/securing-communications/index.asciidoc +++ b/docs/user/security/securing-communications/index.asciidoc @@ -4,121 +4,113 @@ Encrypting communications ++++ -{kib} supports Transport Layer Security (TLS/SSL) encryption for client -requests. -//TBD: It is unclear what "client requests" are in this context. Is it just -// communication between the browser and the Kibana server or are we talking -// about other types of clients connecting to the Kibana server? - -If you are using {security} or a proxy that provides an HTTPS endpoint for {es}, -you can configure {kib} to access {es} via HTTPS. Thus, communications between -{kib} and {es} are also encrypted. - -. Configure {kib} to encrypt communications between the browser and the {kib} -server: +{kib} supports Transport Layer Security (TLS/SSL) encryption for all forms of data-in-transit. Browsers will send traffic to {kib}, and +{kib} will send traffic to {es}. These are configured separately. + +==== Encrypting traffic between the browser and {kib} + +NOTE: You do not need to enable {security} for this type of encryption. + +. Obtain a server certificate and private key for {kib}. + -- -NOTE: You do not need to enable {security} for this type of encryption. +{kib} supports certificates/keys in both PKCS #12 key stores and PEM format. + +When you obtain a certificate, you must do at least one of the following: + +.. Set the certificate's `subjectAltName` to the hostname, fully-qualified domain name (FQDN), or IP address of the {kib} server. + +.. Set the certificate's Common Name (CN) to the {kib} server's hostname or FQDN. Using the server's IP address as the CN does not work. + +You may choose to generate a certificate and private key using {ref}/certutil.html[the {es} certutil tool]. If you already used certutil to +generate a certificate authority (CA), you would generate a certificate/key for Kibana like so (using the `--dns` param to set the +`subjectAltName`): + +[source,sh] +-------------------------------------------------------------------------------- +bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12 --name kibana --dns localhost +-------------------------------------------------------------------------------- + +This will generate a certificate/key contained PKCS #12 keystore named `kibana.p12`. -- -.. Generate a server certificate for {kib}. +. Enable TLS/SSL in `kibana.yml`: + -- -//TBD: Can we provide more information about how they generate the certificate? -//Would they be able to use something like the elasticsearch-certutil command? -You must either set the certificate's -`subjectAltName` to the hostname, fully-qualified domain name (FQDN), or IP -address of the {kib} server, or set the CN to the {kib} server's hostname -or FQDN. Using the server's IP address as the CN does not work. +[source,yaml] +-------------------------------------------------------------------------------- +server.ssl.enabled: true +-------------------------------------------------------------------------------- -- -.. Set the `server.ssl.enabled`, `server.ssl.key`, and `server.ssl.certificate` -properties in `kibana.yml`: +. Specify your server certificate and private key in `kibana.yml`: + -- +If your certificate/key are contained in a PKCS #12 keystore, specify it like so: + [source,yaml] -------------------------------------------------------------------------------- -server.ssl.enabled: true -server.ssl.key: /path/to/your/server.key -server.ssl.certificate: /path/to/your/server.crt +server.ssl.keystore.path: "/path/to/your/keystore.p12" +server.ssl.keystore.password: "optional decryption password" +-------------------------------------------------------------------------------- + +Otherwise, if your certificate/key are in PEM format, specify them like so: + +[source,yaml] +-------------------------------------------------------------------------------- +server.ssl.certificate: "/path/to/your/server.crt" +server.ssl.key: "/path/to/your/server.key" +server.ssl.keyPassphrase: "optional decryption password" -------------------------------------------------------------------------------- After making these changes, you must always access {kib} via HTTPS. For example, https://localhost:5601. -// TBD: The reference information for server.ssl.enabled says it "enables SSL for -// outgoing requests from the Kibana server to the browser". Do we need to -// reiterate here that only one side of the communications is encrypted? - For more information, see <>. -- -. Configure {kib} to connect to {es} via HTTPS: -+ --- +==== Encrypting traffic between {kib} and {es} + NOTE: To perform this step, you must {ref}/configuring-security.html[enable the {es} {security-features}] or you must have a proxy that provides an HTTPS endpoint for {es}. --- - -.. Specify the HTTPS protocol in the `elasticsearch.hosts` setting in the {kib} -configuration file, `kibana.yml`: +. Specify the HTTPS URL in the `elasticsearch.hosts` setting in the {kib} configuration file, `kibana.yml`: + -- [source,yaml] -------------------------------------------------------------------------------- elasticsearch.hosts: ["https://.com:9200"] -------------------------------------------------------------------------------- --- - -.. If you are using your own CA to sign certificates for {es}, set the -`elasticsearch.ssl.certificateAuthorities` setting in `kibana.yml` to specify -the location of the PEM file. -+ --- -[source,yaml] --------------------------------------------------------------------------------- -elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem --------------------------------------------------------------------------------- -Setting the `certificateAuthorities` property lets you use the default -`verificationMode` option of `full`. -//TBD: Is this still true? It isn't mentioned in https://www.elastic.co/guide/en/kibana/master/settings.html +Using the HTTPS protocol results in a default `elasticsearch.ssl.verificationMode` option of `full`, which utilizes hostname verification. For more information, see <>. -- -. (Optional) If the Elastic {monitor-features} are enabled, configure {kib} to -connect to the {es} monitoring cluster via HTTPS: +. Specify the {es} cluster's CA certificate chain in `kibana.yml`: + -- -NOTE: To perform this step, you must -{ref}/configuring-security.html[enable the {es} {security-features}] or you -must have a proxy that provides an HTTPS endpoint for {es}. --- +If you are using your own CA to sign certificates for {es}, then you need to specify the CA certificate chain in {kib} to properly establish +trust in TLS connections. If your CA certificate chain is contained in a PKCS #12 trust store, specify it like so: -.. Specify the HTTPS URL in the `xpack.monitoring.elasticsearch.hosts` setting in -the {kib} configuration file, `kibana.yml` -+ --- [source,yaml] -------------------------------------------------------------------------------- -xpack.monitoring.elasticsearch.hosts: ["https://:9200"] +elasticsearch.ssl.truststore.path: "/path/to/your/truststore.p12" +elasticsearch.ssl.truststore.password: "optional decryption password" -------------------------------------------------------------------------------- --- -.. Specify the `xpack.monitoring.elasticsearch.ssl.*` settings in the -`kibana.yml` file. -+ --- -For example, if you are using your own certificate authority to sign -certificates, specify the location of the PEM file in the `kibana.yml` file: +Otherwise, if your CA certificate chain is in PEM format, specify each certificate like so: [source,yaml] -------------------------------------------------------------------------------- -xpack.monitoring.elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem +elasticsearch.ssl.certificateAuthorities: ["/path/to/your/cacert1.pem", "/path/to/your/cacert2.pem"] -------------------------------------------------------------------------------- + -- + +(Optional) If the Elastic {monitor-features} are enabled, configure {kib} to connect to the {es} monitoring cluster via HTTPS. The steps are +the same as above, but each setting is prefixed by `xpack.monitoring.` -- e.g., `xpack.monitoring.elasticsearch.hosts`, +`xpack.monitoring.elasticsearch.ssl.truststore.path`, etc. diff --git a/x-pack/legacy/plugins/security/README.md b/x-pack/legacy/plugins/security/README.md index b4786a2df6c52..068f19ba9482b 100644 --- a/x-pack/legacy/plugins/security/README.md +++ b/x-pack/legacy/plugins/security/README.md @@ -1,7 +1,3 @@ # Kibana Security Plugin -1. Install the Security plugin on Kibana `bin/kibana plugin --install kibana/security/latest` -1. Modify [kibana.yml](https://github.com/elastic/kibana/blob/master/config/kibana.yml) and add `xpack.security.encryptionKey: "something_at_least_32_characters"` -1. Make sure that the following config options are also set: `elasticsearch.username`, `elasticsearch.password`, `server.ssl.certificate`, and `server.ssl.key` (see [Configuring Kibana to Work with Shield](https://www.elastic.co/guide/en/kibana/current/production.html#configuring-kibana-shield)) - -Once done, open up the following url (assuming standard kibana config): [https://localhost:5601](https://localhost:5601). +See [Configuring security in Kibana](https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html). From 89951d8a12af15d6b5eccf5503bf4d88512bff23 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 2 Jan 2020 11:22:01 -0500 Subject: [PATCH 13/21] Tech writer feedback --- docs/setup/settings.asciidoc | 83 ++++++++++--------- .../security/authentication/index.asciidoc | 2 +- .../securing-communications/index.asciidoc | 16 ++-- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index eb4c67e4a65f8..7d41f8fae945f 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -83,47 +83,51 @@ Elasticsearch nodes on startup. Elasticsearch nodes immediately following a connection fault. `elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls whether to always present the certificate specified by -`elasticsearch.ssl.certificate` or `elasticsearch.ssl.keystore.path` when requested. This applies to all requests to Elasticsearch, -including requests that are proxied for end-users. Setting this to `true` when Elasticsearch is using certificates to authenticate users can -lead to proxied requests for end-users being executed as the identity tied to the configured certificate. +`elasticsearch.ssl.certificate` or `elasticsearch.ssl.keystore.path` when requested. This setting applies to all requests to Elasticsearch, +including requests that are proxied for end users. Setting this to `true` when Elasticsearch is using certificates to authenticate users can +lead to proxied requests for end users being executed as the identity tied to the configured certificate. `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. When `xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required`, the certificate and key are used to prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster. - ++ +-- NOTE: These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. +-- -`elasticsearch.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. This may consist of a root certificate -authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This is used to establish trust when -Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via -`elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. +`elasticsearch.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. These certificates may consist of a root +certificate authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is used to +establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may +be specified via `elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. `elasticsearch.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via `elasticsearch.ssl.key`. This value is optional, as the key may not be encrypted. -`elasticsearch.ssl.keystore.path:`:: Path to a PKCS #12 file -- this should contain an X.509 certificate with its private key. When +`elasticsearch.ssl.keystore.path:`:: Path to a PKCS #12 file that contains an X.509 certificate with its private key. When `xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required`, the certificate and key are used to prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster. If the file contains any additional certificates, those will be -used as a trusted certificate chain for your Elasticsearch cluster. This is used to establish trust when Kibana creates an SSL connection -with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via +used as a trusted certificate chain for your Elasticsearch cluster. This chain is used to establish trust when Kibana creates an SSL +connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. - ++ +-- NOTE: This setting cannot be used in conjunction with `elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. +-- `elasticsearch.ssl.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has no password, leave this unset. If your key store has an empty password, set this to `""`. -`elasticsearch.ssl.truststore.path:`:: Path to a PKCS #12 trust store -- this should contain one or more X.509 certificates. This may -consist of a root certificate authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for your -Elasticsearch cluster. This is used to establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to -this setting, trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.keystore.path`. +`elasticsearch.ssl.truststore.path:`:: Path to a PKCS #12 trust store that contains one or more X.509 certificates. This may consist of a +root certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for your Elasticsearch cluster. +This chain is used to establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting, +trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.keystore.path`. `elasticsearch.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password, leave this unset. If your trust store has an empty password, set this to `""`. `elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the verification of certificates presented by Elasticsearch. Valid values -are `none`, `certificate`, and `full`. `full` performs hostname verification, and `certificate` does not. This is only used when traffic to -Elasticsearch is encrypted -- this is specified by using the HTTPS protocol in `elasticsearch.hosts`. +are `none`, `certificate`, and `full`. `full` performs hostname verification and `certificate` does not. This setting is used only when +traffic to Elasticsearch is encrypted, which is specified by using the HTTPS protocol in `elasticsearch.hosts`. `elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying. @@ -344,14 +348,17 @@ inactive socket. `server.ssl.certificate:` and `server.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. These are used when enabling SSL for inbound requests from web browsers to the Kibana server. - ++ +-- NOTE: These settings cannot be used in conjunction with `server.ssl.keystore.path`. +-- -`server.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. This may consist of a root certificate authority -(CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This is used when a web browser creates an SSL -connection with the Kibana server; the certificate chain is sent to the browser along with the end-entity certificate to establish trust. -This is also used to determine if client certificates should be trusted when PKI authentication is enabled. In addition to this setting, -trusted certificates may be specified via `server.ssl.keystore.path` and/or `server.ssl.truststore.path`. +`server.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. These certificates may consist of a root +certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is used when a +web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the end-entity +certificate to establish trust. This chain is also used to determine whether client certificates should be trusted when PKI authentication +is enabled. In addition to this setting, trusted certificates may be specified via `server.ssl.keystore.path` and/or +`server.ssl.truststore.path`. `server.ssl.cipherSuites:`:: *Default: ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA*. Details on the format, and the valid options, are available via the @@ -362,29 +369,31 @@ connections. Valid values are `required`, `optional`, and `none`. `required` for requests a client certificate but the client is not required to present one. `server.ssl.enabled:`:: *Default: "false"* Enables SSL for inbound requests from the browser to the Kibana server. When set to `true`, a -certificate and private key must be provided. These can be specified via `server.ssl.keystore.path`, or the combination of +certificate and private key must be provided. These can be specified via `server.ssl.keystore.path` or the combination of `server.ssl.certificate` and `server.ssl.key`. `server.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via `server.ssl.key`. This value is optional, as the key may not be encrypted. -`server.ssl.keystore.path:`:: Path to a PKCS #12 file -- this should contain an X.509 certificate with its private key. These are used when -enabling SSL for inbound requests from web browsers to the Kibana server. If the file contains any additional certificates, those will be -used as a trusted certificate chain for Kibana. This is used when a web browser creates an SSL connection with the Kibana server; the -certificate chain is sent to the browser along with the end-entity certificate to establish trust. This is also used to determine if client -certificates should be trusted when PKI authentication is enabled. In addition to this setting, trusted certificates may be specified via -`server.ssl.certificateAuthorities` and/or `server.ssl.truststore.path`. - +`server.ssl.keystore.path:`:: Path to a PKCS #12 file that contains an X.509 certificate with its private key. These are used when enabling +SSL for inbound requests from web browsers to the Kibana server. If the file contains any additional certificates, those will be used as a +trusted certificate chain for Kibana. This chain is used when a web browser creates an SSL connection with the Kibana server; the +certificate chain is sent to the browser along with the end-entity certificate to establish trust. This chain is also used to determine +whether client certificates should be trusted when PKI authentication is enabled. In addition to this setting, trusted certificates may be +specified via `server.ssl.certificateAuthorities` and/or `server.ssl.truststore.path`. ++ +-- NOTE: This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key`. +-- `server.ssl.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has no password, leave this unset. If your key store has an empty password, set this to `""`. -`server.ssl.truststore.path:`:: Path to a PKCS #12 trust store -- this should contain one or more X.509 certificates. This may consist of a -root certificate authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This is used when a -web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the end-entity -certificate to establish trust. This is also used to determine if client certificates should be trusted when PKI authentication is enabled. -In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or +`server.ssl.truststore.path:`:: Path to a PKCS #12 trust store that contains one or more X.509 certificates. These certificates may consist +of a root certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is +used when a web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the +end-entity certificate to establish trust. This chain is also used to determine whether client certificates should be trusted when PKI +authentication is enabled. In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or `server.ssl.keystore.path`. `server.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password, leave diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index ce0f1f8c6545b..05aabfc343be9 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -68,7 +68,7 @@ xpack.security.authc.providers: [pki] -------------------------------------------------------------------------------- NOTE: Trusted CAs can also be specified in a PKCS #12 keystore bundled with your Kibana server certificate/key using -`server.ssl.keystore.path`, or in a separate trust store using `server.ssl.truststore.path`. +`server.ssl.keystore.path` or in a separate trust store using `server.ssl.truststore.path`. PKI support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both PKI and Basic authentication for the same {kib} instance: diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc index f1eaa38ac0e04..b370c35905bce 100644 --- a/docs/user/security/securing-communications/index.asciidoc +++ b/docs/user/security/securing-communications/index.asciidoc @@ -4,12 +4,13 @@ Encrypting communications ++++ -{kib} supports Transport Layer Security (TLS/SSL) encryption for all forms of data-in-transit. Browsers will send traffic to {kib}, and -{kib} will send traffic to {es}. These are configured separately. +{kib} supports Transport Layer Security (TLS/SSL) encryption for all forms of data-in-transit. Browsers send traffic to {kib} and {kib} +sends traffic to {es}. These communications are configured separately. +[[configuring-tls-browser-kib]] ==== Encrypting traffic between the browser and {kib} -NOTE: You do not need to enable {security} for this type of encryption. +NOTE: You do not need to enable {security-features} for this type of encryption. . Obtain a server certificate and private key for {kib}. + @@ -31,7 +32,7 @@ generate a certificate authority (CA), you would generate a certificate/key for bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12 --name kibana --dns localhost -------------------------------------------------------------------------------- -This will generate a certificate/key contained PKCS #12 keystore named `kibana.p12`. +This will generate a certificate and private key in a PKCS #12 keystore named `kibana.p12`. -- @@ -47,7 +48,7 @@ server.ssl.enabled: true . Specify your server certificate and private key in `kibana.yml`: + -- -If your certificate/key are contained in a PKCS #12 keystore, specify it like so: +If your certificate and private key are in a PKCS #12 keystore, specify it like so: [source,yaml] -------------------------------------------------------------------------------- @@ -70,6 +71,7 @@ https://localhost:5601. For more information, see <>. -- +[[configuring-tls-kib-es]] ==== Encrypting traffic between {kib} and {es} NOTE: To perform this step, you must @@ -111,6 +113,6 @@ elasticsearch.ssl.certificateAuthorities: ["/path/to/your/cacert1.pem", "/path/t -- -(Optional) If the Elastic {monitor-features} are enabled, configure {kib} to connect to the {es} monitoring cluster via HTTPS. The steps are -the same as above, but each setting is prefixed by `xpack.monitoring.` -- e.g., `xpack.monitoring.elasticsearch.hosts`, +. (Optional) If the Elastic {monitor-features} are enabled, configure {kib} to connect to the {es} monitoring cluster via HTTPS. The steps +are the same as above, but each setting is prefixed by `xpack.monitoring.`. For example, `xpack.monitoring.elasticsearch.hosts`, `xpack.monitoring.elasticsearch.ssl.truststore.path`, etc. From 035b7189fa8f856a331397c447789eb0d72b31ff Mon Sep 17 00:00:00 2001 From: kobelb Date: Fri, 3 Jan 2020 10:57:33 -0800 Subject: [PATCH 14/21] Changing server integration tests to use explicit CAs --- test/server_integration/config.js | 4 ++-- test/server_integration/http/ssl/config.js | 10 +++++++-- .../http/ssl_redirect/config.js | 10 +++++---- .../http/ssl_with_p12/config.js | 12 +++++++--- test/server_integration/services/index.js | 2 +- test/server_integration/services/supertest.js | 22 +++++++------------ 6 files changed, 34 insertions(+), 26 deletions(-) diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 6928dedb9fb6f..26e00e5fce294 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -18,7 +18,7 @@ */ import { - KibanaSupertestProvider, + createKibanaSupertestProvider, KibanaSupertestWithoutAuthProvider, ElasticsearchSupertestProvider, } from './services'; @@ -30,7 +30,7 @@ export default async function({ readConfigFile }) { return { services: { ...commonConfig.get('services'), - supertest: KibanaSupertestProvider, + supertest: createKibanaSupertestProvider(), supertestWithoutAuth: KibanaSupertestWithoutAuthProvider, esSupertest: ElasticsearchSupertestProvider, }, diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index 4ce4c6002df24..2f2e7b778d361 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -17,14 +17,21 @@ * under the License. */ +import { readFileSync } from 'fs'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { createKibanaSupertestProvider } from '../../services'; export default async function({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); return { testFiles: [require.resolve('./')], - services: httpConfig.get('services'), + services: { + ...httpConfig.get('services'), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA_CERT_PATH)], + }), + }, servers: { ...httpConfig.get('servers'), kibana: { @@ -41,7 +48,6 @@ export default async function({ readConfigFile }) { serverArgs: [ ...httpConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, // this is needed for the test runner to trust the server certificate `--server.ssl.key=${KBN_KEY_PATH}`, `--server.ssl.certificate=${KBN_CERT_PATH}`, ], diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 56f8c5bf3f908..20ab4a210cc7b 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -17,9 +17,10 @@ * under the License. */ +import { readFileSync } from 'fs'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; -import { KibanaSupertestProvider } from '../../services'; +import { createKibanaSupertestProvider } from '../../services'; export default async function({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); @@ -36,8 +37,10 @@ export default async function({ readConfigFile }) { testFiles: [require.resolve('./')], services: { ...httpConfig.get('services'), - //eslint-disable-next-line new-cap - supertest: arg => KibanaSupertestProvider(arg, supertestOptions), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA_CERT_PATH)], + options: supertestOptions, + }), }, servers: { ...httpConfig.get('servers'), @@ -56,7 +59,6 @@ export default async function({ readConfigFile }) { serverArgs: [ ...httpConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, // this is needed for the test runner to trust the server certificate `--server.ssl.key=${KBN_KEY_PATH}`, `--server.ssl.certificate=${KBN_CERT_PATH}`, `--server.ssl.redirectHttpFromPort=${redirectPort}`, diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index caf8a1c857b27..e220914af54f4 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -17,14 +17,21 @@ * under the License. */ -import { KBN_P12_PATH, KBN_P12_PASSWORD, CA_CERT_PATH } from '@kbn/dev-utils'; +import { readFileSync } from 'fs'; +import { CA_CERT_PATH, KBN_P12_PATH, KBN_P12_PASSWORD } from '@kbn/dev-utils'; +import { createKibanaSupertestProvider } from '../../services'; export default async function({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); return { testFiles: [require.resolve('./')], - services: httpConfig.get('services'), + services: { + ...httpConfig.get('services'), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA_CERT_PATH)], + }), + }, servers: { ...httpConfig.get('servers'), kibana: { @@ -41,7 +48,6 @@ export default async function({ readConfigFile }) { serverArgs: [ ...httpConfig.get('kbnTestServer.serverArgs'), '--server.ssl.enabled=true', - `--server.ssl.certificateAuthorities=${CA_CERT_PATH}`, // this is needed for the test runner to trust the server certificate `--server.ssl.keystore.path=${KBN_P12_PATH}`, `--server.ssl.keystore.password=${KBN_P12_PASSWORD}`, ], diff --git a/test/server_integration/services/index.js b/test/server_integration/services/index.js index 32a11f30b8683..4904bfc9eeef6 100644 --- a/test/server_integration/services/index.js +++ b/test/server_integration/services/index.js @@ -18,7 +18,7 @@ */ export { - KibanaSupertestProvider, + createKibanaSupertestProvider, KibanaSupertestWithoutAuthProvider, ElasticsearchSupertestProvider, } from './supertest'; diff --git a/test/server_integration/services/supertest.js b/test/server_integration/services/supertest.js index c09932b0207ac..74bb5400bc299 100644 --- a/test/server_integration/services/supertest.js +++ b/test/server_integration/services/supertest.js @@ -17,25 +17,19 @@ * under the License. */ -import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import supertestAsPromised from 'supertest-as-promised'; -export function KibanaSupertestProvider({ getService }, options) { - const config = getService('config'); - const kibanaServerUrl = options ? formatUrl(options) : formatUrl(config.get('servers.kibana')); - - const kibanaServerCert = config - .get('kbnTestServer.serverArgs') - .filter(arg => arg.startsWith('--server.ssl.certificate')) - .map(arg => arg.split('=').pop()) - .map(path => readFileSync(path)) - .shift(); +export function createKibanaSupertestProvider({ certificateAuthorities, options } = {}) { + return function({ getService }) { + const config = getService('config'); + const kibanaServerUrl = options ? formatUrl(options) : formatUrl(config.get('servers.kibana')); - return kibanaServerCert - ? supertestAsPromised.agent(kibanaServerUrl, { ca: kibanaServerCert }) - : supertestAsPromised(kibanaServerUrl); + return certificateAuthorities + ? supertestAsPromised.agent(kibanaServerUrl, { ca: certificateAuthorities }) + : supertestAsPromised(kibanaServerUrl); + }; } export function KibanaSupertestWithoutAuthProvider({ getService }) { From 0ff4b1df404acf333f39fd8522aaeae84bd2d275 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sat, 4 Jan 2020 14:09:02 -0500 Subject: [PATCH 15/21] Address feedback from code review Also changed readPkcs12Keystore to determine which end-entity key and cert to return based on if they have a matching public key. The old behavior would simply return the first key and cert as the EE key and cert. --- docs/setup/settings.asciidoc | 12 +- .../elasticsearch_config.test.ts | 10 + .../elasticsearch/elasticsearch_config.ts | 16 +- src/core/server/http/ssl_config.test.ts | 568 +++++++++--------- src/core/server/http/ssl_config.ts | 4 +- src/core/utils/crypto/pkcs12.test.ts | 67 ++- src/core/utils/crypto/pkcs12.ts | 86 ++- 7 files changed, 412 insertions(+), 351 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 7d41f8fae945f..986e4b0a9ce94 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -88,8 +88,8 @@ including requests that are proxied for end users. Setting this to `true` when E lead to proxied requests for end users being executed as the identity tied to the configured certificate. `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. -When `xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required`, the certificate and key are used to prove -Kibana's identity when it makes an outbound request to your Elasticsearch cluster. +When `xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required` or `optional`, the certificate and key are used +to prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster. + -- NOTE: These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. @@ -104,10 +104,10 @@ be specified via `elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.tru `elasticsearch.ssl.key`. This value is optional, as the key may not be encrypted. `elasticsearch.ssl.keystore.path:`:: Path to a PKCS #12 file that contains an X.509 certificate with its private key. When -`xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required`, the certificate and key are used to prove Kibana's -identity when it makes an outbound request to your Elasticsearch cluster. If the file contains any additional certificates, those will be -used as a trusted certificate chain for your Elasticsearch cluster. This chain is used to establish trust when Kibana creates an SSL -connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via +`xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required` or `optional`, the certificate and key are used to +prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster. If the file contains any additional certificates, +those will be used as a trusted certificate chain for your Elasticsearch cluster. This chain is used to establish trust when Kibana creates +an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. + -- diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 025c703d75304..c0db7369b4b99 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -257,6 +257,16 @@ describe('throws when config is invalid', () => { ); }); + it('throws if keystore does not contain a key or certificate', () => { + mockReadPkcs12Keystore.mockReturnValueOnce({}); + const value = { ssl: { keystore: { path: 'some-path' } } }; + expect(() => + createElasticsearchConfig(config.schema.validate(value)) + ).toThrowErrorMatchingInlineSnapshot( + `"Did not find key or certificate in Elasticsearch keystore."` + ); + }); + it('throws if truststore path is invalid', () => { const value = { ssl: { keystore: { path: '/invalid/truststore' } } }; expect(() => diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 2910298413d57..0cbc4dafcfb3b 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -232,23 +232,21 @@ export class ElasticsearchConfig { let certificateAuthorities: string[] | undefined; const addCAs = (ca: string[] | undefined) => { if (ca && ca.length) { - certificateAuthorities = certificateAuthorities ? certificateAuthorities.concat(ca) : ca; + certificateAuthorities = [...(certificateAuthorities || []), ...ca]; } }; if (rawConfig.ssl.keystore?.path) { - const { key: k, cert, ca } = readPkcs12Keystore( + const keystore = readPkcs12Keystore( rawConfig.ssl.keystore.path, rawConfig.ssl.keystore.password ); - if (!k && !cert) { - log.warn( - `Did not find key or certificate in keystore; mutual TLS authentication is disabled.` - ); + if (!keystore.key && !keystore.cert) { + throw new Error(`Did not find key or certificate in Elasticsearch keystore.`); } - key = k; - certificate = cert; - addCAs(ca); + key = keystore.key; + certificate = keystore.cert; + addCAs(keystore.ca); } else { if (rawConfig.ssl.key) { key = readFile(rawConfig.ssl.key); diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts index dcfb621326f90..738f86f7a69eb 100644 --- a/src/core/server/http/ssl_config.test.ts +++ b/src/core/server/http/ssl_config.test.ts @@ -25,333 +25,339 @@ import { import { sslSchema, SslConfig } from './ssl_config'; -const createConfig = (obj: any) => new SslConfig(sslSchema.validate(obj)); - -beforeEach(() => { - mockReadFileSync.mockReset(); - mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); - mockReadPkcs12Keystore.mockReset(); - mockReadPkcs12Keystore.mockImplementation((path: string) => ({ - key: `content-of-${path}.key`, - cert: `content-of-${path}.cert`, - ca: [`content-of-${path}.ca`], - })); - mockReadPkcs12Truststore.mockReset(); - mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); -}); +describe('#SslConfig', () => { + const createConfig = (obj: any) => new SslConfig(sslSchema.validate(obj)); -describe('throws when config is invalid', () => { beforeEach(() => { - const realFs = jest.requireActual('fs'); - mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); - const utils = jest.requireActual('../../utils'); - mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => - utils.readPkcs12Keystore(path, password) - ); - mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => - utils.readPkcs12Truststore(path, password) - ); + mockReadFileSync.mockReset(); + mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`); + mockReadPkcs12Keystore.mockReset(); + mockReadPkcs12Keystore.mockImplementation((path: string) => ({ + key: `content-of-${path}.key`, + cert: `content-of-${path}.cert`, + ca: [`content-of-${path}.ca`], + })); + mockReadPkcs12Truststore.mockReset(); + mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]); }); - test('throws if `key` is invalid', () => { - const obj = { key: '/invalid/key', certificate: '/valid/certificate' }; - expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( - `"ENOENT: no such file or directory, open '/invalid/key'"` - ); - }); + describe('throws when config is invalid', () => { + beforeEach(() => { + const realFs = jest.requireActual('fs'); + mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path)); + const utils = jest.requireActual('../../utils'); + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Keystore(path, password) + ); + mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) => + utils.readPkcs12Truststore(path, password) + ); + }); - test('throws if `certificate` is invalid', () => { - mockReadFileSync.mockImplementationOnce((path: string) => `content-of-${path}`); - const obj = { key: '/valid/key', certificate: '/invalid/certificate' }; - expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( - `"ENOENT: no such file or directory, open '/invalid/certificate'"` - ); - }); + test('throws if `key` is invalid', () => { + const obj = { key: '/invalid/key', certificate: '/valid/certificate' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/key'"` + ); + }); - test('throws if `certificateAuthorities` is invalid', () => { - const obj = { certificateAuthorities: '/invalid/ca' }; - expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( - `"ENOENT: no such file or directory, open '/invalid/ca'"` - ); - }); + test('throws if `certificate` is invalid', () => { + mockReadFileSync.mockImplementationOnce((path: string) => `content-of-${path}`); + const obj = { key: '/valid/key', certificate: '/invalid/certificate' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/certificate'"` + ); + }); - test('throws if `keystore.path` is invalid', () => { - const obj = { keystore: { path: '/invalid/keystore' } }; - expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( - `"ENOENT: no such file or directory, open '/invalid/keystore'"` - ); - }); + test('throws if `certificateAuthorities` is invalid', () => { + const obj = { certificateAuthorities: '/invalid/ca' }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/ca'"` + ); + }); - test('throws if `keystore.path` does not contain a private key', () => { - mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ - key: undefined, - certificate: 'foo', - })); - const obj = { keystore: { path: 'some-path' } }; - expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( - `"Did not find private key in keystore at [keystore.path]."` - ); - }); + test('throws if `keystore.path` is invalid', () => { + const obj = { keystore: { path: '/invalid/keystore' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/keystore'"` + ); + }); - test('throws if `keystore.path` does not contain a certificate', () => { - mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ - key: 'foo', - certificate: undefined, - })); - const obj = { keystore: { path: 'some-path' } }; - expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( - `"Did not find certificate in keystore at [keystore.path]."` - ); - }); + test('throws if `keystore.path` does not contain a private key', () => { + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ + key: undefined, + certificate: 'foo', + })); + const obj = { keystore: { path: 'some-path' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"Did not find private key in keystore at [keystore.path]."` + ); + }); - test('throws if `truststore.path` is invalid', () => { - const obj = { truststore: { path: '/invalid/truststore' } }; - expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( - `"ENOENT: no such file or directory, open '/invalid/truststore'"` - ); - }); + test('throws if `keystore.path` does not contain a certificate', () => { + mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({ + key: 'foo', + certificate: undefined, + })); + const obj = { keystore: { path: 'some-path' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"Did not find certificate in keystore at [keystore.path]."` + ); + }); - test('throws if both `key` and `keystore.path` are specified', () => { - const obj = { - key: '/path/to/key', - keystore: { - path: 'path/to/keystore', - }, - }; - expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"cannot use [key] when [keystore.path] is specified"` - ); + test('throws if `truststore.path` is invalid', () => { + const obj = { truststore: { path: '/invalid/truststore' } }; + expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot( + `"ENOENT: no such file or directory, open '/invalid/truststore'"` + ); + }); }); - test('throws if both `certificate` and `keystore.path` are specified', () => { - const obj = { - certificate: '/path/to/certificate', - keystore: { - path: 'path/to/keystore', - }, - }; - expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"cannot use [certificate] when [keystore.path] is specified"` - ); - }); + describe('reads files', () => { + it('reads certificate authorities when `keystore.path` is specified', () => { + const configValue = createConfig({ keystore: { path: 'some-path' } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path.ca']); + }); - test('throws if TLS is enabled but `certificate` is specified and `key` is not', () => { - const obj = { - certificate: '/path/to/certificate', - enabled: true, - }; - expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` - ); - }); + it('reads certificate authorities when `truststore.path` is specified', () => { + const configValue = createConfig({ truststore: { path: 'some-path' } }); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + }); - test('throws if TLS is enabled but `key` is specified and `certificate` is not', () => { - const obj = { - enabled: true, - key: '/path/to/key', - }; - expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` - ); - }); + it('reads certificate authorities when `certificateAuthorities` is specified', () => { + let configValue = createConfig({ certificateAuthorities: 'some-path' }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = createConfig({ certificateAuthorities: ['some-path'] }); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); + + mockReadFileSync.mockClear(); + configValue = createConfig({ certificateAuthorities: ['some-path', 'another-path'] }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.certificateAuthorities).toEqual([ + 'content-of-some-path', + 'content-of-another-path', + ]); + }); - test('throws if TLS is enabled but `key`, `certificate`, and `keystore.path` are not specified', () => { - const obj = { - enabled: true, - }; - expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` - ); - }); + it('reads certificate authorities when `keystore.path`, `truststore.path`, and `certificateAuthorities` are specified', () => { + const configValue = createConfig({ + keystore: { path: 'some-path' }, + truststore: { path: 'another-path' }, + certificateAuthorities: 'yet-another-path', + }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); + expect(mockReadFileSync).toHaveBeenCalledTimes(1); + expect(configValue.certificateAuthorities).toEqual([ + 'content-of-some-path.ca', + 'content-of-another-path', + 'content-of-yet-another-path', + ]); + }); - test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => { - const obj = { - enabled: false, - clientAuthentication: 'optional', - }; - expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"must enable ssl to use [clientAuthentication]"` - ); - }); + it('reads a private key and certificate when `keystore.path` is specified', () => { + const configValue = createConfig({ keystore: { path: 'some-path' } }); + expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); + expect(configValue.key).toEqual('content-of-some-path.key'); + expect(configValue.certificate).toEqual('content-of-some-path.cert'); + }); - test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => { - const obj = { - enabled: false, - clientAuthentication: 'required', - }; - expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( - `"must enable ssl to use [clientAuthentication]"` - ); + it('reads a private key and certificate when `key` and `certificate` are specified', () => { + const configValue = createConfig({ key: 'some-path', certificate: 'another-path' }); + expect(mockReadFileSync).toHaveBeenCalledTimes(2); + expect(configValue.key).toEqual('content-of-some-path'); + expect(configValue.certificate).toEqual('content-of-another-path'); + }); }); }); -describe('reads files', () => { - it('reads certificate authorities when `keystore.path` is specified', () => { - const configValue = createConfig({ keystore: { path: 'some-path' } }); - expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); - expect(configValue.certificateAuthorities).toEqual(['content-of-some-path.ca']); - }); +describe('#sslSchema', () => { + describe('throws when config is invalid', () => { + test('throws if both `key` and `keystore.path` are specified', () => { + const obj = { + key: '/path/to/key', + keystore: { + path: 'path/to/keystore', + }, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"cannot use [key] when [keystore.path] is specified"` + ); + }); - it('reads certificate authorities when `truststore.path` is specified', () => { - const configValue = createConfig({ truststore: { path: 'some-path' } }); - expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); - expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); - }); + test('throws if both `certificate` and `keystore.path` are specified', () => { + const obj = { + certificate: '/path/to/certificate', + keystore: { + path: 'path/to/keystore', + }, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"cannot use [certificate] when [keystore.path] is specified"` + ); + }); - it('reads certificate authorities when `certificateAuthorities` is specified', () => { - let configValue = createConfig({ certificateAuthorities: 'some-path' }); - expect(mockReadFileSync).toHaveBeenCalledTimes(1); - expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); - - mockReadFileSync.mockClear(); - configValue = createConfig({ certificateAuthorities: ['some-path'] }); - expect(mockReadFileSync).toHaveBeenCalledTimes(1); - expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']); - - mockReadFileSync.mockClear(); - configValue = createConfig({ certificateAuthorities: ['some-path', 'another-path'] }); - expect(mockReadFileSync).toHaveBeenCalledTimes(2); - expect(configValue.certificateAuthorities).toEqual([ - 'content-of-some-path', - 'content-of-another-path', - ]); - }); + test('throws if TLS is enabled but `certificate` is specified and `key` is not', () => { + const obj = { + certificate: '/path/to/certificate', + enabled: true, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` + ); + }); + + test('throws if TLS is enabled but `key` is specified and `certificate` is not', () => { + const obj = { + enabled: true, + key: '/path/to/key', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` + ); + }); - it('reads certificate authorities when `keystore.path`, `truststore.path`, and `certificateAuthorities` are specified', () => { - const configValue = createConfig({ - keystore: { path: 'some-path' }, - truststore: { path: 'another-path' }, - certificateAuthorities: 'yet-another-path', + test('throws if TLS is enabled but `key`, `certificate`, and `keystore.path` are not specified', () => { + const obj = { + enabled: true, + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"` + ); }); - expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); - expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1); - expect(mockReadFileSync).toHaveBeenCalledTimes(1); - expect(configValue.certificateAuthorities).toEqual([ - 'content-of-some-path.ca', - 'content-of-another-path', - 'content-of-yet-another-path', - ]); - }); - it('reads a private key and certificate when `keystore.path` is specified', () => { - const configValue = createConfig({ keystore: { path: 'some-path' } }); - expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1); - expect(configValue.key).toEqual('content-of-some-path.key'); - expect(configValue.certificate).toEqual('content-of-some-path.cert'); - }); + test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => { + const obj = { + enabled: false, + clientAuthentication: 'optional', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must enable ssl to use [clientAuthentication]"` + ); + }); - it('reads a private key and certificate when `key` and `certificate` are specified', () => { - const configValue = createConfig({ key: 'some-path', certificate: 'another-path' }); - expect(mockReadFileSync).toHaveBeenCalledTimes(2); - expect(configValue.key).toEqual('content-of-some-path'); - expect(configValue.certificate).toEqual('content-of-another-path'); + test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => { + const obj = { + enabled: false, + clientAuthentication: 'required', + }; + expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot( + `"must enable ssl to use [clientAuthentication]"` + ); + }); }); -}); -describe('#supportedProtocols', () => { - test('accepts known protocols`', () => { - const singleKnownProtocol = { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1'], - }; - - const allKnownProtocols = { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'], - }; - - const singleKnownProtocolConfig = sslSchema.validate(singleKnownProtocol); - expect(singleKnownProtocolConfig.supportedProtocols).toEqual(['TLSv1']); - - const allKnownProtocolsConfig = sslSchema.validate(allKnownProtocols); - expect(allKnownProtocolsConfig.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']); - }); + describe('#supportedProtocols', () => { + test('accepts known protocols`', () => { + const singleKnownProtocol = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1'], + }; + + const allKnownProtocols = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'], + }; + + const singleKnownProtocolConfig = sslSchema.validate(singleKnownProtocol); + expect(singleKnownProtocolConfig.supportedProtocols).toEqual(['TLSv1']); + + const allKnownProtocolsConfig = sslSchema.validate(allKnownProtocols); + expect(allKnownProtocolsConfig.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']); + }); + + test('rejects unknown protocols`', () => { + const singleUnknownProtocol = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['SOMEv100500'], + }; - test('rejects unknown protocols`', () => { - const singleUnknownProtocol = { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['SOMEv100500'], - }; - - const allKnownWithOneUnknownProtocols = { - certificate: '/path/to/certificate', - enabled: true, - key: '/path/to/key', - supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'], - }; - - expect(() => sslSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingInlineSnapshot(` + const allKnownWithOneUnknownProtocols = { + certificate: '/path/to/certificate', + enabled: true, + key: '/path/to/key', + supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'], + }; + + expect(() => sslSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingInlineSnapshot(` "[supportedProtocols.0]: types that failed validation: - [supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500] - [supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] - [supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" `); - expect(() => sslSchema.validate(allKnownWithOneUnknownProtocols)) - .toThrowErrorMatchingInlineSnapshot(` + expect(() => sslSchema.validate(allKnownWithOneUnknownProtocols)) + .toThrowErrorMatchingInlineSnapshot(` "[supportedProtocols.3]: types that failed validation: - [supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500] - [supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500] - [supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]" `); - }); -}); - -describe('#clientAuthentication', () => { - test('can specify `none` client authentication when ssl is not enabled', () => { - const obj = { - enabled: false, - clientAuthentication: 'none', - }; - - const configValue = sslSchema.validate(obj); - expect(configValue.clientAuthentication).toBe('none'); + }); }); - test('should properly interpret `none` client authentication when ssl is enabled', () => { - const sslConfig = new SslConfig( - sslSchema.validate({ - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', + describe('#clientAuthentication', () => { + test('can specify `none` client authentication when ssl is not enabled', () => { + const obj = { + enabled: false, clientAuthentication: 'none', - }) - ); - - expect(sslConfig.requestCert).toBe(false); - expect(sslConfig.rejectUnauthorized).toBe(false); - }); + }; - test('should properly interpret `optional` client authentication when ssl is enabled', () => { - const sslConfig = new SslConfig( - sslSchema.validate({ - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'optional', - }) - ); + const configValue = sslSchema.validate(obj); + expect(configValue.clientAuthentication).toBe('none'); + }); - expect(sslConfig.requestCert).toBe(true); - expect(sslConfig.rejectUnauthorized).toBe(false); - }); + test('should properly interpret `none` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'none', + }) + ); + + expect(sslConfig.requestCert).toBe(false); + expect(sslConfig.rejectUnauthorized).toBe(false); + }); - test('should properly interpret `required` client authentication when ssl is enabled', () => { - const sslConfig = new SslConfig( - sslSchema.validate({ - enabled: true, - key: 'some-key-path', - certificate: 'some-certificate-path', - clientAuthentication: 'required', - }) - ); + test('should properly interpret `optional` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'optional', + }) + ); + + expect(sslConfig.requestCert).toBe(true); + expect(sslConfig.rejectUnauthorized).toBe(false); + }); - expect(sslConfig.requestCert).toBe(true); - expect(sslConfig.rejectUnauthorized).toBe(true); + test('should properly interpret `required` client authentication when ssl is enabled', () => { + const sslConfig = new SslConfig( + sslSchema.validate({ + enabled: true, + key: 'some-key-path', + certificate: 'some-certificate-path', + clientAuthentication: 'required', + }) + ); + + expect(sslConfig.requestCert).toBe(true); + expect(sslConfig.rejectUnauthorized).toBe(true); + }); }); }); diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts index 5ef81b5bc3956..0096eeb092565 100644 --- a/src/core/server/http/ssl_config.ts +++ b/src/core/server/http/ssl_config.ts @@ -113,9 +113,7 @@ export class SslConfig { const addCAs = (ca: string[] | undefined) => { if (ca && ca.length) { - this.certificateAuthorities = this.certificateAuthorities - ? this.certificateAuthorities.concat(ca) - : ca; + this.certificateAuthorities = [...(this.certificateAuthorities || []), ...ca]; } }; diff --git a/src/core/utils/crypto/pkcs12.test.ts b/src/core/utils/crypto/pkcs12.test.ts index f775d37af7764..e162db158f7ea 100644 --- a/src/core/utils/crypto/pkcs12.test.ts +++ b/src/core/utils/crypto/pkcs12.test.ts @@ -28,84 +28,99 @@ import { } from '@kbn/dev-utils'; import { readFileSync } from 'fs'; -import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from './pkcs12'; +import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from '.'; + +const reformatPem = (pem: string) => { + // ensure consistency in line endings when comparing two PEM files + return pem.replace(/\r\n/g, '\n').trim(); +}; const readPem = (file: string) => { const raw = readFileSync(file, 'utf8'); // strip bag attributes that are included from a previous PKCS #12 export - return raw.substr(raw.indexOf('-----BEGIN')).trim(); + const pem = raw.substr(raw.indexOf('-----BEGIN')); + return reformatPem(pem); }; describe('#readPkcs12Keystore', () => { + const expectKey = (pkcs12ReadResult: Pkcs12ReadResult) => { + const result = reformatPem(pkcs12ReadResult.key!); + const pemKey = readPem(ES_KEY_PATH); + expect(result).toEqual(pemKey); + }; + + const expectCert = (pkcs12ReadResult: Pkcs12ReadResult) => { + const result = reformatPem(pkcs12ReadResult.cert!); + const pemCert = readPem(ES_CERT_PATH); + expect(result).toEqual(pemCert); + }; + + const expectCA = (pkcs12ReadResult: Pkcs12ReadResult) => { + const result = pkcs12ReadResult.ca?.map(x => reformatPem(x)); + const pemCA = readPem(CA_CERT_PATH); + expect(result).toEqual([pemCA]); + }; + describe('Succeeds when the correct password is used', () => { let pkcs12ReadResult: Pkcs12ReadResult; - it('Reads the keystore without error', () => { + beforeAll(() => { // this is expensive, just do it once pkcs12ReadResult = readPkcs12Keystore(ES_P12_PATH, ES_P12_PASSWORD); }); it('Extracts the PEM key', () => { - const pemKey = readPem(ES_KEY_PATH); - expect(pkcs12ReadResult.key).toEqual(pemKey); + expectKey(pkcs12ReadResult); }); it('Extracts the PEM instance certificate', () => { - const pemCert = readPem(ES_CERT_PATH); - expect(pkcs12ReadResult.cert).toEqual(pemCert); + expectCert(pkcs12ReadResult); }); it('Extracts the PEM CA certificate', () => { - const pemCA = readPem(CA_CERT_PATH); - expect(pkcs12ReadResult.ca).toEqual([pemCA]); + expectCA(pkcs12ReadResult); }); }); describe('Succeeds on a key store with an empty password', () => { let pkcs12ReadResult: Pkcs12ReadResult; - it('Reads the keystore without error', () => { + beforeAll(() => { // this is expensive, just do it once pkcs12ReadResult = readPkcs12Keystore(ES_EMPTYPASSWORD_P12_PATH, ''); }); it('Extracts the PEM key', () => { - const pemKey = readPem(ES_KEY_PATH); - expect(pkcs12ReadResult.key).toEqual(pemKey); + expectKey(pkcs12ReadResult); }); it('Extracts the PEM instance certificate', () => { - const pemCert = readPem(ES_CERT_PATH); - expect(pkcs12ReadResult.cert).toEqual(pemCert); + expectCert(pkcs12ReadResult); }); it('Extracts the PEM CA certificate', () => { - const pemCA = readPem(CA_CERT_PATH); - expect(pkcs12ReadResult.ca).toEqual([pemCA]); + expectCA(pkcs12ReadResult); }); }); describe('Succeeds on a key store with no password', () => { let pkcs12ReadResult: Pkcs12ReadResult; - it('Reads the keystore without error', () => { + beforeAll(() => { // this is expensive, just do it once pkcs12ReadResult = readPkcs12Keystore(ES_NOPASSWORD_P12_PATH); }); it('Extracts the PEM key', () => { - const pemKey = readPem(ES_KEY_PATH); - expect(pkcs12ReadResult.key).toEqual(pemKey); + expectKey(pkcs12ReadResult); }); it('Extracts the PEM instance certificate', () => { - const pemCert = readPem(ES_CERT_PATH); - expect(pkcs12ReadResult.cert).toEqual(pemCert); + expectCert(pkcs12ReadResult); }); it('Extracts the PEM CA certificate', () => { - const pemCA = readPem(CA_CERT_PATH); - expect(pkcs12ReadResult.ca).toEqual([pemCA]); + expectCA(pkcs12ReadResult); }); }); @@ -131,11 +146,11 @@ describe('#readPkcs12Keystore', () => { }); describe('#readPkcs12Truststore', () => { - it('reads all certificates into one CA array and discards any keys', () => { + it('reads all certificates into one CA array and discards any certificates that have keys', () => { const ca = readPkcs12Truststore(ES_P12_PATH, ES_P12_PASSWORD); - const pemCert = readPem(ES_CERT_PATH); + const result = ca?.map(x => reformatPem(x)); const pemCA = readPem(CA_CERT_PATH); - expect(ca).toEqual([pemCert, pemCA]); + expect(result).toEqual([pemCA]); }); }); diff --git a/src/core/utils/crypto/pkcs12.ts b/src/core/utils/crypto/pkcs12.ts index ad600e905e705..b13a07d952307 100644 --- a/src/core/utils/crypto/pkcs12.ts +++ b/src/core/utils/crypto/pkcs12.ts @@ -56,9 +56,9 @@ export const readPkcs12Keystore = (path: string, password?: string): Pkcs12ReadR const p12Der = util.decode64(p12base64); const p12Asn1 = asn1.fromDer(p12Der); const p12 = pkcs12.pkcs12FromAsn1(p12Asn1, password); - const { ca, cert } = getCerts(p12); - const key = getKey(p12); - return { ca, cert, key }; + const keyObj = getKey(p12); + const { ca, cert } = getCerts(p12, keyObj?.publicKeyData); + return { ca, cert, key: keyObj?.key }; }; /** @@ -82,49 +82,87 @@ export const readPkcs12Truststore = (path: string, password?: string): string[] const p12Der = util.decode64(p12base64); const p12Asn1 = asn1.fromDer(p12Der); const p12 = pkcs12.pkcs12FromAsn1(p12Asn1, password); - const { ca } = getCerts(p12, true); + const keyObj = getKey(p12); + const { ca } = getCerts(p12, keyObj?.publicKeyData); return ca; }; -const getCerts = (p12: pkcs12.Pkcs12Pfx, combineBags: boolean = false) => { +// jsbn.BigInteger as described in type definition is wrong, it doesn't include `compareTo` +interface BigInteger { + data: number[]; + t: number; + s: number; + toString(): string; + compareTo(bn: BigInteger): number; +} + +interface PublicKeyData { + n: BigInteger; // modulus + e: BigInteger; // public exponent +} + +const doesPubKeyMatch = (a?: PublicKeyData, b?: PublicKeyData) => { + if (a && b) { + return a.n.compareTo(b.n) === 0 && a.e.compareTo(b.e) === 0; + } + return false; +}; + +const getCerts = (p12: pkcs12.Pkcs12Pfx, pubKey?: PublicKeyData) => { // OID 1.2.840.113549.1.12.10.1.3 (certBag) const bags = getBags(p12, pki.oids.certBag); - let ca: string[] | undefined; - let cert: string | undefined; + let ca; + let cert; if (bags && bags.length) { - if (!combineBags) { - const certBag = bags.shift(); - if (certBag) cert = convertCert(certBag); - } if (bags.length) { - ca = bags.map(convertCert).filter(x => x !== undefined) as string[]; + ca = bags.map(convertCert).filter(x => x !== undefined); } } + if (ca) { + cert = ca.find(x => doesPubKeyMatch(x?.publicKeyData, pubKey))?.cert; + ca = ca.filter(x => !doesPubKeyMatch(x?.publicKeyData, pubKey)).map(x => x!.cert); + } return { ca, cert }; }; -const convertCert = (bag: pkcs12.Bag | undefined) => { - if (bag) { - const cert = bag.cert; - if (cert) { - const pem = pki.certificateToPem(cert); - return reformatPem(pem); - } +export const convertCert = (bag: pkcs12.Bag) => { + const cert = bag.cert; + if (cert) { + const pem = pki.certificateToPem(cert); + const key = cert.publicKey as pki.rsa.PublicKey; + const publicKeyData: PublicKeyData = { + n: key.n as BigInteger, + e: key.e as BigInteger, + }; + return { + cert: pem, + publicKeyData, + }; } return undefined; }; const getKey = (p12: pkcs12.Pkcs12Pfx) => { // OID 1.2.840.113549.1.12.10.1.1 (keyBag) || OID 1.2.840.113549.1.12.10.1.2 (pkcs8ShroudedKeyBag) - const bags = getBags(p12, pki.oids.keyBag) || getBags(p12, pki.oids.pkcs8ShroudedKeyBag); + const bags = [ + ...(getBags(p12, pki.oids.keyBag) || []), + ...(getBags(p12, pki.oids.pkcs8ShroudedKeyBag) || []), + ]; if (bags && bags.length) { if (bags.length > 1) { throw new Error(`Keystore contains multiple private keys.`); } - const key = bags[0].key; + const key = bags[0].key as pki.rsa.PrivateKey; if (key) { const pem = pki.privateKeyToPem(key); - return reformatPem(pem); + const publicKeyData: PublicKeyData = { + n: key.n as BigInteger, + e: key.e as BigInteger, + }; + return { + key: pem, + publicKeyData, + }; } } return undefined; @@ -138,7 +176,3 @@ const getBags = (p12: pkcs12.Pkcs12Pfx, bagType: string) => { } return undefined; }; - -const reformatPem = (pem: string) => { - return pem.replace(/\r\n/g, '\n').trim(); -}; From b05c0049a33303c7ac918c844d7764a1a062dd8a Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 6 Jan 2020 12:19:54 -0500 Subject: [PATCH 16/21] Rebuild @kbn/pm to fix CI errors --- packages/kbn-pm/dist/index.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 7c5937af441a2..a881cfa7933b6 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -4500,6 +4500,14 @@ var certs_1 = __webpack_require__(422); exports.CA_CERT_PATH = certs_1.CA_CERT_PATH; exports.ES_KEY_PATH = certs_1.ES_KEY_PATH; exports.ES_CERT_PATH = certs_1.ES_CERT_PATH; +exports.ES_P12_PATH = certs_1.ES_P12_PATH; +exports.ES_P12_PASSWORD = certs_1.ES_P12_PASSWORD; +exports.ES_EMPTYPASSWORD_P12_PATH = certs_1.ES_EMPTYPASSWORD_P12_PATH; +exports.ES_NOPASSWORD_P12_PATH = certs_1.ES_NOPASSWORD_P12_PATH; +exports.KBN_KEY_PATH = certs_1.KBN_KEY_PATH; +exports.KBN_CERT_PATH = certs_1.KBN_CERT_PATH; +exports.KBN_P12_PATH = certs_1.KBN_P12_PATH; +exports.KBN_P12_PASSWORD = certs_1.KBN_P12_PASSWORD; var run_1 = __webpack_require__(423); exports.run = run_1.run; exports.createFailError = run_1.createFailError; @@ -36986,6 +36994,14 @@ const path_1 = __webpack_require__(16); exports.CA_CERT_PATH = path_1.resolve(__dirname, '../certs/ca.crt'); exports.ES_KEY_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.key'); exports.ES_CERT_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.crt'); +exports.ES_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.p12'); +exports.ES_P12_PASSWORD = 'storepass'; +exports.ES_EMPTYPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_emptypassword.p12'); +exports.ES_NOPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); +exports.KBN_KEY_PATH = path_1.resolve(__dirname, '../certs/kibana.key'); +exports.KBN_CERT_PATH = path_1.resolve(__dirname, '../certs/kibana.crt'); +exports.KBN_P12_PATH = path_1.resolve(__dirname, '../certs/kibana.p12'); +exports.KBN_P12_PASSWORD = 'storepass'; /***/ }), From 015bf17e13fb9806cb7518deb1a26349e81dc386 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 6 Jan 2020 13:11:40 -0500 Subject: [PATCH 17/21] Extract utility method in elasticsearch_config.ts --- .../elasticsearch/elasticsearch_config.ts | 113 ++++++++++-------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 0cbc4dafcfb3b..815005f65c6e7 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -225,57 +225,7 @@ export class ElasticsearchConfig { this.customHeaders = rawConfig.customHeaders; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; - - let key: string | undefined; - let keyPassphrase: string | undefined; - let certificate: string | undefined; - let certificateAuthorities: string[] | undefined; - const addCAs = (ca: string[] | undefined) => { - if (ca && ca.length) { - certificateAuthorities = [...(certificateAuthorities || []), ...ca]; - } - }; - - if (rawConfig.ssl.keystore?.path) { - const keystore = readPkcs12Keystore( - rawConfig.ssl.keystore.path, - rawConfig.ssl.keystore.password - ); - if (!keystore.key && !keystore.cert) { - throw new Error(`Did not find key or certificate in Elasticsearch keystore.`); - } - key = keystore.key; - certificate = keystore.cert; - addCAs(keystore.ca); - } else { - if (rawConfig.ssl.key) { - key = readFile(rawConfig.ssl.key); - keyPassphrase = rawConfig.ssl.keyPassphrase; - } - if (rawConfig.ssl.certificate) { - certificate = readFile(rawConfig.ssl.certificate); - } - } - - if (rawConfig.ssl.truststore?.path) { - const ca = readPkcs12Truststore( - rawConfig.ssl.truststore.path, - rawConfig.ssl.truststore.password - ); - addCAs(ca); - } - - const ca = rawConfig.ssl.certificateAuthorities; - if (ca) { - const parsed: string[] = []; - const paths = Array.isArray(ca) ? ca : [ca]; - if (paths.length > 0) { - for (const path of paths) { - parsed.push(readFile(path)); - } - addCAs(parsed); - } - } + const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig); if (key && !certificate) { log.warn(`Detected a key without a certificate; mutual TLS authentication is disabled.`); @@ -294,6 +244,67 @@ export class ElasticsearchConfig { } } +const readKeyAndCerts = (rawConfig: ElasticsearchConfigType) => { + let key: string | undefined; + let keyPassphrase: string | undefined; + let certificate: string | undefined; + let certificateAuthorities: string[] | undefined; + + const addCAs = (ca: string[] | undefined) => { + if (ca && ca.length) { + certificateAuthorities = [...(certificateAuthorities || []), ...ca]; + } + }; + + if (rawConfig.ssl.keystore?.path) { + const keystore = readPkcs12Keystore( + rawConfig.ssl.keystore.path, + rawConfig.ssl.keystore.password + ); + if (!keystore.key && !keystore.cert) { + throw new Error(`Did not find key or certificate in Elasticsearch keystore.`); + } + key = keystore.key; + certificate = keystore.cert; + addCAs(keystore.ca); + } else { + if (rawConfig.ssl.key) { + key = readFile(rawConfig.ssl.key); + keyPassphrase = rawConfig.ssl.keyPassphrase; + } + if (rawConfig.ssl.certificate) { + certificate = readFile(rawConfig.ssl.certificate); + } + } + + if (rawConfig.ssl.truststore?.path) { + const ca = readPkcs12Truststore( + rawConfig.ssl.truststore.path, + rawConfig.ssl.truststore.password + ); + addCAs(ca); + } + + const ca = rawConfig.ssl.certificateAuthorities; + if (ca) { + const parsed: string[] = []; + const paths = Array.isArray(ca) ? ca : [ca]; + if (paths.length > 0) { + for (const path of paths) { + parsed.push(readFile(path)); + } + addCAs(parsed); + } + } + + return { + key, + keyPassphrase, + certificate, + certificateAuthorities, + }; +}; + const readFile = (file: string) => { return readFileSync(file, 'utf8'); }; From 7857a1b044af9e0b30459ced7fea9aac91bb3964 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 6 Jan 2020 12:28:01 -0500 Subject: [PATCH 18/21] Add extra PKCS12 tests and fix outdated jsdoc --- src/core/utils/crypto/__fixtures__/README.md | 44 +++++++++++ src/core/utils/crypto/__fixtures__/index.ts | 26 +++++++ src/core/utils/crypto/__fixtures__/no_ca.p12 | Bin 0 -> 2431 bytes .../utils/crypto/__fixtures__/no_cert.p12 | Bin 0 -> 2217 bytes src/core/utils/crypto/__fixtures__/no_key.p12 | Bin 0 -> 1939 bytes .../utils/crypto/__fixtures__/two_cas.p12 | Bin 0 -> 4215 bytes .../utils/crypto/__fixtures__/two_keys.p12 | Bin 0 -> 4781 bytes src/core/utils/crypto/pkcs12.test.ts | 71 +++++++++++++++--- src/core/utils/crypto/pkcs12.ts | 9 ++- 9 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 src/core/utils/crypto/__fixtures__/README.md create mode 100644 src/core/utils/crypto/__fixtures__/index.ts create mode 100644 src/core/utils/crypto/__fixtures__/no_ca.p12 create mode 100644 src/core/utils/crypto/__fixtures__/no_cert.p12 create mode 100644 src/core/utils/crypto/__fixtures__/no_key.p12 create mode 100644 src/core/utils/crypto/__fixtures__/two_cas.p12 create mode 100644 src/core/utils/crypto/__fixtures__/two_keys.p12 diff --git a/src/core/utils/crypto/__fixtures__/README.md b/src/core/utils/crypto/__fixtures__/README.md new file mode 100644 index 0000000000000..2652b16c420f0 --- /dev/null +++ b/src/core/utils/crypto/__fixtures__/README.md @@ -0,0 +1,44 @@ +# PKCS12 Test Fixtures + +These PKCS12 files are used to test different scenarios. Each has an empty password. + +Including `-noiter` uses a single encryption iteration, and `-nomaciter` uses a single MAC verification iteration. +This makes each P12 keystore much quicker to parse. + +Commands to generate files: + +```shell +# Generate a PKCS12 file with an EE cert and CA cert, but no EE key +cat elasticsearch.crt ca.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -nokeys -out no_key.p12 + +# Generate a PKCS12 file with an EE key and EE cert, but no CA cert +cat elasticsearch.key elasticsearch.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -out no_ca.p12 + +# Generate a PKCS12 file with an EE key, EE cert, and two CA certs +cat elasticsearch.key elasticsearch.crt ca.crt ca.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -out two_cas.p12 + +# Generate a PKCS12 file with two EE keys and EE certs +cat elasticsearch.key elasticsearch.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -out two_keys.p12 +cat kibana.key kibana.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -name 2 -out tmp.p12 +keytool -importkeystore -srckeystore tmp.p12 -srcstorepass '' -destkeystore two_keys.p12 -deststorepass '' -deststoretype PKCS12 +rm tmp.p12 +``` + +No commonly available tools seem to be able to generate a PKCS12 file with a key and no certificate, so we use node-forge to do that: + +```js +const utils = require('@kbn/dev-utils'); +const forge = require('node-forge'); +const fs = require('fs'); + +const pemCA = fs.readFileSync(utils.CA_CERT_PATH, 'utf8'); +const pemKey = fs.readFileSync(utils.ES_KEY_PATH, 'utf8'); +const privateKey = forge.pki.privateKeyFromPem(pemKey); + +const p12Asn = forge.pkcs12.toPkcs12Asn1(privateKey, pemCA, null, { + useMac: false, + generateLocalKeyId: false, +}); +const p12Der = forge.asn1.toDer(p12Asn).getBytes(); +fs.writeFileSync('no_cert.p12', p12Der, { encoding: 'binary' }); +``` diff --git a/src/core/utils/crypto/__fixtures__/index.ts b/src/core/utils/crypto/__fixtures__/index.ts new file mode 100644 index 0000000000000..12481963ff4f6 --- /dev/null +++ b/src/core/utils/crypto/__fixtures__/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; + +export const NO_CA_PATH = resolve(__dirname, './no_ca.p12'); +export const NO_CERT_PATH = resolve(__dirname, './no_cert.p12'); +export const NO_KEY_PATH = resolve(__dirname, './no_key.p12'); +export const TWO_CAS_PATH = resolve(__dirname, './two_cas.p12'); +export const TWO_KEYS_PATH = resolve(__dirname, './two_keys.p12'); diff --git a/src/core/utils/crypto/__fixtures__/no_ca.p12 b/src/core/utils/crypto/__fixtures__/no_ca.p12 new file mode 100644 index 0000000000000000000000000000000000000000..1e6df9a0f71c54158e7dbf3b6cad2a062ae3a9d0 GIT binary patch literal 2431 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 B>q`It literal 0 HcmV?d00001 diff --git a/src/core/utils/crypto/pkcs12.test.ts b/src/core/utils/crypto/pkcs12.test.ts index e162db158f7ea..c6c28697c4bcc 100644 --- a/src/core/utils/crypto/pkcs12.test.ts +++ b/src/core/utils/crypto/pkcs12.test.ts @@ -26,6 +26,7 @@ import { ES_EMPTYPASSWORD_P12_PATH, ES_NOPASSWORD_P12_PATH, } from '@kbn/dev-utils'; +import { NO_CA_PATH, NO_CERT_PATH, NO_KEY_PATH, TWO_CAS_PATH, TWO_KEYS_PATH } from './__fixtures__'; import { readFileSync } from 'fs'; import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from '.'; @@ -42,23 +43,30 @@ const readPem = (file: string) => { return reformatPem(pem); }; +let pemCA: string; +let pemCert: string; +let pemKey: string; + +beforeAll(() => { + pemCA = readPem(CA_CERT_PATH); + pemCert = readPem(ES_CERT_PATH); + pemKey = readPem(ES_KEY_PATH); +}); + describe('#readPkcs12Keystore', () => { const expectKey = (pkcs12ReadResult: Pkcs12ReadResult) => { const result = reformatPem(pkcs12ReadResult.key!); - const pemKey = readPem(ES_KEY_PATH); expect(result).toEqual(pemKey); }; const expectCert = (pkcs12ReadResult: Pkcs12ReadResult) => { const result = reformatPem(pkcs12ReadResult.cert!); - const pemCert = readPem(ES_CERT_PATH); expect(result).toEqual(pemCert); }; - const expectCA = (pkcs12ReadResult: Pkcs12ReadResult) => { + const expectCA = (pkcs12ReadResult: Pkcs12ReadResult, ca = [pemCA]) => { const result = pkcs12ReadResult.ca?.map(x => reformatPem(x)); - const pemCA = readPem(CA_CERT_PATH); - expect(result).toEqual([pemCA]); + expect(result).toEqual(ca); }; describe('Succeeds when the correct password is used', () => { @@ -124,24 +132,67 @@ describe('#readPkcs12Keystore', () => { }); }); - describe('Throws when an invalid password is used', () => { + describe('Handles other key store permutations', () => { + it('Succeeds with no key', () => { + const pkcs12ReadResult = readPkcs12Keystore(NO_KEY_PATH, ''); + expectCA(pkcs12ReadResult, [pemCert, pemCA]); + expect(pkcs12ReadResult.cert).toBeUndefined(); + expect(pkcs12ReadResult.key).toBeUndefined(); + }); + + it('Succeeds with no instance certificate', () => { + const pkcs12ReadResult = readPkcs12Keystore(NO_CERT_PATH, ''); + expectCA(pkcs12ReadResult); + expect(pkcs12ReadResult.cert).toBeUndefined(); + expectKey(pkcs12ReadResult); + }); + + it('Succeeds with no CA certificate', () => { + const pkcs12ReadResult = readPkcs12Keystore(NO_CA_PATH, ''); + expect(pkcs12ReadResult.ca).toBeUndefined(); + expectCert(pkcs12ReadResult); + expectKey(pkcs12ReadResult); + }); + + it('Succeeds with two CA certificates', () => { + const pkcs12ReadResult = readPkcs12Keystore(TWO_CAS_PATH, ''); + expectCA(pkcs12ReadResult, [pemCA, pemCA]); + expectCert(pkcs12ReadResult); + expectKey(pkcs12ReadResult); + }); + }); + + describe('Throws errors', () => { const expectError = (password?: string) => { expect(() => readPkcs12Keystore(ES_P12_PATH, password)).toThrowError( 'PKCS#12 MAC could not be verified. Invalid password?' ); }; - it('Incorrect password', () => { + it('When an invalid password is used (incorrect)', () => { expectError('incorrect'); }); - it('Empty password', () => { + it('When an invalid password is used (empty)', () => { expectError(''); }); - it('Undefined password', () => { + it('When an invalid password is used (undefined)', () => { expectError(); }); + + it('When an invalid file path is used', () => { + const path = 'invalid-filepath'; + expect(() => readPkcs12Keystore(path)).toThrowError( + `ENOENT: no such file or directory, open '${path}'` + ); + }); + + it('When two keys are present', () => { + expect(() => readPkcs12Keystore(TWO_KEYS_PATH, '')).toThrowError( + 'Keystore contains multiple private keys.' + ); + }); }); }); @@ -149,8 +200,6 @@ describe('#readPkcs12Truststore', () => { it('reads all certificates into one CA array and discards any certificates that have keys', () => { const ca = readPkcs12Truststore(ES_P12_PATH, ES_P12_PASSWORD); const result = ca?.map(x => reformatPem(x)); - const pemCA = readPem(CA_CERT_PATH); - expect(result).toEqual([pemCA]); }); }); diff --git a/src/core/utils/crypto/pkcs12.ts b/src/core/utils/crypto/pkcs12.ts index b13a07d952307..5eae044b19584 100644 --- a/src/core/utils/crypto/pkcs12.ts +++ b/src/core/utils/crypto/pkcs12.ts @@ -32,8 +32,9 @@ export interface Pkcs12ReadResult { * @remarks * The PKCS12 key store may contain the following: * - 0 or more certificates contained in a `certBag` (OID - * 1.2.840.113549.1.12.10.1.3); the first is treated as an instance - * certificate, and any others are treated as CA certificates + * 1.2.840.113549.1.12.10.1.3); if a certificate has an associated + * private key it is treated as an instance certificate, otherwise it is + * treated as a CA certificate * - 0 or 1 private keys contained in a `keyBag` (OID * 1.2.840.113549.1.12.10.1.1) or a `pkcs8ShroudedKeyBag` (OID * 1.2.840.113549.1.12.10.1.2) @@ -121,7 +122,11 @@ const getCerts = (p12: pkcs12.Pkcs12Pfx, pubKey?: PublicKeyData) => { if (ca) { cert = ca.find(x => doesPubKeyMatch(x?.publicKeyData, pubKey))?.cert; ca = ca.filter(x => !doesPubKeyMatch(x?.publicKeyData, pubKey)).map(x => x!.cert); + if (ca.length === 0) { + ca = undefined; + } } + return { ca, cert }; }; From 3330bdcb5dc88aacff6eef79e2da5a803085ebd6 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 8 Jan 2020 18:00:16 -0500 Subject: [PATCH 19/21] Address final PR review feedback Also simplified README regarding cert generation --- packages/kbn-dev-utils/certs/README.md | 14 ++++---------- src/core/utils/crypto/pkcs12.ts | 10 +++------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/kbn-dev-utils/certs/README.md b/packages/kbn-dev-utils/certs/README.md index 49762c6d61aa5..fdf7892789404 100644 --- a/packages/kbn-dev-utils/certs/README.md +++ b/packages/kbn-dev-utils/certs/README.md @@ -48,20 +48,14 @@ openssl pkcs12 -in elasticsearch.p12 -nodes -passin pass:"storepass" -passout pa # Extract the PEM-formatted X.509 certificate for the CA openssl pkcs12 -in elasticsearch.p12 -out ca.crt -cacerts -passin pass:"storepass" -passout pass: -# Extract the PEM-formatted PKCS #8 encrypted private key for Elasticsearch -openssl pkcs12 -in elasticsearch.p12 -out elasticsearch-encrypted.key -nocerts -passin pass:"storepass" -passout pass:"keypass" - -# Decrypt the private key for Elasticsearch and convert it to a PEM-formatted PKCS #1 private key -openssl rsa -in elasticsearch-encrypted.key -out elasticsearch.key -passin pass:keypass +# Extract the PEM-formatted PKCS #1 private key for Elasticsearch +openssl pkcs12 -in elasticsearch.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:keypass -out elasticsearch.key # Extract the PEM-formatted X.509 certificate for Elasticsearch openssl pkcs12 -in elasticsearch.p12 -out elasticsearch.crt -clcerts -passin pass:"storepass" -passout pass: -# Extract the PEM-formatted PKCS #8 encrypted private key for Kibana -openssl pkcs12 -in kibana.p12 -out kibana-encrypted.key -nocerts -passin pass:"storepass" -passout pass:"keypass" - -# Decrypt the private key for Kibana and convert it to a PEM-formatted PKCS #1 private key -openssl rsa -in kibana-encrypted.key -out kibana.key -passin pass:keypass +# Extract the PEM-formatted PKCS #1 private key for Kibana +openssl pkcs12 -in kibana.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:keypass -out kibana.key # Extract the PEM-formatted X.509 certificate for Kibana openssl pkcs12 -in kibana.p12 -out kibana.crt -clcerts -passin pass:"storepass" -passout pass: diff --git a/src/core/utils/crypto/pkcs12.ts b/src/core/utils/crypto/pkcs12.ts index 5eae044b19584..74f22bb685c2f 100644 --- a/src/core/utils/crypto/pkcs12.ts +++ b/src/core/utils/crypto/pkcs12.ts @@ -115,13 +115,9 @@ const getCerts = (p12: pkcs12.Pkcs12Pfx, pubKey?: PublicKeyData) => { let ca; let cert; if (bags && bags.length) { - if (bags.length) { - ca = bags.map(convertCert).filter(x => x !== undefined); - } - } - if (ca) { - cert = ca.find(x => doesPubKeyMatch(x?.publicKeyData, pubKey))?.cert; - ca = ca.filter(x => !doesPubKeyMatch(x?.publicKeyData, pubKey)).map(x => x!.cert); + const certs = bags.map(convertCert).filter(x => x !== undefined); + cert = certs.find(x => doesPubKeyMatch(x!.publicKeyData, pubKey))?.cert; + ca = certs.filter(x => !doesPubKeyMatch(x!.publicKeyData, pubKey)).map(x => x!.cert); if (ca.length === 0) { ca = undefined; } From e306b7200496320d30f40e09be867b342a05d6c2 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 9 Jan 2020 10:51:51 -0500 Subject: [PATCH 20/21] Integration test for Kibana with intermediate CA --- .../server_integration/__fixtures__/README.md | 79 ++++++++++++++++++ test/server_integration/__fixtures__/index.ts | 25 ++++++ .../__fixtures__/localhost.p12 | Bin 0 -> 4184 bytes .../__fixtures__/test_intermediate_ca.crt | 79 ++++++++++++++++++ .../__fixtures__/test_root_ca.crt | 24 ++++++ .../http/ssl_with_p12/index.js | 2 +- .../http/ssl_with_p12_intermediate/config.js | 56 +++++++++++++ .../http/ssl_with_p12_intermediate/index.js | 28 +++++++ 8 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 test/server_integration/__fixtures__/README.md create mode 100644 test/server_integration/__fixtures__/index.ts create mode 100644 test/server_integration/__fixtures__/localhost.p12 create mode 100644 test/server_integration/__fixtures__/test_intermediate_ca.crt create mode 100644 test/server_integration/__fixtures__/test_root_ca.crt create mode 100644 test/server_integration/http/ssl_with_p12_intermediate/config.js create mode 100644 test/server_integration/http/ssl_with_p12_intermediate/index.js diff --git a/test/server_integration/__fixtures__/README.md b/test/server_integration/__fixtures__/README.md new file mode 100644 index 0000000000000..faf881202e55e --- /dev/null +++ b/test/server_integration/__fixtures__/README.md @@ -0,0 +1,79 @@ +# HTTP SSL Test Fixtures + +These PKCS12 files are used to test SSL with a root CA and an intermediate CA. + +The files that are provided by `@kbn/dev-utils` only use a root CA, so we need additional test files for this. + +To generate these additional test files, see the steps below. + +## Step 1. Set environment variables + +```sh +CA1='test_root_ca' +CA2='test_intermediate_ca' +EE='localhost' +``` + +## Step 2. Generate PKCS12 key stores + +Using [elasticsearch-certutil](https://www.elastic.co/guide/en/elasticsearch/reference/current/certutil.html): + +```sh +bin/elasticsearch-certutil ca --ca-dn "CN=Test Root CA" -days 18250 --out $CA1.p12 --pass castorepass +bin/elasticsearch-certutil ca --ca-dn "CN=Test Intermediate CA" -days 18250 --out $CA2.p12 --pass castorepass +bin/elasticsearch-certutil cert --ca $CA2.p12 --ca-pass castorepass --name $EE --dns $EE --out $EE.p12 --pass storepass +``` + +## Step 3. Convert PKCS12 key stores + +Using OpenSSL on macOS: + +```sh +### CONVERT P12 KEYSTORES TO PEM FILES +openssl pkcs12 -in $CA1.p12 -out $CA1.crt -nokeys -passin pass:"castorepass" -passout pass: +openssl pkcs12 -in $CA1.p12 -nocerts -passin pass:"castorepass" -passout pass:"keypass" | openssl rsa -passin pass:"keypass" -out $CA1.key + +openssl pkcs12 -in $CA2.p12 -out $CA2.crt -nokeys -passin pass:"castorepass" -passout pass: +openssl pkcs12 -in $CA2.p12 -nocerts -passin pass:"castorepass" -passout pass:"keypass" | openssl rsa -passin pass:"keypass" -out $CA2.key + +openssl pkcs12 -in $EE.p12 -out $EE.crt -clcerts -passin pass:"storepass" -passout pass: +openssl pkcs12 -in $EE.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:"keypass" -out $EE.key + +### RE-SIGN INTERMEDIATE CA CERT +mkdir -p ./tmp +openssl x509 -x509toreq -in $CA2.crt -signkey $CA2.key -out ./tmp/$CA2.csr +dd if=/dev/urandom of=./tmp/rand bs=256 count=1 +touch ./tmp/index.txt +echo "01" > ./tmp/serial +cp /System/Library/OpenSSL/openssl.cnf ./tmp/ +echo " +[ tmpcnf ] +dir = ./ +certs = ./ +new_certs_dir = ./tmp +crl_dir = ./tmp/crl +database = ./tmp/index.txt +unique_subject = no +certificate = ./$CA1.crt +serial = ./tmp/serial +crlnumber = ./tmp/crlnumber +crl = ./tmp/crl.pem +private_key = ./$CA1.key +RANDFILE = ./tmp/rand +x509_extensions = v3_ca +name_opt = ca_default +cert_opt = ca_default +default_days = 18250 +default_crl_days= 30 +default_md = sha256 +preserve = no +policy = policy_anything +" >> ./tmp/openssl.cnf + +# The next command requires user input +openssl ca -config ./tmp/openssl.cnf -name tmpcnf -in ./tmp/$CA2.csr -out $CA2.crt -verbose + +### CONVERT PEM FILES BACK TO P12 KEYSTORES +cat $CA2.key $CA2.crt $CA1.crt | openssl pkcs12 -export -name $CA2 -passout pass:"castorepass" -out $CA2.p12 +cat $EE.key $EE.crt $CA1.crt $CA2.crt | openssl pkcs12 -export -name $EE -passout pass:"storepass" -out $EE.p12 +``` diff --git a/test/server_integration/__fixtures__/index.ts b/test/server_integration/__fixtures__/index.ts new file mode 100644 index 0000000000000..40f1ddb7fa0ba --- /dev/null +++ b/test/server_integration/__fixtures__/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; + +export const CA1_CERT_PATH = resolve(__dirname, './test_root_ca.crt'); +export const CA2_CERT_PATH = resolve(__dirname, './test_intermediate_ca.crt'); +export const EE_P12_PATH = resolve(__dirname, './localhost.p12'); +export const EE_P12_PASSWORD = 'storepass'; diff --git a/test/server_integration/__fixtures__/localhost.p12 b/test/server_integration/__fixtures__/localhost.p12 new file mode 100644 index 0000000000000000000000000000000000000000..1b0d11fb880980d2f8ce3c919afe3319cab74360 GIT binary patch literal 4184 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 { - it('handles requests using ssl', async () => { + it('handles requests using ssl with a P12 keystore', async () => { await supertest.get('/').expect(302); }); }); diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js new file mode 100644 index 0000000000000..73a77425ec774 --- /dev/null +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readFileSync } from 'fs'; +import { CA1_CERT_PATH, CA2_CERT_PATH, EE_P12_PATH, EE_P12_PASSWORD } from '../../__fixtures__'; +import { createKibanaSupertestProvider } from '../../services'; + +export default async function({ readConfigFile }) { + const httpConfig = await readConfigFile(require.resolve('../../config')); + + return { + testFiles: [require.resolve('./')], + services: { + ...httpConfig.get('services'), + supertest: createKibanaSupertestProvider({ + certificateAuthorities: [readFileSync(CA1_CERT_PATH), readFileSync(CA2_CERT_PATH)], + }), + }, + servers: { + ...httpConfig.get('servers'), + kibana: { + ...httpConfig.get('servers.kibana'), + protocol: 'https', + }, + }, + junit: { + reportName: 'Http SSL Integration Tests', + }, + esTestCluster: httpConfig.get('esTestCluster'), + kbnTestServer: { + ...httpConfig.get('kbnTestServer'), + serverArgs: [ + ...httpConfig.get('kbnTestServer.serverArgs'), + '--server.ssl.enabled=true', + `--server.ssl.keystore.path=${EE_P12_PATH}`, + `--server.ssl.keystore.password=${EE_P12_PASSWORD}`, + ], + }, + }; +} diff --git a/test/server_integration/http/ssl_with_p12_intermediate/index.js b/test/server_integration/http/ssl_with_p12_intermediate/index.js new file mode 100644 index 0000000000000..fb079a4e091c3 --- /dev/null +++ b/test/server_integration/http/ssl_with_p12_intermediate/index.js @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('kibana server with ssl', () => { + it('handles requests using ssl with a P12 keystore that uses an intermediate CA', async () => { + await supertest.get('/').expect(302); + }); + }); +} From 835a3fdcf1fd4a3b7879f21b49a240df6177665e Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 9 Jan 2020 11:19:38 -0500 Subject: [PATCH 21/21] Fix CI (renovate) --- renovate.json5 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index f970874890740..378e4065230ff 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -115,14 +115,6 @@ '@types/base64-js', ], }, - { - groupSlug: 'node-forge', - groupName: 'node-forge related packages', - packageNames: [ - 'node-forge', - '@types/node-forge', - ], - }, { groupSlug: 'bluebird', groupName: 'bluebird related packages', @@ -633,6 +625,14 @@ '@types/node-fetch', ], }, + { + groupSlug: 'node-forge', + groupName: 'node-forge related packages', + packageNames: [ + 'node-forge', + '@types/node-forge', + ], + }, { groupSlug: 'nodemailer', groupName: 'nodemailer related packages',