Skip to content

Commit 6703b54

Browse files
committed
Added property to accept certificates for tls termination at tcp router
Signed-off-by: Ashish Naware <[email protected]>
1 parent a165159 commit 6703b54

File tree

7 files changed

+323
-10
lines changed

7 files changed

+323
-10
lines changed

jobs/tcp_router/spec

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,31 @@ properties:
5959
be set. For mTLS also set tcp_router.backend_tls.client_cert and
6060
tcp_router.backend_tls.client_key.
6161
default: false
62+
tcp_router.frontend_tls_pem.enabled:
63+
description: |
64+
This is enabled if certificates and keys are provided for tls traffic to be terminated at tcp router.
65+
default: false
66+
tcp_router.frontend_tls_pem.certificate_path:
67+
description: Path to the certs and key store
68+
tcp_router.frontend_tls:
69+
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."
70+
example: |
71+
- cert_chain: |
72+
-----BEGIN CERTIFICATE-----
73+
-----END CERTIFICATE-----
74+
-----BEGIN CERTIFICATE-----
75+
-----END CERTIFICATE-----
76+
private_key: |
77+
-----BEGIN RSA PRIVATE KEY-----
78+
-----END RSA PRIVATE KEY-----
79+
name: |
80+
name of the cert
6281
tcp_router.backend_tls.client_cert:
6382
description: "TCP Router's TLS client cert used for mTLS with route backends"
6483
tcp_router.backend_tls.client_key:
6584
description: "TCP Router's TLS client private key used for mTLS with route backends"
6685
tcp_router.backend_tls.ca_cert:
6786
description: "TCP Router's TLS CA used with route backends"
68-
6987
routing_api.uri:
7088
description: "URL where the routing API can be reached internally"
7189
default: https://routing-api.service.cf.internal

jobs/tcp_router/templates/tcp_router.yml.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,4 @@ backend_tls:
9090
<% if client_cert != '' and client_key != '' %>
9191
client_cert_and_key_path: "/var/vcap/jobs/tcp_router/config/keys/tcp-router/backend/client_cert_and_key.pem"
9292
<% end %>
93-
<% end -%>
93+
<% end -%>

src/code.cloudfoundry.org/cf-tcp-router/config/config.go

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"os"
11+
"path/filepath"
1112
"strings"
1213
"time"
1314

@@ -38,15 +39,27 @@ type BackendTLSConfig struct {
3839
CACertificatePath string `yaml:"ca_cert_path"`
3940
ClientCertAndKeyPath string `yaml:"client_cert_and_key_path"`
4041
}
42+
type FrontendTLSConfig struct {
43+
Enabled bool `yaml:"enabled"`
44+
CertPath string `yaml:"cert_path"`
45+
}
46+
47+
type FrontendTLSJob struct {
48+
Name string `yaml:"name"`
49+
CertChain string `yaml:"cert_chain"`
50+
PrivateKey string `yaml:"private_key"`
51+
}
4152

4253
type Config struct {
43-
OAuth OAuthConfig `yaml:"oauth"`
44-
RoutingAPI RoutingAPIConfig `yaml:"routing_api"`
45-
HaProxyPidFile string `yaml:"haproxy_pid_file"`
46-
IsolationSegments []string `yaml:"isolation_segments"`
47-
ReservedSystemComponentPorts []uint16 `yaml:"reserved_system_component_ports"`
48-
DrainWaitDuration time.Duration `yaml:"drain_wait"`
49-
BackendTLS BackendTLSConfig `yaml:"backend_tls"`
54+
OAuth OAuthConfig `yaml:"oauth"`
55+
RoutingAPI RoutingAPIConfig `yaml:"routing_api"`
56+
HaProxyPidFile string `yaml:"haproxy_pid_file"`
57+
IsolationSegments []string `yaml:"isolation_segments"`
58+
ReservedSystemComponentPorts []uint16 `yaml:"reserved_system_component_ports"`
59+
DrainWaitDuration time.Duration `yaml:"drain_wait"`
60+
BackendTLS BackendTLSConfig `yaml:"backend_tls"`
61+
FrontendTLS []FrontendTLSConfig `yaml:"frontend_tls_pem"`
62+
FrontendTLSJob []FrontendTLSJob `yaml:"frontend_tls"`
5063
}
5164

