diff --git a/jobs/tcp_router/spec b/jobs/tcp_router/spec index 234e3db92..96b6fec45 100644 --- a/jobs/tcp_router/spec +++ b/jobs/tcp_router/spec @@ -59,13 +59,31 @@ properties: be set. For mTLS also set tcp_router.backend_tls.client_cert and tcp_router.backend_tls.client_key. default: false + tcp_router.frontend_tls_pem.enabled: + description: | + This is enabled if certificates and keys are provided for tls traffic to be terminated at tcp router. + default: false + tcp_router.frontend_tls_pem.certificate_path: + description: Path to the certs and key store + tcp_router.frontend_tls: + description: "Array of private keys, certificates and names for serving TLS requests. Each element in the array is an object containing fields 'private_key' and 'cert_chain', each of which supports a PEM block." + example: | + - cert_chain: | + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + private_key: | + -----BEGIN RSA PRIVATE KEY----- + -----END RSA PRIVATE KEY----- + name: | + name of the cert tcp_router.backend_tls.client_cert: description: "TCP Router's TLS client cert used for mTLS with route backends" tcp_router.backend_tls.client_key: description: "TCP Router's TLS client private key used for mTLS with route backends" tcp_router.backend_tls.ca_cert: description: "TCP Router's TLS CA used with route backends" - routing_api.uri: description: "URL where the routing API can be reached internally" default: https://routing-api.service.cf.internal diff --git a/jobs/tcp_router/templates/tcp_router.yml.erb b/jobs/tcp_router/templates/tcp_router.yml.erb index 645a48caa..582585a35 100644 --- a/jobs/tcp_router/templates/tcp_router.yml.erb +++ b/jobs/tcp_router/templates/tcp_router.yml.erb @@ -91,3 +91,16 @@ backend_tls: client_cert_and_key_path: "/var/vcap/jobs/tcp_router/config/keys/tcp-router/backend/client_cert_and_key.pem" <% end %> <% end -%> + +<% +frontend_tls = p('tcp_router.frontend_tls', []) +%> + +frontend_tls: +<% frontend_tls.each do |cert_pair| -%> + - name: <%= cert_pair['name'].inspect %> + cert_chain: | +<%= cert_pair['cert_chain'].to_s.lines.map { |line| " #{line.rstrip}" }.join("\n") %> + private_key: | +<%= cert_pair['private_key'].to_s.lines.map { |line| " #{line.rstrip}" }.join("\n") %> +<% end -%> diff --git a/spec/tcp_router_templates_spec.rb b/spec/tcp_router_templates_spec.rb index 549abcfb1..2609d6c46 100644 --- a/spec/tcp_router_templates_spec.rb +++ b/spec/tcp_router_templates_spec.rb @@ -10,12 +10,25 @@ let(:release) { Bosh::Template::Test::ReleaseDir.new(release_path) } let(:job) { release.job('tcp_router') } let(:backend_tls) { {} } + let(:frontend_tls) { {} } let(:merged_manifest_properties) do { 'tcp_router' => { 'oauth_secret' => '', 'backend_tls' => backend_tls, + 'frontend_tls' => [ + { + 'name' => 'testkey', + 'cert_chain' => TEST_CERT, + 'private_key' => TEST_KEY + }, + { + 'name' => 'testkey2', + 'cert_chain' => TEST_CERT2, + 'private_key' => TEST_KEY + } + ], }, 'uaa' => { 'tls_port' => 1000 @@ -306,6 +319,18 @@ }, 'drain_wait' => '20s', 'backend_tls' => { 'enabled' => false }, + 'frontend_tls' => [ + { + 'name' => 'testkey', + 'cert_chain' => TEST_CERT + "\n", + 'private_key' => TEST_KEY+ "\n" + }, + { + 'name' => 'testkey2', + 'cert_chain' => TEST_CERT2+ "\n", + 'private_key' => TEST_KEY+ "\n" + } + ], 'reserved_system_component_ports' => [8080, 8081], 'routing_api' => { 'uri' => 'https://routing-api.service.cf.internal', diff --git a/src/code.cloudfoundry.org/cf-tcp-router/config/config.go b/src/code.cloudfoundry.org/cf-tcp-router/config/config.go index 279e223f9..8a3037d87 100644 --- a/src/code.cloudfoundry.org/cf-tcp-router/config/config.go +++ b/src/code.cloudfoundry.org/cf-tcp-router/config/config.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" "time" @@ -38,15 +39,27 @@ type BackendTLSConfig struct { CACertificatePath string `yaml:"ca_cert_path"` ClientCertAndKeyPath string `yaml:"client_cert_and_key_path"` } +type FrontendTLSConfig struct { + Enabled bool `yaml:"enabled"` + CertPath string `yaml:"cert_path"` +} + +type FrontendTLSJob struct { + Name string `yaml:"name"` + CertChain string `yaml:"cert_chain"` + PrivateKey string `yaml:"private_key"` +} type Config struct { - OAuth OAuthConfig `yaml:"oauth"` - RoutingAPI RoutingAPIConfig `yaml:"routing_api"` - HaProxyPidFile string `yaml:"haproxy_pid_file"` - IsolationSegments []string `yaml:"isolation_segments"` - ReservedSystemComponentPorts []uint16 `yaml:"reserved_system_component_ports"` - DrainWaitDuration time.Duration `yaml:"drain_wait"` - BackendTLS BackendTLSConfig `yaml:"backend_tls"` + OAuth OAuthConfig `yaml:"oauth"` + RoutingAPI RoutingAPIConfig `yaml:"routing_api"` + HaProxyPidFile string `yaml:"haproxy_pid_file"` + IsolationSegments []string `yaml:"isolation_segments"` + ReservedSystemComponentPorts []uint16 `yaml:"reserved_system_component_ports"` + DrainWaitDuration time.Duration `yaml:"drain_wait"` + BackendTLS BackendTLSConfig `yaml:"backend_tls"` + FrontendTLS []FrontendTLSConfig `yaml:"frontend_tls_pem"` + FrontendTLSJob []FrontendTLSJob `yaml:"frontend_tls"` } const DrainWaitDefault = 20 * time.Second @@ -60,6 +73,13 @@ func New(path string) (*Config, error) { return c, nil } +func (c *Config) FrontendTLSJobBasePath() string { + if bp := os.Getenv("FRONTEND_TLS_BASE_PATH"); bp != "" { + return bp + } + return "/var/vcap/jobs/tcp_router/config/keys/tcp-router/frontend" +} + func (c *Config) initConfigFromFile(path string) error { var e error @@ -81,6 +101,53 @@ func (c *Config) initConfigFromFile(path string) error { c.DrainWaitDuration = DrainWaitDefault } + if len(c.FrontendTLSJob) > 0 { + var outputs []FrontendTLSConfig + basePath := c.FrontendTLSJobBasePath() + for i, cert := range c.FrontendTLSJob { + + name := strings.TrimSpace(cert.Name) + certChain := strings.TrimSpace(cert.CertChain) + privateKey := strings.TrimSpace(cert.PrivateKey) + + if name == "" || certChain == "" || privateKey == "" { + return fmt.Errorf("frontend_tls[%d] must include name, cert_chain, and private_key", i) + } + + block, _ := pem.Decode([]byte(certChain)) + if block == nil { + return errors.New("failed to parse PEM block") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + + hasSAN := certHasSAN(cert) + if !hasSAN { + return fmt.Errorf("frontend_tls[%d].cert_chain must include a subjectAltName extension", i) + } + + dirPath := filepath.Join(basePath, name) + os.MkdirAll(dirPath, 0755) + + certFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.pem", name)) + keyFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.pem.key", name)) + + os.WriteFile(certFilePath, []byte(certChain), 0644) + + os.WriteFile(keyFilePath, []byte(privateKey), 0600) + + outputs = append(outputs, FrontendTLSConfig{ + Enabled: true, + CertPath: certFilePath, + }) + } + + c.FrontendTLS = outputs + + } + if c.BackendTLS.Enabled { if c.BackendTLS.CACertificatePath != "" { pemData, err := os.ReadFile(c.BackendTLS.CACertificatePath) @@ -156,3 +223,17 @@ func (c *Config) initConfigFromFile(path string) error { return nil } + +func certHasSAN(cert *x509.Certificate) bool { + hasSANExtension := false + for _, ext := range cert.Extensions { + if ext.Id.String() == "2.5.29.17" { + hasSANExtension = true + break + } + } + + hasDNSEntries := len(cert.DNSNames) > 0 + + return hasSANExtension || hasDNSEntries +} diff --git a/src/code.cloudfoundry.org/cf-tcp-router/config/config_test.go b/src/code.cloudfoundry.org/cf-tcp-router/config/config_test.go index eaed00f4d..1f780076b 100644 --- a/src/code.cloudfoundry.org/cf-tcp-router/config/config_test.go +++ b/src/code.cloudfoundry.org/cf-tcp-router/config/config_test.go @@ -3,6 +3,7 @@ package config_test import ( "fmt" "os" + "path/filepath" "time" tlshelpers "code.cloudfoundry.org/cf-routing-test-helpers/tls" @@ -208,4 +209,74 @@ var _ = Describe("Config", Serial, func() { }) }) + + Context("When frontend_tls is enabled", func() { + var ( + tmpDir string + cfg *config.Config + err error + ) + + BeforeEach(func() { + tmpDir = GinkgoT().TempDir() + os.Setenv("FRONTEND_TLS_BASE_PATH", tmpDir) + }) + + AfterEach(func() { + os.Unsetenv("FRONTEND_TLS_BASE_PATH") + }) + + Context("with valid cert and key", func() { + BeforeEach(func() { + cfg, err = config.New("fixtures/valid_frontend_cert.yml") + }) + + It("loads config without error", func() { + Expect(err).NotTo(HaveOccurred()) + }) + + It("adds the certs and keys to the expected directories", func() { + Expect(err).NotTo(HaveOccurred()) + Expect(cfg.FrontendTLS).To(HaveLen(2)) + + Expect(cfg.FrontendTLS[0]).To(Equal(config.FrontendTLSConfig{ + Enabled: true, + CertPath: filepath.Join(tmpDir, "prod"), + })) + + Expect(cfg.FrontendTLS[1]).To(Equal(config.FrontendTLSConfig{ + Enabled: true, + CertPath: filepath.Join(tmpDir, "dev"), + })) + }) + + It("writes the correct cert and key files", func() { + for i, name := range []string{"prod", "dev"} { + certPath := filepath.Join(tmpDir, name, name+".cert.pem") + keyPath := filepath.Join(tmpDir, name, name+".key.pem") + + Expect(certPath).To(BeAnExistingFile()) + Expect(keyPath).To(BeAnExistingFile()) + + certData, certErr := os.ReadFile(certPath) + Expect(certErr).NotTo(HaveOccurred()) + Expect(string(certData)).To(Equal(cfg.FrontendTLSJob[i].CertChain)) + + keyData, keyErr := os.ReadFile(keyPath) + Expect(keyErr).NotTo(HaveOccurred()) + Expect(string(keyData)).To(Equal(cfg.FrontendTLSJob[i].PrivateKey)) + } + }) + }) + + Context("with invalid frontend_tls config", func() { + It("should fail if cert_chain is missing SAN information", func() { + _, err := config.New("fixtures/invalid_frontend_cert.yml") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("frontend_tls[0].cert_chain must include a subjectAltName extension")) + }) + + }) + }) + }) diff --git a/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/bad_client_cert_config.yml b/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/bad_client_cert_config.yml index b8af5d4b1..935082f1c 100644 --- a/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/bad_client_cert_config.yml +++ b/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/bad_client_cert_config.yml @@ -20,4 +20,4 @@ reserved_system_component_ports: [8080, 8081] backend_tls: enabled: true ca_cert_path: fixtures/ca.pem - client_cert_and_key_path: fixtures/bad_cert_and_key.pem + client_cert_and_key_path: fixtures/bad_cert_and_key.pem \ No newline at end of file diff --git a/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/invalid_frontend_cert.yml b/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/invalid_frontend_cert.yml new file mode 100644 index 000000000..8a081c1f6 --- /dev/null +++ b/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/invalid_frontend_cert.yml @@ -0,0 +1,48 @@ +oauth: + token_endpoint: "uaa.service.cf.internal" + client_name: "someclient" + client_secret: "somesecret" + port: 8443 + skip_ssl_validation: true + ca_certs: "some-ca-cert" + +routing_api: + uri: http://routing-api.service.cf.internal + port: 3000 + auth_disabled: false + client_cert_path: /a/client_cert + client_private_key_path: /b/private_key + ca_cert_path: /c/ca_cert + +haproxy_pid_file: /path/to/pid/file +isolation_segments: ["foo-iso-seg"] +reserved_system_component_ports: [8080, 8081] +frontend_tls: + - name: "prod" + cert_chain: |- + -----BEGIN CERTIFICATE----- + MIIEITCCAgmgAwIBAgIRAMGCNmHhXZnK1fSdCinKK9owDQYJKoZIhvcNAQELBQAw + EjEQMA4GA1UEAxMHdGVzdC1jYTAeFw0yMTEwMjExNjU2NTJaFw0yMzA0MjExNjI5 + MDVaMBMxETAPBgNVBAMTCHRlc3QuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A + MIIBCgKCAQEAwY9FO90qNGnztTlPSUODTLvdKex08dA+/hQ2URMBStqI5g6dJZP9 + RcLVyRpp9719KKs2PL2ol/QEfUMXKSB1pld6kRGFEXbPkz8rxLhYt79UzjAC8lWj + z/NbyIvNVzqgYlB7Tk+sgIBF3LSV3Zh4ZsrNoXMu/VDG+ODm/1dcLZJE3QXaMM6Z + nbvdy/eUOhJ12BzgM+1PKjNi93azOB6uBiXZ1QgzWbmWJHnGmvX/HUdT8s4e1snt + 5mAsS7hmsrxpu2QD9b3gGUIgy6z6ZuFp1kq0S5HxoFDNjvi88p2E4Jk+unfFMaO9 + 4+OyOZWW5TqyyhTYCrhBEcZ4m5hm82v76wIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC + A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRZ7D+U + LkHi0vbszx8bMG2LZSqUejAfBgNVHSMEGDAWgBQSH/Cc1RDZyRh9u9sfHeS3eWYz + TDANBgkqhkiG9w0BAQsFAAOCAgEA1YluE0iSE4HEc2N2fdYhmwF2LP3pjUfmzF/g + NcxjhydQUoxyOxf6+1RsNe7taXQRLhmpN2JaiE8yCf+wDciIhRWnqyHgJEKoJgK6 + 4liu7JUpOFgAloe8koKhWxEerkU4VcPy8kN5gZ8I6b8Mso4hTq2O5NhntqKDFRS0 + v0ZpMkz1PhWwI79No8WXU0tUwx5pT3mcwjCr57mnyYWmeHqAXgnUI4U0QnSyr3sa + jmjpLk2TncpC3CSTr1AbOhm/yglsrbLllvufHUbYv5QNlzkOauvgCzvXQ4ScFttn + epDzPE8PrsY8N/26BwOCc6ftQqabhpIKzT6w6DN5xYRZi5fyzRNho5+5RuBDRKmL + AGfrpiixm4zzgUL7jVlOVlZXQ/vkQ+h4+aqS2ssRwPoqGxilFxfUMgO+hr3jZkxz + o9Z7Yeljt7rzeYESEDtkwou+75LHzfKduVT8Kxwn8LwiB0trgbcx3qj2ab8fucM4 + UUXAXr6ve5DcdkKevLoNypq2kCh7hySjrjDp/gnCMhuc0ch8oV2RV2ZlA+QOD+J4 + VAgYLhy03ZZaUFvmGhCx+FEkkzq/d2GGWuNd1T2MMkTBplf+pK+3l+jHxYuSc8DR + gPYhs8i50bWlTVu/yJgJGBzAmWcybfi7NmUkQyYHmpLP3GRbtdI+eESF9vAJpKSs + ONppgXo= + -----END CERTIFICATE----- + private_key: "some key" \ No newline at end of file diff --git a/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/valid_frontend_cert.yml b/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/valid_frontend_cert.yml new file mode 100644 index 000000000..86c6138a2 --- /dev/null +++ b/src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/valid_frontend_cert.yml @@ -0,0 +1,76 @@ +oauth: + token_endpoint: "uaa.service.cf.internal" + client_name: "someclient" + client_secret: "somesecret" + port: 8443 + skip_ssl_validation: true + ca_certs: "some-ca-cert" + +routing_api: + uri: http://routing-api.service.cf.internal + port: 3000 + auth_disabled: false + client_cert_path: /a/client_cert + client_private_key_path: /b/private_key + ca_cert_path: /c/ca_cert + +haproxy_pid_file: /path/to/pid/file +isolation_segments: ["foo-iso-seg"] +reserved_system_component_ports: [8080, 8081] +frontend_tls: + - name: "prod" + cert_chain: |- + -----BEGIN CERTIFICATE----- + MIIESzCCAjOgAwIBAgIQDnaPUSkJl2T+TaMLHUlWqzANBgkqhkiG9w0BAQsFADAS + MRAwDgYDVQQDEwd0ZXN0LWNhMB4XDTIxMTAyMTIwMDIzMloXDTIzMDQyMTE2Mjkw + NVowHTEbMBkGA1UEAxMSdGVzdDItd2l0aC1zYW4uY29tMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEAwc8fvFNfGF1SqVs7UOTwYbQCv18wF+EfJYT4tq3P + 1MLBuW7eURKnJ4ZAslsogX4WXmksYHnjRbIsQw6mtgAkMtkC+C5tuRO5uaEBSFxP + vA9z7b9uM9MGA2YSJVP1+U00y/HCrwI5LEc/SGij3bvKOcs+CUAEmHhr3sG95BTF + atVE5vbG+XHLw2DwaWzDFrtPG3o9zBtDb7/yqTNJCb+i7iyp9Yh0N2ZHgKjNI8Ru + 6rEkQz8nYk44NjCwV5l0fKV3eKLXTRyfEb+Gr1RHfTtG7wRvfDcDanS5XTZQWAAr + a61V38xfR+bYniYsSLmH/VZ1CAhqY8t45N9Sc47cH6gOHQIDAQABo4GRMIGOMA4G + A1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYD + VR0OBBYEFMl9dHRf9wJoWyuWNg93QldFc5IKMB8GA1UdIwQYMBaAFBIf8JzVENnJ + GH272x8d5Ld5ZjNMMB0GA1UdEQQWMBSCEnRlc3QyLXdpdGgtc2FuLmNvbTANBgkq + hkiG9w0BAQsFAAOCAgEADQFp7nPDLMRPbwWo1fRj7jF6XC85qJF5F9hMRyuioxu9 + ZHnlejEpi8o41/NRCV0lPLIAXe9/0owppZF3WhqF7eYUFa1YxFr+BkQg3r4nAq9g + UKkT2qB/AmJA72HYGGYbb1OdxccRxbgh1+nWzEkxFzB7HbiIrY7OqmDFLr7JRAeF + kaH27wLnZ2TJ2MCDQZNM8n12+3szwytZDl8uz6Dl5W7L4HcK6KIROhvjA6s3lAvA + 3E2VJ07bkAvMcXGX0jobcTVDB/+WVvVoZym0TfmMUVQ0JD6vFe8sNdsJysWCsUe9 + DbE9ZRA+3GaURVlpZ89n4sURVIopP+N51Vs6aZAIZdOuOvFwu+7N82LjI4i8800c + P90vC+M77jSGmu7Cuehu62Q0aUIx+X98TXQXpb4KLQ5Ot5RIPV0E3ksmKuqIrwuO + m5tSO2hX/BIkj160bikWbi3oqU8+91+jeW9fQnRLApkPwLWXSViF1Q7K8c6+/H/p + oyX7VxkqnU43+nzL+Egc9ibYRF22XMkCBICZFrPu3rZbz8zHTw43ldqHezvx+O/J + R5DG1U9dcbt9urELWUBEWlrDudlyC1p6ZvMYQIHP2e27pUaU6wFy7xnIrTxYbDM6 + HTngE/Gz+qIUe7OkPXPPkFeoSfR1poQ3yNz4bim9Vx+w50l6m+h6SZOYJTxEUds= + -----END CERTIFICATE----- + private_key: "some key" + - name: "dev" + cert_chain: |- + -----BEGIN CERTIFICATE----- + MIIESzCCAjOgAwIBAgIQDnaPUSkJl2T+TaMLHUlWqzANBgkqhkiG9w0BAQsFADAS + MRAwDgYDVQQDEwd0ZXN0LWNhMB4XDTIxMTAyMTIwMDIzMloXDTIzMDQyMTE2Mjkw + NVowHTEbMBkGA1UEAxMSdGVzdDItd2l0aC1zYW4uY29tMIIBIjANBgkqhkiG9w0B + AQEFAAOCAQ8AMIIBCgKCAQEAwc8fvFNfGF1SqVs7UOTwYbQCv18wF+EfJYT4tq3P + 1MLBuW7eURKnJ4ZAslsogX4WXmksYHnjRbIsQw6mtgAkMtkC+C5tuRO5uaEBSFxP + vA9z7b9uM9MGA2YSJVP1+U00y/HCrwI5LEc/SGij3bvKOcs+CUAEmHhr3sG95BTF + atVE5vbG+XHLw2DwaWzDFrtPG3o9zBtDb7/yqTNJCb+i7iyp9Yh0N2ZHgKjNI8Ru + 6rEkQz8nYk44NjCwV5l0fKV3eKLXTRyfEb+Gr1RHfTtG7wRvfDcDanS5XTZQWAAr + a61V38xfR+bYniYsSLmH/VZ1CAhqY8t45N9Sc47cH6gOHQIDAQABo4GRMIGOMA4G + A1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYD + VR0OBBYEFMl9dHRf9wJoWyuWNg93QldFc5IKMB8GA1UdIwQYMBaAFBIf8JzVENnJ + GH272x8d5Ld5ZjNMMB0GA1UdEQQWMBSCEnRlc3QyLXdpdGgtc2FuLmNvbTANBgkq + hkiG9w0BAQsFAAOCAgEADQFp7nPDLMRPbwWo1fRj7jF6XC85qJF5F9hMRyuioxu9 + ZHnlejEpi8o41/NRCV0lPLIAXe9/0owppZF3WhqF7eYUFa1YxFr+BkQg3r4nAq9g + UKkT2qB/AmJA72HYGGYbb1OdxccRxbgh1+nWzEkxFzB7HbiIrY7OqmDFLr7JRAeF + kaH27wLnZ2TJ2MCDQZNM8n12+3szwytZDl8uz6Dl5W7L4HcK6KIROhvjA6s3lAvA + 3E2VJ07bkAvMcXGX0jobcTVDB/+WVvVoZym0TfmMUVQ0JD6vFe8sNdsJysWCsUe9 + DbE9ZRA+3GaURVlpZ89n4sURVIopP+N51Vs6aZAIZdOuOvFwu+7N82LjI4i8800c + P90vC+M77jSGmu7Cuehu62Q0aUIx+X98TXQXpb4KLQ5Ot5RIPV0E3ksmKuqIrwuO + m5tSO2hX/BIkj160bikWbi3oqU8+91+jeW9fQnRLApkPwLWXSViF1Q7K8c6+/H/p + oyX7VxkqnU43+nzL+Egc9ibYRF22XMkCBICZFrPu3rZbz8zHTw43ldqHezvx+O/J + R5DG1U9dcbt9urELWUBEWlrDudlyC1p6ZvMYQIHP2e27pUaU6wFy7xnIrTxYbDM6 + HTngE/Gz+qIUe7OkPXPPkFeoSfR1poQ3yNz4bim9Vx+w50l6m+h6SZOYJTxEUds= + -----END CERTIFICATE----- + private_key: "some other key"