Skip to content

Commit b81b823

Browse files
feat: add --with-ssl-cert-file option to build command
This makes the root CAs from the host available to containers during a build. Co-authored-by: Tom Sweeney <[email protected]> Signed-off-by: Adam Eijdenberg <[email protected]>
1 parent 243d897 commit b81b823

13 files changed

+264
-49
lines changed

define/build.go

+2
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ type CommonBuildOptions struct {
113113
OCIHooksDir []string
114114
// Paths to unmask
115115
Unmasks []string
116+
// WithSSLCertFile specifies whether the host root CAs should be ephemerally mounted within the container
117+
WithSSLCertFile bool
116118
}
117119

118120
// BuildOptions can be used to alter how an image is built.

docs/buildah-build.1.md

+5
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,11 @@ will convert /foo into a `shared` mount point. The propagation properties of th
12321232
mount can be changed directly. For instance if `/` is the source mount for
12331233
`/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount.
12341234

1235+
**--with-ssl-cert-file**
1236+
1237+
If set, bind mount the host CA cert file (override location by setting `SSL_CERT_FILE`) within the RUN containers,
1238+
and set the environment variable `SSL_CERT_FILE` to point to it.
1239+
12351240
## BUILD TIME VARIABLES
12361241

12371242
The ENV instruction in a Containerfile can be used to define variable values. When the image

docs/buildah-from.1.md

+5
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,11 @@ will convert /foo into a `shared` mount point. The propagation properties of th
690690
mount can be changed directly. For instance if `/` is the source mount for
691691
`/foo`, then use `mount --make-shared /` to convert `/` into a `shared` mount.
692692

693+
**--with-ssl-cert-file**
694+
695+
If set, bind mount the host CA cert file (override location by setting `SSL_CERT_FILE`) within the RUN containers,
696+
and set the environment variable `SSL_CERT_FILE` to point to it.
697+
693698
## EXAMPLE
694699

695700
buildah from --pull imagename

pkg/cli/common.go

+28-26
Original file line numberDiff line numberDiff line change
@@ -125,32 +125,33 @@ type BudResults struct {
125125
// FromAndBugResults represents the results for common flags
126126
// in build and from
127127
type FromAndBudResults struct {
128-
AddHost []string
129-
BlobCache string
130-
CapAdd []string
131-
CapDrop []string
132-
CDIConfigDir string
133-
CgroupParent string
134-
CPUPeriod uint64
135-
CPUQuota int64
136-
CPUSetCPUs string
137-
CPUSetMems string
138-
CPUShares uint64
139-
DecryptionKeys []string
140-
Devices []string
141-
DNSSearch []string
142-
DNSServers []string
143-
DNSOptions []string
144-
HTTPProxy bool
145-
Isolation string
146-
Memory string
147-
MemorySwap string
148-
Retry int
149-
RetryDelay string
150-
SecurityOpt []string
151-
ShmSize string
152-
Ulimit []string
153-
Volumes []string
128+
AddHost []string
129+
BlobCache string
130+
CapAdd []string
131+
CapDrop []string
132+
CDIConfigDir string
133+
CgroupParent string
134+
CPUPeriod uint64
135+
CPUQuota int64
136+
CPUSetCPUs string
137+
CPUSetMems string
138+
CPUShares uint64
139+
DecryptionKeys []string
140+
Devices []string
141+
DNSSearch []string
142+
DNSServers []string
143+
DNSOptions []string
144+
HTTPProxy bool
145+
Isolation string
146+
Memory string
147+
MemorySwap string
148+
Retry int
149+
RetryDelay string
150+
SecurityOpt []string
151+
ShmSize string
152+
Ulimit []string
153+
Volumes []string
154+
WithSSLCertFile bool
154155
}
155156

156157
// GetUserNSFlags returns the common flags for usernamespace
@@ -409,6 +410,7 @@ func GetFromAndBudFlags(flags *FromAndBudResults, usernsResults *UserNSResults,
409410
fs.String("variant", "", "override the `variant` of the specified image")
410411
fs.StringArrayVar(&flags.SecurityOpt, "security-opt", []string{}, "security options (default [])")
411412
fs.StringVar(&flags.ShmSize, "shm-size", defaultContainerConfig.Containers.ShmSize, "size of '/dev/shm'. The format is `<number><unit>`.")
413+
fs.BoolVar(&flags.WithSSLCertFile, "with-ssl-cert-file", false, "mount host root CA bundle within container during build, and set SSL_CERT_FILE to point to that bundle")
412414
fs.StringSliceVar(&flags.Ulimit, "ulimit", defaultContainerConfig.Containers.DefaultUlimits.Get(), "ulimit options")
413415
fs.StringArrayVarP(&flags.Volumes, "volume", "v", defaultContainerConfig.Volumes(), "bind mount a volume into the container")
414416

pkg/parse/parse.go

+25-23
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ func CommonBuildOptionsFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name
162162
cpuQuota, _ := flags.GetInt64("cpu-quota")
163163
cpuShares, _ := flags.GetUint64("cpu-shares")
164164
httpProxy, _ := flags.GetBool("http-proxy")
165+
withSSLCertFile, _ := flags.GetBool("with-ssl-cert-file")
165166
identityLabel, _ := flags.GetBool("identity-label")
166167
omitHistory, _ := flags.GetBool("omit-history")
167168

@@ -175,29 +176,30 @@ func CommonBuildOptionsFromFlagSet(flags *pflag.FlagSet, findFlagFunc func(name
175176
ociHooks, _ := flags.GetStringArray("hooks-dir")
176177

177178
commonOpts := &define.CommonBuildOptions{
178-
AddHost: addHost,
179-
CPUPeriod: cpuPeriod,
180-
CPUQuota: cpuQuota,
181-
CPUSetCPUs: findFlagFunc("cpuset-cpus").Value.String(),
182-
CPUSetMems: findFlagFunc("cpuset-mems").Value.String(),
183-
CPUShares: cpuShares,
184-
CgroupParent: findFlagFunc("cgroup-parent").Value.String(),
185-
DNSOptions: dnsOptions,
186-
DNSSearch: dnsSearch,
187-
DNSServers: dnsServers,
188-
HTTPProxy: httpProxy,
189-
IdentityLabel: types.NewOptionalBool(identityLabel),
190-
Memory: memoryLimit,
191-
MemorySwap: memorySwap,
192-
NoHostname: noHostname,
193-
NoHosts: noHosts,
194-
OmitHistory: omitHistory,
195-
ShmSize: findFlagFunc("shm-size").Value.String(),
196-
Ulimit: ulimit,
197-
Volumes: volumes,
198-
Secrets: secrets,
199-
SSHSources: sshsources,
200-
OCIHooksDir: ociHooks,
179+
AddHost: addHost,
180+
CPUPeriod: cpuPeriod,
181+
CPUQuota: cpuQuota,
182+
CPUSetCPUs: findFlagFunc("cpuset-cpus").Value.String(),
183+
CPUSetMems: findFlagFunc("cpuset-mems").Value.String(),
184+
CPUShares: cpuShares,
185+
CgroupParent: findFlagFunc("cgroup-parent").Value.String(),
186+
DNSOptions: dnsOptions,
187+
DNSSearch: dnsSearch,
188+
DNSServers: dnsServers,
189+
HTTPProxy: httpProxy,
190+
IdentityLabel: types.NewOptionalBool(identityLabel),
191+
Memory: memoryLimit,
192+
MemorySwap: memorySwap,
193+
NoHostname: noHostname,
194+
NoHosts: noHosts,
195+
OmitHistory: omitHistory,
196+
ShmSize: findFlagFunc("shm-size").Value.String(),
197+
Ulimit: ulimit,
198+
Volumes: volumes,
199+
Secrets: secrets,
200+
SSHSources: sshsources,
201+
WithSSLCertFile: withSSLCertFile,
202+
OCIHooksDir: ociHooks,
201203
}
202204
securityOpts, _ := flags.GetStringArray("security-opt")
203205
if err := parseSecurityOpts(securityOpts, commonOpts); err != nil {

run_common.go

+48
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,54 @@ func (b *Builder) generateHostname(rdir, hostname string, chownOpts *idtools.IDP
208208
return cfile, nil
209209
}
210210

211+
// createSSLCertFile creates a containers CA cert file
212+
func (b *Builder) createSSLCertFile(rdir, containerPath string, chownOpts *idtools.IDPair) (_ string, retErr error) {
213+
resolvedSSLCertPath, err := util.ResolveRootCACertFile()
214+
if err != nil {
215+
return "", fmt.Errorf("error resolving cert file on host: %w", err)
216+
}
217+
218+
inCertFile, err := os.Open(resolvedSSLCertPath)
219+
if err != nil {
220+
return "", fmt.Errorf("error opening ssl cert file on host: %w", err)
221+
}
222+
defer func() {
223+
if err := inCertFile.Close(); err != nil && retErr == nil {
224+
retErr = fmt.Errorf("error closing ssl cert file on host: %w", err)
225+
}
226+
}()
227+
228+
cfile := filepath.Join(rdir, filepath.Base(containerPath))
229+
outCertFile, err := os.Create(cfile)
230+
if err != nil {
231+
return "", fmt.Errorf("error opening ssl cert file for container: %w", err)
232+
}
233+
defer func() {
234+
if err := outCertFile.Close(); err != nil && retErr == nil {
235+
retErr = fmt.Errorf("error closing ssl cert file for container: %w", err)
236+
}
237+
}()
238+
239+
if _, err = io.Copy(outCertFile, inCertFile); err != nil {
240+
return "", fmt.Errorf("error copying cert file for container: %w", err)
241+
}
242+
243+
uid := 0
244+
gid := 0
245+
if chownOpts != nil {
246+
uid = chownOpts.UID
247+
gid = chownOpts.GID
248+
}
249+
if err = os.Chown(cfile, uid, gid); err != nil {
250+
return "", err
251+
}
252+
if err = relabel(cfile, b.MountLabel, false); err != nil {
253+
return "", err
254+
}
255+
256+
return cfile, nil
257+
}
258+
211259
func setupTerminal(g *generate.Generator, terminalPolicy TerminalPolicy, terminalSize *specs.Box) {
212260
switch terminalPolicy {
213261
case DefaultTerminal:

run_linux.go

+21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ package buildah
44

55
import (
66
"context"
7+
"crypto/rand"
8+
"encoding/hex"
79
"errors"
810
"fmt"
911
"os"
@@ -367,6 +369,17 @@ func (b *Builder) Run(command []string, options RunOptions) error {
367369

368370
g.SetProcessApparmorProfile(b.CommonBuildOpts.ApparmorProfile)
369371

372+
var optionalContainerCaBundlePath string
373+
if b.CommonBuildOpts.WithSSLCertFile {
374+
// we populate this later in this function, but generate the filename
375+
// now so we can set it in the env. We make it random so that this path
376+
// is not relied on, and instead the env var is used
377+
var b [8]byte
378+
_, _ = rand.Read(b[:]) // documented to never return an err
379+
optionalContainerCaBundlePath = "/cabundle-" + hex.EncodeToString(b[:])
380+
g.AddProcessEnv("SSL_CERT_FILE", optionalContainerCaBundlePath)
381+
}
382+
370383
// Now grab the spec from the generator. Set the generator to nil so that future contributors
371384
// will quickly be able to tell that they're supposed to be modifying the spec directly from here.
372385
spec := g.Config
@@ -503,6 +516,14 @@ rootless=%d
503516
bindFiles["/run/.containerenv"] = containerenvPath
504517
}
505518

519+
if b.CommonBuildOpts.WithSSLCertFile {
520+
resolvedSSLCertPath, err := b.createSSLCertFile(path, optionalContainerCaBundlePath, rootIDPair)
521+
if err != nil {
522+
return err
523+
}
524+
bindFiles[optionalContainerCaBundlePath] = resolvedSSLCertPath
525+
}
526+
506527
// Setup OCI hooks
507528
_, err = b.setupOCIHooks(spec, (len(options.Mounts) > 0 || len(volumes) > 0))
508529
if err != nil {

tests/bud.bats

+16
Original file line numberDiff line numberDiff line change
@@ -7442,3 +7442,19 @@ EOF
74427442
# should be the same
74437443
diff "${outpath}.b" "${outpath}.c"
74447444
}
7445+
7446+
@test "build-with-ssl-cert-file" {
7447+
echo "foofoofoofoo" > "${TEST_SCRATCH_DIR}/foocafile"
7448+
echo "barbarbarbar" > "${TEST_SCRATCH_DIR}/barcafile"
7449+
7450+
# use foocafile, and ensure that "foo" is output
7451+
SSL_CERT_FILE="${TEST_SCRATCH_DIR}/foocafile" run_buildah build -f <(echo 'FROM alpine'; echo 'RUN cat "${SSL_CERT_FILE}"') --tag="oci-archive:${TEST_SCRATCH_DIR}/foo.tar" --timestamp 0 --with-ssl-cert-file
7452+
expect_output --substring "foofoofoofoo"
7453+
7454+
# use barcafile, and ensure that "bar" is output
7455+
SSL_CERT_FILE="${TEST_SCRATCH_DIR}/barcafile" run_buildah build -f <(echo 'FROM alpine'; echo 'RUN cat "${SSL_CERT_FILE}"') --tag="oci-archive:${TEST_SCRATCH_DIR}/bar.tar" --timestamp 0 --with-ssl-cert-file
7456+
expect_output --substring "barbarbarbar"
7457+
7458+
# however regardless the produced images from both above should be identical as this is not persisted to image
7459+
diff "${TEST_SCRATCH_DIR}/foo.tar" "${TEST_SCRATCH_DIR}/bar.tar"
7460+
}

util/x509.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package util
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
)
8+
9+
func ResolveRootCACertFile() (string, error) {
10+
if rv, ok := os.LookupEnv("SSL_CERT_FILE"); ok {
11+
return rv, nil
12+
}
13+
for _, potFile := range certFiles {
14+
if _, err := os.Stat(potFile); err != nil {
15+
if os.IsNotExist(err) {
16+
continue
17+
}
18+
return "", fmt.Errorf("unexpected error resolving cert file: %w", err)
19+
}
20+
return potFile, nil
21+
}
22+
return "", errors.New("unable to resolve host cert file. Consider setting SSL_CERT_FILE environment variable")
23+
}

util/x509_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package util
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestResolveRootCACertFileWitEnvSet(t *testing.T) {
10+
t.Setenv("SSL_CERT_FILE", "/path/to/file")
11+
12+
path, err := ResolveRootCACertFile()
13+
assert.Nil(t, err)
14+
15+
assert.Equal(t, "/path/to/file", path)
16+
}

util/x509_unix.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//go:build !windows
2+
3+
package util
4+
5+
// Source: https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/crypto/x509/root_linux.go
6+
7+
/*
8+
Copyright 2009 The Go Authors.
9+
10+
Redistribution and use in source and binary forms, with or without
11+
modification, are permitted provided that the following conditions are
12+
met:
13+
14+
* Redistributions of source code must retain the above copyright
15+
notice, this list of conditions and the following disclaimer.
16+
* Redistributions in binary form must reproduce the above
17+
copyright notice, this list of conditions and the following disclaimer
18+
in the documentation and/or other materials provided with the
19+
distribution.
20+
* Neither the name of Google LLC nor the names of its
21+
contributors may be used to endorse or promote products derived from
22+
this software without specific prior written permission.
23+
24+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35+
*/
36+
37+
// Possible certificate files; stop after finding one.
38+
var certFiles = []string{
39+
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
40+
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6
41+
"/etc/ssl/ca-bundle.pem", // OpenSUSE
42+
"/etc/pki/tls/cacert.pem", // OpenELEC
43+
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
44+
"/etc/ssl/cert.pem", // Alpine Linux
45+
}

util/x509_unix_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//go:build !windows
2+
3+
package util
4+
5+
import (
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestResolveRootCACertFileWithNoEnvSetUnix(t *testing.T) {
13+
// calling t.SetEnv has the side-effect of restoring it to the previous
14+
// value after the test (or leaving it unset).
15+
// We call this before unsetting it so that we benefit from that cleanup
16+
t.Setenv("SSL_CERT_FILE", "bogusval")
17+
18+
// now unset it (t.Unsetenv doesn't exist or we'd use that.)
19+
assert.Nil(t, os.Unsetenv("SSL_CERT_FILE"))
20+
path, err := ResolveRootCACertFile()
21+
assert.Nil(t, err)
22+
23+
// if our test env doesn't have any of the default locations set,
24+
// then we need to fix our lists
25+
assert.NotEmpty(t, path)
26+
}

util/x509_windows.go

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package util
2+
3+
// not implemented
4+
var certFiles []string

0 commit comments

Comments
 (0)