5265
const DrainWaitDefault = 20 * time.Second
@@ -60,6 +73,13 @@ func New(path string) (*Config, error) {
6073
return c, nil
6174
}
6275

76+
func (c *Config) FrontendTLSJobBasePath() string {
77+
if bp := os.Getenv("FRONTEND_TLS_BASE_PATH"); bp != "" {
78+
return bp
79+
}
80+
return "/var/vcap/jobs/tcp_router/config/keys/tcp-router/frontend"
81+
}
82+
6383
func (c *Config) initConfigFromFile(path string) error {
6484
var e error
6585

@@ -81,6 +101,59 @@ func (c *Config) initConfigFromFile(path string) error {
81101
c.DrainWaitDuration = DrainWaitDefault
82102
}
83103

104+
if len(c.FrontendTLSJob) > 0 {
105+
var outputs []FrontendTLSConfig
106+
basePath := c.FrontendTLSJobBasePath()
107+
for i, cert := range c.FrontendTLSJob {
108+
109+
name := strings.TrimSpace(cert.Name)
110+
certChain := strings.TrimSpace(cert.CertChain)
111+
privateKey := strings.TrimSpace(cert.PrivateKey)
112+
113+
if name == "" || certChain == "" || privateKey == "" {
114+
return fmt.Errorf("frontend_tls[%d] must include name, cert_chain, and private_key", i)
115+
}
116+
117+
block, _ := pem.Decode([]byte(certChain))
118+
if block == nil {
119+
return errors.New("failed to parse PEM block")
120+
}
121+
cert, err := x509.ParseCertificate(block.Bytes)
122+
if err != nil {
123+
return err
124+
}
125+
126+
hasSAN := certHasSAN(cert)
127+
if !hasSAN {
128+
return fmt.Errorf("frontend_tls[%d].cert_chain must include a subjectAltName extension", i)
129+
}
130+
131+
dirPath := filepath.Join(basePath, name)
132+
if err := os.MkdirAll(dirPath, 0755); err != nil {
133+
return fmt.Errorf("failed to create cert directory at %s: %w", dirPath, err)
134+
}
135+
136+
certFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.cert.pem", name))
137+
keyFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.key.pem", name))
138+
139+
if err := os.WriteFile(certFilePath, []byte(certChain), 0644); err != nil {
140+
return fmt.Errorf("failed to write cert file at %s: %w", certFilePath, err)
141+
}
142+
143+
if err := os.WriteFile(keyFilePath, []byte(privateKey), 0600); err != nil {
144+
return fmt.Errorf("failed to write key file at %s: %w", keyFilePath, err)
145+
}
146+
147+
outputs = append(outputs, FrontendTLSConfig{
148+
Enabled: true,
149+
CertPath: dirPath,
150+
})
151+
}
152+
153+
c.FrontendTLS = outputs
154+
155+
}
156+
84157
if c.BackendTLS.Enabled {
85158
if c.BackendTLS.CACertificatePath != "" {
86159
pemData, err := os.ReadFile(c.BackendTLS.CACertificatePath)
@@ -156,3 +229,20 @@ func (c *Config) initConfigFromFile(path string) error {
156229

157230
return nil
158231
}
232+
233+
func certHasSAN(cert *x509.Certificate) bool {
234+
hasSANExtension := false
235+
for _, ext := range cert.Extensions {
236+
if ext.Id.String() == "2.5.29.17" {
237+
hasSANExtension = true
238+
break
239+
}
240+
}
241+
242+
hasSANEntries := len(cert.DNSNames) > 0 ||
243+
len(cert.EmailAddresses) > 0 ||
244+
len(cert.IPAddresses) > 0 ||
245+
len(cert.URIs) > 0
246+
247+
return hasSANExtension || hasSANEntries
248+
}

src/code.cloudfoundry.org/cf-tcp-router/config/config_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config_test
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
67
"time"
78

89
tlshelpers "code.cloudfoundry.org/cf-routing-test-helpers/tls"
@@ -208,4 +209,84 @@ var _ = Describe("Config", Serial, func() {
208209

209210
})
210211
})
212+
213+
Context("When frontend_tls is enabled", func() {
214+
var (
215+
tmpDir string
216+
cfg *config.Config
217+
err error
218+
)
219+
220+
BeforeEach(func() {
221+
tmpDir = GinkgoT().TempDir()
222+
os.Setenv("FRONTEND_TLS_BASE_PATH", tmpDir)
223+
})
224+
225+
AfterEach(func() {
226+
os.Unsetenv("FRONTEND_TLS_BASE_PATH")
227+
})
228+
229+
Context("with valid cert and key", func() {
230+
BeforeEach(func() {
231+
cfg, err = config.New("fixtures/valid_frontend_cert.yml")
232+
})
233+
234+
It("loads config without error", func() {
235+
Expect(err).NotTo(HaveOccurred())
236+
})
237+
238+
It("adds the certs and keys to the expected directories", func() {
239+
Expect(err).NotTo(HaveOccurred())
240+
Expect(cfg.FrontendTLS).To(HaveLen(2))
241+
242+
Expect(cfg.FrontendTLS[0]).To(Equal(config.FrontendTLSConfig{
243+
Enabled: true,
244+
CertPath: filepath.Join(tmpDir, "prod"),
245+
}))
246+
247+
Expect(cfg.FrontendTLS[1]).To(Equal(config.FrontendTLSConfig{
248+
Enabled: true,
249+
CertPath: filepath.Join(tmpDir, "dev"),
250+
}))
251+
})
252+
253+
It("writes the correct cert and key files", func() {
254+
for i, name := range []string{"prod", "dev"} {
255+
certPath := filepath.Join(tmpDir, name, name+".cert.pem")
256+
keyPath := filepath.Join(tmpDir, name, name+".key.pem")
257+
258+
Expect(certPath).To(BeAnExistingFile())
259+
Expect(keyPath).To(BeAnExistingFile())
260+
261+
certData, certErr := os.ReadFile(certPath)
262+
Expect(certErr).NotTo(HaveOccurred())
263+
Expect(string(certData)).To(Equal(cfg.FrontendTLSJob[i].CertChain))
264+
265+
keyData, keyErr := os.ReadFile(keyPath)
266+
Expect(keyErr).NotTo(HaveOccurred())
267+
Expect(string(keyData)).To(Equal(cfg.FrontendTLSJob[i].PrivateKey))
268+
}
269+
})
270+
})
271+
272+
Context("with invalid frontend_tls config", func() {
273+
It("should fail if cert_chain is missing SAN information", func() {
274+
_, err := config.New("fixtures/invalid_frontend_cert.yml")
275+
Expect(err).To(HaveOccurred())
276+
Expect(err.Error()).To(Equal("frontend_tls[0].cert_chain must include a subjectAltName extension"))
277+
})
278+
279+
It("fails if directory cannot be created", func() {
280+
tmpFile, err := os.CreateTemp("", "non-existing-dir")
281+
Expect(err).NotTo(HaveOccurred())
282+
defer os.Remove(tmpFile.Name())
283+
284+
os.Setenv("FRONTEND_TLS_BASE_PATH", tmpFile.Name())
285+
286+
_, err = config.New("fixtures/valid_frontend_cert.yml")
287+
Expect(err).To(HaveOccurred())
288+
})
289+
})
290+
})
291+
211292
})

