Skip to content
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

Allow certs to be include in an image #97

Merged
merged 3 commits into from
Apr 12, 2022
Merged
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
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# `gcr.io/paketo-buildpacks/ca-certificates`

The Paketo CA Certificates Buildpack is a Cloud Native Buildpack that adds CA certificates to the system truststore at build and runtime.

## Behavior

This buildpack always participates.

The buildpack will do the following:

* At build time:
* Contributes the `ca-cert-helper` to the application image.
* If `$BP_RUNTIME_CERT_BINDING_DISABLED` is false, it contributes the `ca-cert-helper` to the application image. Default is false.
* If one or more bindings with `type` of `ca-certificates` exists, it adds all CA certificates from the bindings to the system truststore.
* If another buildpack provides an entry of `type` `ca-certificates` in the build plan with `metadata.paths` containing an array of certificate paths, it adds all CA certificates from the given paths to the system truststore.
* If `$BP_EMBED_CERTS` is true, it includes the layer with all of the CA certificates into the application image.
* At runtime:
* If one or more bindings with `type` of `ca-certificates` exists, the `ca-cert-helper` adds all CA certificates from the bindings to the system truststore.

Expand All @@ -22,19 +25,25 @@ The buildpack configures trusted certs at both build and runtime by:
To learn about the conventional meaning of `SSL_CERT_DIR` and `SSL_CERT_FILE` environment variables see the OpenSSL documentation for [SSL_CTX_load_verify_locations][s]. This buildpack may not work with tools that do not respect these environment variables.

## Bindings

The buildpack optionally accepts the following bindings:

### Type: `ca-certificates`
|Key | Value | Description
|----------------------|---------|------------
|`<certificate-name>` | `<certificate>` | CA certificate to trust. Should contain exactly one PEM encoded certificate.

| Key | Value | Description |
| -------------------- | --------------- | ---------------------------------------------------------------------------- |
| `<certificate-name>` | `<certificate>` | CA certificate to trust. Should contain exactly one PEM encoded certificate. |

## Configuration
| Environment Variable | Description
| -------------------- | -----------
| `$BP_ENABLE_RUNTIME_CERT_BINDING` | Enable/disable the ability to set certificates at runtime via the certificate helper layer. Default is true.

| Environment Variable | Description |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `$BP_EMBED_CERTS` | Embed all CA certificate bindings present at buildtime into the application image. This removes the need to have any embedded CA certificate bindings present at runtime. Default is false. |
| `$BP_RUNTIME_CERT_BINDING_DISABLED` | Disable the helper that adds certificates at runtime. This means any provided CA certificates will not be included. Default to false, which means certificates are loaded by default. |
| `$BP_ENABLE_RUNTIME_CERT_BINDING` | Deprecated in favour of `$BP_RUNTIME_CERT_BINDING_DISABLED`. Enable/disable the ability to set certificates at runtime via the certificate helper layer. Default is true. |

## License

This buildpack is released under version 2.0 of the [Apache License][a].

[a]: http://www.apache.org/licenses/LICENSE-2.0
Expand Down
14 changes: 13 additions & 1 deletion buildpack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,19 @@ api = "0.7"

[[metatdata.configurations]]
build = true
description = "Enable/disable certificate helper layer to add certs at runtime"
default = false
description = "Disable certificate helper layer to add certs at runtime"
name = "BP_RUNTIME_CERT_BINDING_DISABLED"

[[metatdata.configurations]]
build = true
default = false
description = "Embed certificates into the image"
name = "BP_EMBED_CERTS"

[[metatdata.configurations]]
build = true
description = "Deprecated: Enable/disable certificate helper layer to add certs at runtime"
name = "BP_ENABLE_RUNTIME_CERT_BINDING"

