From 02034c1c84f221cbc3460453fbf134960c7017c4 Mon Sep 17 00:00:00 2001 From: Jesse Schmidt Date: Tue, 25 Jun 2024 10:35:12 -0700 Subject: [PATCH] add mTLS tests --- cmd/utils.go | 21 ++++- docker/mtls/config/aerospike-proximus.yml | 87 ++++++++++++++++++ docker/mtls/config/aerospike.conf | 82 +++++++++++++++++ docker/mtls/config/tls/ca.aerospike.com.crt | 24 +++++ .../tls/ca.aerospike.com.truststore.jks | Bin 0 -> 1099 bytes docker/mtls/config/tls/keypass | 1 + docker/mtls/config/tls/localhost.crt | 21 +++++ docker/mtls/config/tls/localhost.key | 27 ++++++ docker/mtls/config/tls/localhost.keystore.jks | Bin 0 -> 2581 bytes docker/mtls/config/tls/storepass | 1 + docker/mtls/docker-compose.yml | 23 +++++ e2e_test.go | 53 +++++++++-- 12 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 docker/mtls/config/aerospike-proximus.yml create mode 100644 docker/mtls/config/aerospike.conf create mode 100644 docker/mtls/config/tls/ca.aerospike.com.crt create mode 100644 docker/mtls/config/tls/ca.aerospike.com.truststore.jks create mode 100644 docker/mtls/config/tls/keypass create mode 100644 docker/mtls/config/tls/localhost.crt create mode 100644 docker/mtls/config/tls/localhost.key create mode 100644 docker/mtls/config/tls/localhost.keystore.jks create mode 100644 docker/mtls/config/tls/storepass create mode 100644 docker/mtls/docker-compose.yml diff --git a/cmd/utils.go b/cmd/utils.go index add5701..0160f9f 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -8,8 +8,11 @@ import ( "encoding/pem" "fmt" "log/slog" + "os" "time" + "golang.org/x/term" + avs "github.com/aerospike/avs-client-go" ) @@ -26,9 +29,21 @@ func createClientFromFlags(clientFlags *flags.ClientFlags, connectTimeout time.D } var password *string - if len(clientFlags.Password) != 0 { - strPass := clientFlags.Password.String() - password = &strPass + if clientFlags.User.Val != nil { + if len(clientFlags.Password) != 0 { + strPass := clientFlags.Password.String() + password = &strPass + } else { + fmt.Print("Enter Password: ") + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + logger.Error("failed to read password", slog.Any("error", err)) + return nil, err + } + fmt.Println() // Print a newline after the password input + strPass := string(bytePassword) + password = &strPass + } } adminClient, err := avs.NewAdminClient( diff --git a/docker/mtls/config/aerospike-proximus.yml b/docker/mtls/config/aerospike-proximus.yml new file mode 100644 index 0000000..60473cf --- /dev/null +++ b/docker/mtls/config/aerospike-proximus.yml @@ -0,0 +1,87 @@ +# Change the configuration for your use case. +cluster: + # Custom node-id. It will be auto-generated if not specified. + # node-id: a1 + + # Unique identifier for this cluster. + cluster-name: prism-image-search + +tls: + service-tls: + mutual-auth: true + trust-store: + store-file: /etc/aerospike-proximus/tls/ca.aerospike.com.truststore.jks + store-password-file: /etc/aerospike-proximus/tls/storepass + key-store: + store-file: /etc/aerospike-proximus/tls/localhost.keystore.jks + store-password-file: /etc/aerospike-proximus/tls/storepass + key-password-file: /etc/aerospike-proximus/tls/keypass + +# The Proximus service listening ports, TLS and network interface. +service: + ports: + 10000: + # If TLS needs to be enabled, tls configuration id. + tls-id: service-tls + advertised-listeners: + default: + address: 127.0.0.1 + port: 10000 + +# Management API listening ports, TLS and network interface. +manage: + ports: + 5040: + tls-id: service-tls + +# Intra cluster interconnect listening ports, TLS and network interface. +interconnect: + ports: + 5001: {} + +#heartbeat: +# seeds: +# - address: localhost +# port: 6001 + +# Target Aerospike cluster +aerospike: + seeds: + - aerospike: + port: 3000 + +# File based credentials store only if security should be enabled. +#security: +# credentials-store: +# type: file +# credentials-file: samples/credentials.yml +# auth-token: +# private-key: samples/auth/private_key.pem +# public-key: samples/auth/public_key.pem + +# Vault based credentials store only if security should be enabled. +#security: +# credentials-store: +# type: vault +# url: https://vault:8200 +# secrets-path: /secret/aerospike/aerodb1 +# tls: +# key-store: +# store-type: PEM +# store-file: key.pem +# store-password-file: keypass.txt # Password protecting key.pem. +# certificate-chain-files: certchain.pem +# trust-store: +# store-type: PEM +# certificate-files: cacert.pem +# auth-token: +# private-key: samples/auth/private_key.pem +# public-key: samples/auth/public_key.pem + +# The logging properties. +logging: + #format: json + #file: /var/log/aerospike-proximus/aerospike-proximus.log + enable-console-logging: true + levels: + metrics-ticker: off diff --git a/docker/mtls/config/aerospike.conf b/docker/mtls/config/aerospike.conf new file mode 100644 index 0000000..a23c052 --- /dev/null +++ b/docker/mtls/config/aerospike.conf @@ -0,0 +1,82 @@ +# Aerospike database configuration file for use with systemd. + +service { + cluster-name prism-demo + proto-fd-max 15000 +} + + +logging { + file /var/log/aerospike/aerospike.log { + context any info + } + + # Send log messages to stdout + console { + context any info + context query critical + } +} + +network { + service { + address any + port 3000 + } + + heartbeat { + mode multicast + multicast-group 239.1.99.222 + port 9918 + + # To use unicast-mesh heartbeats, remove the 3 lines above, and see + # aerospike_mesh.conf for alternative. + + interval 150 + timeout 10 + } + + fabric { + port 3001 + } + + info { + port 3003 + } +} + +namespace test { + replication-factor 1 + nsup-period 60 + + storage-engine memory { + data-size 1G + } +} + +namespace bar { + replication-factor 1 + nsup-period 60 + + storage-engine memory { + data-size 1G + } +} + +namespace proximus-meta { + replication-factor 1 + nsup-period 100 + + storage-engine memory { + data-size 1G + } + + # To use file storage backing, comment out the line above and use the + # following lines instead. +# storage-engine device { +# file /opt/aerospike/data/bar.dat +# filesize 16G +# data-in-memory true # Store data in memory in addition to file. +# } +} + diff --git a/docker/mtls/config/tls/ca.aerospike.com.crt b/docker/mtls/config/tls/ca.aerospike.com.crt new file mode 100644 index 0000000..522410a --- /dev/null +++ b/docker/mtls/config/tls/ca.aerospike.com.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIJALiEh0EwIowCMA0GCSqGSIb3DQEBCwUAMIGLMQswCQYD +VQQGEwJJTjELMAkGA1UECAwCS0ExCzAJBgNVBAcMAkJOMRIwEAYDVQQKDAlhZXJv +c3Bpa2UxEjAQBgNVBAsMCWVjb3N5c3RlbTEZMBcGA1UEAwwQY2EuYWVyb3NwaWtl +LmNvbTEfMB0GCSqGSIb3DQEJARYQY2FAYWVyb3NwaWtlLmNvbTAeFw0xOTA3MDgw +OTA2NTZaFw0zOTA3MDMwOTA2NTZaMIGLMQswCQYDVQQGEwJJTjELMAkGA1UECAwC +S0ExCzAJBgNVBAcMAkJOMRIwEAYDVQQKDAlhZXJvc3Bpa2UxEjAQBgNVBAsMCWVj +b3N5c3RlbTEZMBcGA1UEAwwQY2EuYWVyb3NwaWtlLmNvbTEfMB0GCSqGSIb3DQEJ +ARYQY2FAYWVyb3NwaWtlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAOkpqtQIiWhUouKqsyoHs7+mXL4fahOKZBG1asUaNpY/rhR500OpczWfHXK1 ++W5WA2yizhIFFpgzNJOtXW2Sai0Dqk5MO7hPLoKzlA/pSYnVFyM34kECWiqFo9PZ +6FQxOBfRJjE4sLuLJGh+Pr/bii8Cb8GwuckUGFQaJLv9VZXcsGkpvyOEy9CsRI7o +7wPn0VVNxj6bbag0AUCe5s1ZKsNxFh3Ekqbx+pFA/7KxxVSmOCVhX+W87K4qJz0+ +XfGtBJjM4YqGun3ul4JCzcPHoqA5vKAB00LHa7bNY/5Zf2iGcfPLEmyk09PyqAQF +KTj8D4OTiP85L+wo7jxY6U/Xz1cCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFEz8sA/FRlupRxPfrDrnFZwN4g+HMB8G +A1UdIwQYMBaAFEz8sA/FRlupRxPfrDrnFZwN4g+HMA0GCSqGSIb3DQEBCwUAA4IB +AQDTDCjMVX11S9GZGNExnqtGVzOEAlKyWnx0g1pULTffN3vc8DG1gynY6n7/9vPI +V2pheuyd/aKoR0Ig8CvjOnj90DcMePwh3Zk6eG7SlUK41x4yrkw04VEqvyw02Dw7 +SPZRgEs5/AHVLscOaDeJxW6Nzm5XYS5mfhto5nZCBEq5u5FfsktwYisIlK9JLbYE +ATjQbkwoNeg2Ubdtddn9HgnCEV0ht0VE2bZc0OUmv29R5XTNEIEIf/bXjdgnbv57 +IhJElLyHziPbD08JgkqqQw+6zAbxO9OLury69eUQoC0nynVX+Ub9GoXSuQ2lGyyq +ouSZQ0M5aZVQvsqUyREGBsAF +-----END CERTIFICATE----- diff --git a/docker/mtls/config/tls/ca.aerospike.com.truststore.jks b/docker/mtls/config/tls/ca.aerospike.com.truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..8d72ac67e8199cf74e1aecd70587ac3acac082c6 GIT binary patch literal 1099 zcmezO_TO6u1_mY|W(3m$$%%T2sYUt41)15Yddc~@K#A;&+U|V}tPy&q29^vAEKCMX z%>N9Un4T?QW@2Pw;$+y-((Y)W)Wc-J%f_kI=F#?@mywa1mBFB~+mPFUlZ`o)g-w{r z(+|es;9>H1gmKt;n4J6!g$x8h%D8wq;daBsxOq5JlkKv*?@q=-a_l*;bEp;PZ{$kg1g+;aND z8kfEo@0p)p4D~%`H#>KQ38TZjXJ;d|4i}2a9+|Z4>m9zMQkf#seBjF+8`XKy>3{4cUTqpk4sX`!4YmoI-> z!NRI(@rS>8a>sv5{Wlu#Y$9IzUq2tt#LURRxH#D$(SRSAB4mXb8UM4e8ZZMX13r)d zKS+QDm_XZr=>o{-V-aH!@%giX|EOE^N_XM=YpkA&&f$H;-)ZCKCl{v9^?c6;vev@}Wk~YVb^`5%hSQsrX zzg!C1xZt}ZvX#awu*wm(1cX~|iEMvnS#*L!cM=l!cz5^|Zc zr~RDrZGL~wCa+b_{JYMueYC#Zy=%{|uTKRQ=&GM84gcx(SE}{WPTr-`I;$2vnd$6o mnK?CJ->E4l1=-jRu(E48Ts#u5Eo{?tLq7M!rIn^GL5~3;Cxf8? literal 0 HcmV?d00001 diff --git a/docker/mtls/config/tls/keypass b/docker/mtls/config/tls/keypass new file mode 100644 index 0000000..0f673cc --- /dev/null +++ b/docker/mtls/config/tls/keypass @@ -0,0 +1 @@ +citrusstore \ No newline at end of file diff --git a/docker/mtls/config/tls/localhost.crt b/docker/mtls/config/tls/localhost.crt new file mode 100644 index 0000000..e713708 --- /dev/null +++ b/docker/mtls/config/tls/localhost.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhzCCAm+gAwIBAgIUES99at1e+OgIcnfEwS6+uBLdLlkwDQYJKoZIhvcNAQEL +BQAwgYsxCzAJBgNVBAYTAklOMQswCQYDVQQIDAJLQTELMAkGA1UEBwwCQk4xEjAQ +BgNVBAoMCWFlcm9zcGlrZTESMBAGA1UECwwJZWNvc3lzdGVtMRkwFwYDVQQDDBBj +YS5hZXJvc3Bpa2UuY29tMR8wHQYJKoZIhvcNAQkBFhBjYUBhZXJvc3Bpa2UuY29t +MB4XDTE5MDcxNDE3NTI1M1oXDTI5MDcxMTE3NTI1M1owSDELMAkGA1UEBhMCSU4x +CzAJBgNVBAgMAktBMRgwFgYDVQQKDA9BZXJvc3Bpa2UsIEluYy4xEjAQBgNVBAMM +CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOOLzx9 +a8XnBLrTQn2RBJfUUi7qZZTBAEraQ0Tsh04wqEycwxsh4HfpqP4QDF8JpAs5mvMj +fotnamVCNncXJgvOIaPQVRoDdGAAfCcOvT9B0Y7xBH3jj+e9YBiOEfee7cb7mjnX +2XYa9UGepNIktHUR0RgsuNooxDcT8wIbt+QMK0ZqF5GBQtgX24646ow5VOzfAGZy +SZfc9J0mWIZwHc0Gkb+V15iJBzdwJ/15FrmHSiJqGIsesqThrdAh5SYZCG/1+RL8 +FuwtZuoKm3V4OoQzQ0TIz6fCaiLI1PgrKJnMiGa3PIPX5DyPEU+5opXJCClGW2iM +pGmDQ5W+GmZwickCAwEAAaMlMCMwIQYDVR0RBBowGIIJbG9jYWxob3N0ggsqLmxv +Y2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAdVCQWrRC3bCI6nQ1GuEfbEa7Fn2x +88EvMoJb7PLCLnumDd68AJ38qHUrmvrTbFFJemrCJ2cI0vGQPf5bMm+L2KyxVD9D +zSyy/mbKmAvJl05gv+Si0N9Ikhnqyj9NuF2hnMVJp7IowCBWU/JTfmXJFJU+bJDr +NntdjuUGHYq+zlTPowGS1p1Bx5QC+6YpelAcYiV7JiUXPQXd0SpJMcp0iYhJLjHv +qoM/jPl0gIHrn8WMrIKmf81Uv3mNd2nkKhiNuqqrHs5tVarYHfY2IJVnf8nMKlh9 +LVUXkze20oB2uc6IKxrb7nbXk0Q2XKb9xmpaFb+Fut3musY5sYrE9l+bkw== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/docker/mtls/config/tls/localhost.key b/docker/mtls/config/tls/localhost.key new file mode 100644 index 0000000..23b929e --- /dev/null +++ b/docker/mtls/config/tls/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA044vPH1rxecEutNCfZEEl9RSLupllMEAStpDROyHTjCoTJzD +GyHgd+mo/hAMXwmkCzma8yN+i2dqZUI2dxcmC84ho9BVGgN0YAB8Jw69P0HRjvEE +feOP571gGI4R957txvuaOdfZdhr1QZ6k0iS0dRHRGCy42ijENxPzAhu35AwrRmoX +kYFC2BfbjrjqjDlU7N8AZnJJl9z0nSZYhnAdzQaRv5XXmIkHN3An/XkWuYdKImoY +ix6ypOGt0CHlJhkIb/X5EvwW7C1m6gqbdXg6hDNDRMjPp8JqIsjU+CsomcyIZrc8 +g9fkPI8RT7milckIKUZbaIykaYNDlb4aZnCJyQIDAQABAoIBAQCntYX43Cy93KBB +Qwzo4jfT7TuheaxBuqbysAi38RJqh+RDp9p7/eUm6pNPpYVJKiljxKzzpuXAuaD8 +2Pq4eh9tKGI+rP9p+ecd3ASQKf0Y0qLAQI0hB2+jdNtjW+0ecl1pazgeNuFr4X8g +IBXlibeNPyyVj46TU9IJH8V7nGGxZArm5hOWbLxfygVzulkeEsozjeDcVaUZPA1o +gN9QgqSC850jjqddV7a0sZuHyd3KECkfU1uRoSHikTKHJLxaV6BIHzliQijRutLT +hTFpk4QzM+7xixY66hh56YAoTB44A0sXHNrH5Zt3YkUv4mT/P9ue9DQ1mhz2Eh4h +DAzWFq4BAoGBAPOCOyar6K0mogxUl1Q1LBH34HvyrZ4uVN7ICeE12neh/fS04fnV +lM5e6bxKj2HEzqqJ8JUxjQypurz6aQBSI1o0hc13eZqykoNa7c9TREY8swM4Vwl9 +NOdyBuysJIqG9+tPp+vReP4ISyLG8fyeJNDl1OEaaz2GcxPGDbf7hR1hAoGBAN5o +V0tXp5wG/RnUZn0FU2g8faw5tfB4iO38igYJPEo5YbiC7FcJHYiPUlEfjlWmWwGT +RNggYK6Q7lQ4NKJU80DTG2k5AK5yLHtmtRmPPhYMZAt5txCXF7zpuoQS7OTKnc4l +R9pnsEYn+kYN/YA3EC56j/uSj6VEdB/AgHNvs51pAoGBAN4DlrqjcfisiIKFfZOh +BxU60skvcWwPAgI8kAVtfEomv8wkPwPx30Jo9uJdeGzDa0nBij/8dYVeGovCI4nP +WbwctwGmNJD+zuZEOR4V5OHE5dHBxFk6dsmuBPIz4P0MIW3BqnAvBAlYtmh2yppv +9VEguv6hf7UQqEsW/9sGz08BAoGBAKMnripCMl3+rnvdWhYK6yYDgjnu2C6BbgoQ +Afztl4Hn2G0v9krfEABXC48hdBwW/poIPC/EiMhm379+v/X6Fb0PYQNu4rYWYdVh +Aieu8l/gVSAp+Qa9oJdgawhqjchFb0CEDtMEz8aXmzz7FGWTf1ZpaOinmqMltX55 +jIGihwRJAoGATH9eCCCv1rbdnM8+qY3R8PZcZAOugICN55woXHJBCRRT+CpY4kiG +LNXyLZILWs8LXbzkKtynCXmVHMGbehtY/PnGXoSpsgLLz15CuGrbyQLoLnTr8NTB +atV5a96f8fOPhFl9EIRzV8u3DIF4CoYvNAFws7VwPDw01FCw5MncX2g= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/docker/mtls/config/tls/localhost.keystore.jks b/docker/mtls/config/tls/localhost.keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..8062bebe308ed12fbfb527ae72cf615cdf153807 GIT binary patch literal 2581 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