Skip to content

Commit 93b7311

Browse files
authored
Merge pull request #508 from neowulf/siva/0824-cert-perms
tcp_router: fix cert directory permissions
2 parents 572cbec + 8520e54 commit 93b7311

File tree

6 files changed

+264
-105
lines changed

6 files changed

+264
-105
lines changed
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/bin/bash -e
22

3-
<% if spec.bootstrap == true %>
3+
# This pre-start script runs (1) config validation and (2) creates frontend certs on the VM (if any)
44
/var/vcap/packages/tcp_router/bin/config-validator -config /var/vcap/jobs/tcp_router/config/tcp_router.yml -enable-cert-creation true
5-
<% end %>

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

Lines changed: 111 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ func resolveGroupID(primary, fallback string) (int, error) {
102102
return gid, nil
103103
}
104104

105+
// initConfigFromFile initializes the config and write the certs to the filesystem when enableCertCreation is true.
106+
//
107+
// enableCertCreation is set to true during pre-start on all VMs. During pre-start initConfigFromFile is run
108+
// to (1) validate the config provided and (2) create the FrontendTLSJob certs on the filesystem if provided.
109+
//
110+
// enableCertCreation is set to false when bosh starts the tcp-router job.
105111
func (c *Config) initConfigFromFile(path string, enableCertCreation bool) error {
106112
b, err := os.ReadFile(path)
107113
if err != nil {
@@ -121,70 +127,56 @@ func (c *Config) initConfigFromFile(path string, enableCertCreation bool) error
121127
}
122128

123129
if enableCertCreation && len(c.FrontendTLSJob) > 0 {
124-
owner, err := user.Lookup("root")
125-
if err != nil {
126-
return err
130+
// remove existing directory which will clean out old certs if necessary
131+
if err := os.RemoveAll(c.FrontendTLSJobBasePath()); err != nil && !os.IsNotExist(err) {
132+
return fmt.Errorf("directory could not be removed: %w", err)
127133
}
128134

129-
uid, err := strconv.Atoi(owner.Uid)
130-
if err != nil {
131-
return err
135+
if err := c.createDir(c.FrontendTLSJobBasePath()); err != nil {
136+
return fmt.Errorf("could not create directory: %w", err)
132137
}
138+
}
133139

134-
gid, err := resolveGroupID("vcap", "root")
135-
if err != nil {
136-
return err
140+
for i, cert := range c.FrontendTLSJob {
141+
// validations
142+
name := strings.TrimSpace(cert.Name)
143+
if name == "" {
144+
return fmt.Errorf("frontend_tls[%d]: empty name", i)
137145
}
138-
var outputs []FrontendTLSConfig
139-
basePath := c.FrontendTLSJobBasePath()
140-
for i, cert := range c.FrontendTLSJob {
141146

142-
name := strings.TrimSpace(cert.Name)
143-
certChain := strings.TrimSpace(cert.CertChain)
144-
privateKey := strings.TrimSpace(cert.PrivateKey)
145-
146-
if name == "" || certChain == "" || privateKey == "" {
147-
return fmt.Errorf("frontend_tls[%d] must include name, cert_chain, and private_key", i)
148-
}
149-
150-
block, _ := pem.Decode([]byte(certChain))
151-
if block == nil {
152-
return errors.New("failed to parse PEM block")
153-
}
154-
cert, err := x509.ParseCertificate(block.Bytes)
155-
if err != nil {
156-
return err
157-
}
147+
certChain := strings.TrimSpace(cert.CertChain)
148+
if certChain == "" {
149+
return fmt.Errorf("frontend_tls[%d]: empty cert_chain", i)
150+
}
158151

159-
hasSAN := certHasSAN(cert)
160-
if !hasSAN {
161-
return fmt.Errorf("frontend_tls[%d].cert_chain must include a subjectAltName extension", i)
162-
}
152+
privateKey := strings.TrimSpace(cert.PrivateKey)
153+
if privateKey == "" {
154+
return fmt.Errorf("frontend_tls[%d]: empty private_key", i)
155+
}
163156

164-
dirPath := filepath.Join(basePath, name)
165-
if err := os.MkdirAll(dirPath, 0750); err != nil {
166-
return err
167-
}
157+
// check if san is present and that the cert chain is valid
158+
if err := certHasSAN(cert.CertChain); err != nil {
159+
return fmt.Errorf("frontend_tls[%d]: %w", i, err)
160+
}
168161

169-
certFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.pem", name))
170-
keyFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.pem.key", name))
162+
dirPath := filepath.Join(c.FrontendTLSJobBasePath(), name)
171163

172-
if err := writeFile(certFilePath, []byte(certChain), 0750, uid, gid); err != nil {
173-
return err
164+
// write certs to the disk
165+
if enableCertCreation {
166+
if err := c.createDir(dirPath); err != nil {
167+
return fmt.Errorf("frontend_tls[%d]: %w", i, err)
174168
}
175169

176-
if err := writeFile(keyFilePath, []byte(privateKey), 0750, uid, gid); err != nil {
177-
return err
170+
if err := c.writeCertsToDisk(dirPath, name, cert, privateKey); err != nil {
171+
return fmt.Errorf("frontend_tls[%d]: %w", i, err)
178172
}
179-
180-
outputs = append(outputs, FrontendTLSConfig{
181-
Enabled: true,
182-
CertificateDir: dirPath,
183-
})
184173
}
185174

186-
c.FrontendTLS = outputs
187-
175+
// update the config
176+
c.FrontendTLS = append(c.FrontendTLS, FrontendTLSConfig{
177+
Enabled: true,
178+
CertificateDir: dirPath,
179+
})
188180
}
189181

190182
if c.BackendTLS.Enabled {
@@ -262,28 +254,86 @@ func (c *Config) initConfigFromFile(path string, enableCertCreation bool) error
262254
return nil
263255
}
264256

265-
func certHasSAN(cert *x509.Certificate) bool {
266-
hasSANExtension := false
267-
for _, ext := range cert.Extensions {
268-
if ext.Id.String() == "2.5.29.17" {
269-
hasSANExtension = true
270-
break
271-
}
257+
func (c *Config) createDir(dirPath string) error {
258+
// ensure directory exists
259+
if err := os.MkdirAll(dirPath, 0750); err != nil {
260+
return err
261+
}
262+
263+
targetUID, targetGID, err := c.getUIDGID()
264+
if err != nil {
265+
return err
272266
}
273267

274-
hasDNSEntries := len(cert.DNSNames) > 0
268+
// Change ownership
269+
err = os.Chown(dirPath, targetUID, targetGID)
270+
if err != nil {
271+
return fmt.Errorf("Error changing ownership: %s", err)
272+
}
275273

276-
return hasSANExtension || hasDNSEntries
274+
return nil
277275
}
278276

279-
func writeFile(path string, data []byte, mode os.FileMode, uid, gid int) error {
280-
if err := os.WriteFile(path, data, mode); err != nil {
277+
func (c *Config) writeCertsToDisk(dirPath string, name string, cert FrontendTLSJob, privateKey string) error {
278+
uid, gid, err := c.getUIDGID()
279+
if err != nil {
281280
return err
282281
}
283282

284-
if err := os.Chown(path, uid, gid); err != nil {
283+
certFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.pem", name))
284+
keyFilePath := filepath.Join(dirPath, fmt.Sprintf("%s.pem.key", name))
285+
286+
if err := writeFile(certFilePath, []byte(cert.CertChain), 0750, uid, gid); err != nil {
285287
return err
286288
}
287289

290+
if err := writeFile(keyFilePath, []byte(privateKey), 0750, uid, gid); err != nil {
291+
return err
292+
}
288293
return nil
289294
}
295+
296+
func (c *Config) getUIDGID() (int, int, error) {
297+
owner, err := user.Lookup("root")
298+
if err != nil {
299+
return 0, 0, err
300+
}
301+
302+
uid, err := strconv.Atoi(owner.Uid)
303+
if err != nil {
304+
return 0, 0, err
305+
}
306+
307+
gid, err := resolveGroupID("vcap", "root")
308+
if err != nil {
309+
return 0, 0, err
310+
}
311+
312+
return uid, gid, nil
313+
}
314+
315+
func certHasSAN(certChain string) error {
316+
block, _ := pem.Decode([]byte(certChain))
317+
if block == nil {
318+
return errors.New("failed to parse PEM block")
319+
}
320+
321+
cert, err := x509.ParseCertificate(block.Bytes)
322+
if err != nil {
323+
return fmt.Errorf("could not parse certificate: %w", err)
324+
}
325+
326+
if len(cert.DNSNames) > 0 {
327+
return nil
328+
}
329+
330+
return fmt.Errorf("cert_chain must include either a subjectAltName extension or DNSNames")
331+
}
332+
333+
func writeFile(path string, data []byte, mode os.FileMode, uid, gid int) error {
334+
if err := os.WriteFile(path, data, mode); err != nil {
335+
return err
336+
}
337+
338+
return os.Chown(path, uid, gid)
339+
}

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

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package config_test
22

33
import (
44
"fmt"
5+
"math/rand/v2"
56
"os"
7+
"path"
68
"path/filepath"
79
"time"
810

@@ -221,15 +223,15 @@ var _ = Describe("Config", Serial, func() {
221223
)
222224

223225
BeforeEach(func() {
224-
tmpDir = GinkgoT().TempDir()
226+
tmpDir = path.Join(GinkgoT().TempDir(), fmt.Sprintf("frontend-%d", rand.IntN(100)))
225227
os.Setenv("FRONTEND_TLS_BASE_PATH", tmpDir)
226228
})
227229

228230
AfterEach(func() {
229231
os.Unsetenv("FRONTEND_TLS_BASE_PATH")
230232
})
231233

232-
Context("with valid cert and key", func() {
234+
Context("with valid cert and key and enableCertCreation set to true", func() {
233235
BeforeEach(func() {
234236
cfg, err = config.New("fixtures/valid_frontend_cert.yml", true)
235237
})
@@ -238,7 +240,7 @@ var _ = Describe("Config", Serial, func() {
238240
Expect(err).NotTo(HaveOccurred())
239241
})
240242

241-
It("adds the certs and keys to the expected directories", func() {
243+
It("updates the config with the expected directories", func() {
242244
Expect(err).NotTo(HaveOccurred())
243245
Expect(cfg.FrontendTLS).To(HaveLen(2))
244246

@@ -253,14 +255,26 @@ var _ = Describe("Config", Serial, func() {
253255
}))
254256
})
255257

256-
It("writes the correct cert and key files", func() {
258+
It("writes the cert and key files", func() {
259+
Expect(tmpDir).To(BeADirectory())
260+
257261
for i, name := range []string{"prod", "dev"} {
258262
certPath := filepath.Join(tmpDir, name, name+".pem")
259263
keyPath := filepath.Join(tmpDir, name, name+".pem.key")
260264

265+
Expect(path.Join(tmpDir, name)).To(BeADirectory())
266+
261267
Expect(certPath).To(BeAnExistingFile())
262268
Expect(keyPath).To(BeAnExistingFile())
263269

270+
certInfo, err := os.Stat(certPath)
271+
Expect(err).NotTo(HaveOccurred())
272+
Expect(os.FileMode(0750)).To(Equal(certInfo.Mode().Perm()))
273+
274+
keyInfo, err := os.Stat(keyPath)
275+
Expect(err).NotTo(HaveOccurred())
276+
Expect(os.FileMode(0750)).To(Equal(keyInfo.Mode().Perm()))
277+
264278
certData, certErr := os.ReadFile(certPath)
265279
Expect(certErr).NotTo(HaveOccurred())
266280
Expect(string(certData)).To(Equal(cfg.FrontendTLSJob[i].CertChain))
@@ -272,16 +286,16 @@ var _ = Describe("Config", Serial, func() {
272286
})
273287
})
274288

275-
Context("with invalid cert and key", func() {
289+
Context("with valid cert (having san and dnsnames) and key and enableCertCreation set to false", func() {
276290
BeforeEach(func() {
277-
cfg, err = config.New("fixtures/valid_frontend_cert.yml", true)
291+
cfg, err = config.New("fixtures/valid_frontend_cert.yml", false)
278292
})
279293

280294
It("loads config without error", func() {
281295
Expect(err).NotTo(HaveOccurred())
282296
})
283297

284-
It("adds the certs and keys to the expected directories", func() {
298+
It("updates the config with the expected directories", func() {
285299
Expect(err).NotTo(HaveOccurred())
286300
Expect(cfg.FrontendTLS).To(HaveLen(2))
287301

@@ -296,51 +310,53 @@ var _ = Describe("Config", Serial, func() {
296310
}))
297311
})
298312

299-
It("writes the correct cert and key files with correct permissions", func() {
300-
for i, name := range []string{"prod", "dev"} {
313+
It("does not write the cert and key files as enableCertCreation is false", func() {
314+
for _, name := range []string{"prod", "dev"} {
301315
certPath := filepath.Join(tmpDir, name, name+".pem")
302316
keyPath := filepath.Join(tmpDir, name, name+".pem.key")
303317

304-
Expect(certPath).To(BeAnExistingFile())
305-
Expect(keyPath).To(BeAnExistingFile())
306-
307-
certData, certErr := os.ReadFile(certPath)
308-
Expect(certErr).NotTo(HaveOccurred())
309-
Expect(string(certData)).To(Equal(cfg.FrontendTLSJob[i].CertChain))
310-
311-
keyData, keyErr := os.ReadFile(keyPath)
312-
Expect(keyErr).NotTo(HaveOccurred())
313-
Expect(string(keyData)).To(Equal(cfg.FrontendTLSJob[i].PrivateKey))
314-
315-
certInfo, err := os.Stat(certPath)
316-
Expect(err).NotTo(HaveOccurred())
317-
Expect(os.FileMode(0750)).To(Equal(certInfo.Mode().Perm()))
318-
319-
keyInfo, err := os.Stat(keyPath)
320-
Expect(err).NotTo(HaveOccurred())
321-
Expect(os.FileMode(0750)).To(Equal(keyInfo.Mode().Perm()))
318+
Expect(certPath).ToNot(BeAnExistingFile())
319+
Expect(keyPath).ToNot(BeAnExistingFile())
322320
}
323321
})
324322
})
325323

326324
Context("with invalid frontend_tls config", func() {
327-
It("should fail if cert_chain is missing SAN information", func() {
328-
_, err := config.New("fixtures/frontend_cert_without_san.yml", true)
325+
It("should fail if cert is empty", func() {
326+
_, err := config.New("fixtures/no_frontend_certs.yml", false)
327+
Expect(err).To(HaveOccurred())
328+
Expect(err.Error()).To(Equal("frontend_tls[0]: empty cert_chain"))
329+
})
330+
331+
It("should fail if key is missing", func() {
332+
_, err := config.New("fixtures/invalid_frontend_key.yml", false)
329333
Expect(err).To(HaveOccurred())
330-
Expect(err.Error()).To(Equal("frontend_tls[0].cert_chain must include a subjectAltName extension"))
334+
Expect(err.Error()).To(Equal("frontend_tls[0]: empty private_key"))
331335
})
332-
It("should fail if certs or keys are empty", func() {
333-
_, err := config.New("fixtures/no_frontend_certs.yml", true)
336+
337+
It("should fail if name is missing", func() {
338+
_, err := config.New("fixtures/invalid_frontend_name.yml", false)
334339
Expect(err).To(HaveOccurred())
335-
Expect(err.Error()).To(Equal("frontend_tls[0] must include name, cert_chain, and private_key"))
340+
Expect(err.Error()).To(Equal("frontend_tls[0]: empty name"))
336341
})
342+
337343
It("should fail if cert is invalid", func() {
338-
_, err := config.New("fixtures/invalid_frontend_certs.yml", true)
344+
_, err := config.New("fixtures/invalid_frontend_certs.yml", false)
345+
Expect(err).To(HaveOccurred())
346+
Expect(err.Error()).To(Equal("frontend_tls[0]: failed to parse PEM block"))
347+
})
348+
349+
It("should fail if cert_chain is missing SAN information and DNSnames", func() {
350+
_, err := config.New("fixtures/frontend_cert_without_san.yml", false)
339351
Expect(err).To(HaveOccurred())
340-
Expect(err.Error()).To(Equal("failed to parse PEM block"))
352+
Expect(err.Error()).To(Equal("frontend_tls[0]: cert_chain must include either a subjectAltName extension or DNSNames"))
341353
})
342354

355+
It("should fail if cert_chain is invalid", func() {
356+
_, err := config.New("fixtures/invalid_frontend_cert_certchain.yml", false)
357+
Expect(err).To(HaveOccurred())
358+
Expect(err.Error()).To(Equal("frontend_tls[0]: could not parse certificate: x509: malformed certificate"))
359+
})
343360
})
344361
})
345-
346362
})

0 commit comments

Comments
 (0)