[[stacks]]
Expand Down
2 changes: 1 addition & 1 deletion cacerts/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (
func getsCertsFromBindings(binds libcnb.Bindings) []string {
var paths []string
for _, bind := range bindings.Resolve(binds, bindings.OfType(BindingType)) {
for k, _ := range bind.Secret {
for k := range bind.Secret {
if path, ok := bind.SecretFilePath(k); ok {
paths = append(paths, path)
}
Expand Down
6 changes: 3 additions & 3 deletions cacerts/build.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2020 the original author or authors.
* Copyright 2018-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -42,7 +42,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {

b.Logger.Title(context.Buildpack)

_, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger)
cr, err := libpak.NewConfigurationResolver(context.Buildpack, &b.Logger)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err)
}
Expand Down Expand Up @@ -78,7 +78,7 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {

if len(certPaths) > 0 {
sort.Strings(certPaths)
layer := NewTrustedCACerts(certPaths)
layer := NewTrustedCACerts(certPaths, cr.ResolveBool("BP_EMBED_CERTS"))
layer.Logger = b.Logger
result.Layers = append(result.Layers, layer)
}
Expand Down
8 changes: 4 additions & 4 deletions cacerts/certs.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2020 the original author or authors.
* Copyright 2018-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -85,7 +85,7 @@ func decodeOneCert(raw []byte) (*x509.Certificate, error) {
if block == nil {
return nil, errors.New("failed find PEM data")
}
extra, rest := pem.Decode(rest)
extra, _ := pem.Decode(rest)
if extra != nil {
return nil, errors.New("found multiple PEM blocks, expected exactly one")
}
Expand Down Expand Up @@ -164,8 +164,8 @@ func CanonicalName(name []byte) ([]byte, error) {
//
// This is a reimplementation of the asn1_string_canon in openssl
func CanonicalString(s string) string {
s = strings.TrimLeft(s, " \f\t\n\n\v")
s = strings.TrimRight(s, " \f\t\n\n\v")
s = strings.TrimLeft(s, " \f\t\n\v")
s = strings.TrimRight(s, " \f\t\n\v")
s = strings.ToLower(s)
return string(regexp.MustCompile(`[[:space:]]+`).ReplaceAll([]byte(s), []byte(" ")))
}
13 changes: 10 additions & 3 deletions cacerts/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
// PlanEntryCACerts if present in the build plan indicates that certificates should be added to the
// truststore at build time.
PlanEntryCACerts = "ca-certificates"

// PlanEntryCACertsHelper if present in the build plan indicates the the ca-cert-helper binary should be
// contributed to the app image.
PlanEntryCACertsHelper = "ca-certificates-helper"
Expand All @@ -40,7 +41,7 @@ const (
// plan entry metadata.
//
// To prevent default detection, users can set the
// BP_ENABLE_RUNTIME_CERT_BINDING environment variable to "false" at
// BP_RUNTIME_CERT_BINDING_DISABLED environment variable to "true" at
// build-time. This will disable the helper layer, and the buildpack will only
// detect if there is no ca-certificates binding present at build-time.
func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) {
Expand Down Expand Up @@ -71,7 +72,7 @@ func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error
},
}

// If BP_ENABLE_RUNTIME_CERT_BINDING = false, do not enable helper layer.
// If BP_RUNTIME_CERT_BINDING_DISABLED = true, do not enable helper layer.
cr, err := libpak.NewConfigurationResolver(context.Buildpack, nil)
if err != nil {
return libcnb.DetectResult{}, fmt.Errorf("unable to create configuration resolver\n%w", err)
Expand Down Expand Up @@ -99,7 +100,12 @@ func (d Detect) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error
}

func (d Detect) runtimeCertBindingEnabled(cr libpak.ConfigurationResolver) (bool, error) {
if val, ok := cr.Resolve("BP_ENABLE_RUNTIME_CERT_BINDING"); ok {
if cr.ResolveBool("BP_RUNTIME_CERT_BINDING_DISABLED") {
return false, nil
}

// Deprecated: Remove support for this environment variable in the future
if val, isSet := cr.Resolve("BP_ENABLE_RUNTIME_CERT_BINDING"); isSet {
enable, err := strconv.ParseBool(val)
if err != nil {
return false, fmt.Errorf(
Expand All @@ -110,5 +116,6 @@ func (d Detect) runtimeCertBindingEnabled(cr libpak.ConfigurationResolver) (bool
}
return enable, nil
}

return true, nil
}
70 changes: 70 additions & 0 deletions cacerts/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,47 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
}))
})
})

context("BP_RUNTIME_CERT_BINDING_DISABLED is set to true", func() {
var result libcnb.DetectResult
it.Before(func() {
os.Setenv("BP_RUNTIME_CERT_BINDING_DISABLED", "true")

var err error
result, err = detect.Detect(ctx)
Expect(err).NotTo(HaveOccurred())
})

it.After(func() {
os.Unsetenv("BP_RUNTIME_CERT_BINDING_DISABLED")
})

it("detect passes", func() {
Expect(result.Pass).To(BeTrue())
})

it("there is no ca-certificates-helper plan entry", func() {
Expect(len(result.Plans)).To(Equal(1))

Expect(result.Plans[0]).To(Equal(libcnb.BuildPlan{
Provides: []libcnb.BuildPlanProvide{
{Name: cacerts.PlanEntryCACerts},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: cacerts.PlanEntryCACerts,
Metadata: map[string]interface{}{
"paths": []string{
filepath.Join("other-path", "cert3.pem"),
filepath.Join("some-path", "cert1.pem"),
filepath.Join("some-path", "cert2.pem"),
},
},
},
},
}))
})
})
})

context("Binding does not exist with type ca-certificates", func() {
Expand Down Expand Up @@ -199,5 +240,34 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
}))
})
})

