Skip to content

Added property to accept certificates for tls termination at tcp router #502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion jobs/tcp_router/spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions jobs/tcp_router/templates/tcp_router.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 -%>
25 changes: 25 additions & 0 deletions spec/tcp_router_templates_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
95 changes: 88 additions & 7 deletions src/code.cloudfoundry.org/cf-tcp-router/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand All @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have to worry about proper permissions? Copying the sibling's (backend) permissions:

chown -R root:vcap /var/vcap/jobs/tcp_router/config/certs/tcp-router/frontend
chmod -R 640 	  /var/vcap/jobs/tcp_router/config/certs/tcp-router/frontend
chmod      750 	  /var/vcap/jobs/tcp_router/config/certs/tcp-router/frontend

}

func (c *Config) initConfigFromFile(path string) error {
var e error

Expand All @@ -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)
Expand Down Expand Up @@ -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" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some documentation here may help.

Where did this id 2.5.29.17 come from? Is this a well known constant in the go library perhaps? What would happen if this value changes? How do we update this value?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the OID of the SAN field coming from the ASN1 spec. It will never change. https://oidref.com/2.5.29.17

I think that the check that computes hasSANEntries below is sufficient / redundant, but not 100% sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasSANExtension = true
break
}
}

hasDNSEntries := len(cert.DNSNames) > 0

return hasSANExtension || hasDNSEntries
}
71 changes: 71 additions & 0 deletions src/code.cloudfoundry.org/cf-tcp-router/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config_test
import (
"fmt"
"os"
"path/filepath"
"time"

tlshelpers "code.cloudfoundry.org/cf-routing-test-helpers/tls"
Expand Down Expand Up @@ -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"))
})

})
})

})
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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"
Loading