src/code.cloudfoundry.org/cf-tcp-router/config/fixtures/bad_client_cert_config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ reserved_system_component_ports: [8080, 8081]
2020
backend_tls:
2121
enabled: true
2222
ca_cert_path: fixtures/ca.pem
23-
client_cert_and_key_path: fixtures/bad_cert_and_key.pem
23+
client_cert_and_key_path: fixtures/bad_cert_and_key.pem
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
oauth:
2+
token_endpoint: "uaa.service.cf.internal"
3+
client_name: "someclient"
4+
client_secret: "somesecret"
5+
port: 8443
6+
skip_ssl_validation: true
7+
ca_certs: "some-ca-cert"
8+
9+
routing_api:
10+
uri: http://routing-api.service.cf.internal
11+
port: 3000
12+
auth_disabled: false
13+
client_cert_path: /a/client_cert
14+
client_private_key_path: /b/private_key
15+
ca_cert_path: /c/ca_cert
16+
17+
haproxy_pid_file: /path/to/pid/file
18+
isolation_segments: ["foo-iso-seg"]
19+
reserved_system_component_ports: [8080, 8081]
20+
frontend_tls:
21+
- name: "prod"
22+
cert_chain: |-
23+
-----BEGIN CERTIFICATE-----
24+
MIIEITCCAgmgAwIBAgIRAMGCNmHhXZnK1fSdCinKK9owDQYJKoZIhvcNAQELBQAw
25+
EjEQMA4GA1UEAxMHdGVzdC1jYTAeFw0yMTEwMjExNjU2NTJaFw0yMzA0MjExNjI5
26+
MDVaMBMxETAPBgNVBAMTCHRlc3QuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
27+
MIIBCgKCAQEAwY9FO90qNGnztTlPSUODTLvdKex08dA+/hQ2URMBStqI5g6dJZP9
28+
RcLVyRpp9719KKs2PL2ol/QEfUMXKSB1pld6kRGFEXbPkz8rxLhYt79UzjAC8lWj
29+
z/NbyIvNVzqgYlB7Tk+sgIBF3LSV3Zh4ZsrNoXMu/VDG+ODm/1dcLZJE3QXaMM6Z
30+
nbvdy/eUOhJ12BzgM+1PKjNi93azOB6uBiXZ1QgzWbmWJHnGmvX/HUdT8s4e1snt
31+
5mAsS7hmsrxpu2QD9b3gGUIgy6z6ZuFp1kq0S5HxoFDNjvi88p2E4Jk+unfFMaO9
32+
4+OyOZWW5TqyyhTYCrhBEcZ4m5hm82v76wIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
33+
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRZ7D+U
34+
LkHi0vbszx8bMG2LZSqUejAfBgNVHSMEGDAWgBQSH/Cc1RDZyRh9u9sfHeS3eWYz
35+
TDANBgkqhkiG9w0BAQsFAAOCAgEA1YluE0iSE4HEc2N2fdYhmwF2LP3pjUfmzF/g
36+
NcxjhydQUoxyOxf6+1RsNe7taXQRLhmpN2JaiE8yCf+wDciIhRWnqyHgJEKoJgK6
37+
4liu7JUpOFgAloe8koKhWxEerkU4VcPy8kN5gZ8I6b8Mso4hTq2O5NhntqKDFRS0
38+
v0ZpMkz1PhWwI79No8WXU0tUwx5pT3mcwjCr57mnyYWmeHqAXgnUI4U0QnSyr3sa
39+
jmjpLk2TncpC3CSTr1AbOhm/yglsrbLllvufHUbYv5QNlzkOauvgCzvXQ4ScFttn
40+
epDzPE8PrsY8N/26BwOCc6ftQqabhpIKzT6w6DN5xYRZi5fyzRNho5+5RuBDRKmL
41+
AGfrpiixm4zzgUL7jVlOVlZXQ/vkQ+h4+aqS2ssRwPoqGxilFxfUMgO+hr3jZkxz
42+
o9Z7Yeljt7rzeYESEDtkwou+75LHzfKduVT8Kxwn8LwiB0trgbcx3qj2ab8fucM4
43+
UUXAXr6ve5DcdkKevLoNypq2kCh7hySjrjDp/gnCMhuc0ch8oV2RV2ZlA+QOD+J4
44+
VAgYLhy03ZZaUFvmGhCx+FEkkzq/d2GGWuNd1T2MMkTBplf+pK+3l+jHxYuSc8DR
45+
gPYhs8i50bWlTVu/yJgJGBzAmWcybfi7NmUkQyYHmpLP3GRbtdI+eESF9vAJpKSs
46+
ONppgXo=
47+
-----END CERTIFICATE-----
48+
private_key: "some key"

0 commit comments

Comments
 (0)