context("BP_RUNTIME_CERT_BINDING_DISABLED is set to true", func() {
var result libcnb.DetectResult
it.Before(func() {
os.Setenv("BP_RUNTIME_CERT_BINDING_DISABLED", "true")

var err error
result, err = detect.Detect(ctx)
Expect(err).NotTo(HaveOccurred())
})

it.After(func() {
os.Unsetenv("BP_RUNTIME_CERT_BINDING_DISABLED")
})

it("detect passes", func() {
Expect(result.Pass).To(BeTrue())
})

it("first plan does not require ca-certificates", func() {
Expect(len(result.Plans)).To(Equal(1))
Expect(result.Plans[0]).To(Equal(libcnb.BuildPlan{
Provides: []libcnb.BuildPlanProvide{
{Name: cacerts.PlanEntryCACerts},
},
Requires: []libcnb.BuildPlanRequire{},
}))
})
})
})
}
65 changes: 56 additions & 9 deletions cacerts/trusted_ca_certs.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2020 the original author or authors.
* Copyright 2018-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,26 +25,35 @@ import (

"github.com/paketo-buildpacks/libpak"
"github.com/paketo-buildpacks/libpak/bard"
"github.com/paketo-buildpacks/libpak/sherpa"
)

const (
CACertsDir = "ca-certificates"
EmbedCertsDir = "embedded-certs"
)

type TrustedCACerts struct {
CertPaths []string
LayerContributor libpak.LayerContributor
EmbeddedCerts bool
GenerateHashLinks func(dir string, certPaths []string) error
LayerContributor libpak.LayerContributor
Logger bard.Logger
}

func NewTrustedCACerts(paths []string) *TrustedCACerts {
func NewTrustedCACerts(paths []string, embedCACerts bool) *TrustedCACerts {
return &TrustedCACerts{
CertPaths: paths,
CertPaths: paths,
GenerateHashLinks: GenerateHashLinks,
EmbeddedCerts: embedCACerts,
LayerContributor: libpak.NewLayerContributor(
"CA Certificates",
map[string]interface{}{},
libcnb.LayerTypes{
Build: true,
Build: true,
Launch: embedCACerts,
},
),
GenerateHashLinks: GenerateHashLinks,
}
}

Expand All @@ -53,15 +62,25 @@ func (l TrustedCACerts) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
l.LayerContributor.Logger = l.Logger

return l.LayerContributor.Contribute(layer, func() (libcnb.Layer, error) {
layer.BuildEnvironment = libcnb.Environment{}
certsDir := filepath.Join(layer.Path, CACertsDir)

certsDir := filepath.Join(layer.Path, "ca-certificates")
if err := os.Mkdir(certsDir, 0777); err != nil {
if err := os.Mkdir(certsDir, 0755); err != nil {
return libcnb.Layer{}, fmt.Errorf("failed to create directory %q\n%w", certsDir, err)
}

if l.EmbeddedCerts {
if err := l.ContributeEmbedCACerts(layer); err != nil {
return libcnb.Layer{}, err
}

layer.LaunchEnvironment.Append(EnvCAPath, string(filepath.ListSeparator), certsDir)
layer.LaunchEnvironment.Default(EnvCAFile, DefaultCAFile)
}

if err := l.GenerateHashLinks(certsDir, l.CertPaths); err != nil {
return libcnb.Layer{}, fmt.Errorf("failed to generate CA certificate symlinks\n%w", err)
}

l.Logger.Bodyf("Added %d additional CA certificate(s) to system truststore", len(l.CertPaths))

layer.BuildEnvironment.Append(
Expand All @@ -70,10 +89,38 @@ func (l TrustedCACerts) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
certsDir,
)
layer.BuildEnvironment.Default(EnvCAFile, DefaultCAFile)

return layer, nil
})
}

func (l *TrustedCACerts) ContributeEmbedCACerts(layer libcnb.Layer) error {
l.Logger.Body("Embedding CA certificate(s)")

embeddedDir := filepath.Join(layer.Path, EmbedCertsDir)
if err := os.Mkdir(embeddedDir, 0755); err != nil {
return fmt.Errorf("failed to create directory %q\n%w", embeddedDir, err)
}

newCertPaths := []string{}
for _, certPath := range l.CertPaths {
certFile, err := os.Open(certPath)
if err != nil {
return fmt.Errorf("failed to open cert %q\n%w", certPath, err)
}

dest := filepath.Join(embeddedDir, filepath.Base(certPath))
err = sherpa.CopyFile(certFile, dest)
if err != nil {
return fmt.Errorf("failed to copy cert %q to %q\n%w", certPath, dest, err)
}
newCertPaths = append(newCertPaths, dest)
}
l.CertPaths = newCertPaths

return nil
}

func (TrustedCACerts) Name() string {
return "ca-certificates"
}
Loading