Repository: coder/envbuilder
Pull request #391 (2024-10-16 17:36:27)

feat(envbuilder.go): add support for build secrets
This PR adopts support for build secrets from our recent updates to Kaniko to provide a safer alternative to this: https://github.com/coder/envbuilder/issues/93

SasSwart: > Before building the image using Kaniko, set all the environment variables from above without ENVBUILDER_SECRET prefix.

I did attempt this, but wasn't able to get it to work. each RUN directive starts with a mostly blank environment as far as I can tell. I will revisit this approach though. Perhaps I can get it to work upon closer inspection.

Out of interest, why do you prefer the ENV approach as opposed to persisting them to disk?
johnstcn: > Out of interest, why do you prefer the ENV approach as opposed to persisting them to disk?

I actually don't have a good answer to this apart from 'environment variables are easy and it seems to be the method of least surprise'.

However, I can see some folks being wary of passing build-time secrets via environment variable, as someone with access to either the container spec or some 'back-door' into the container (e.g. via `kubectl exec`) would then potentially end up being able to read the env var.
SasSwart: @johnstcn The updated PR should demonstrate a more flexible approach in line with docker standards. The user can now choose whether they want an ENV, or a file. They can also specify where that file should be.
SasSwart: @mafredri I've tended to all review notes. Thanks for all the feedback, it was useful!
Tests should be in a much better space now. Please have another look?

Patch:
diff --git a/devcontainer/devcontainer.go b/devcontainer/devcontainer.go
index 6135c0e..0bf7cc3 100644
--- a/devcontainer/devcontainer.go
+++ b/devcontainer/devcontainer.go
@@ -400,11 +400,11 @@ func ImageFromDockerfile(dockerfileContent string) (name.Reference, error) {
 			arg = strings.TrimSpace(arg)
 			if strings.Contains(arg, "=") {
 				parts := strings.SplitN(arg, "=", 2)
-				key, err := lexer.ProcessWord(parts[0], args)
+				key, _, err := lexer.ProcessWord(parts[0], shell.EnvsFromSlice(args))
 				if err != nil {
 					return nil, fmt.Errorf("processing %q: %w", line, err)
 				}
-				val, err := lexer.ProcessWord(parts[1], args)
+				val, _, err := lexer.ProcessWord(parts[1], shell.EnvsFromSlice(args))
 				if err != nil {
 					return nil, fmt.Errorf("processing %q: %w", line, err)
 				}
@@ -421,7 +421,7 @@ func ImageFromDockerfile(dockerfileContent string) (name.Reference, error) {
 	if imageRef == "" {
 		return nil, fmt.Errorf("no FROM directive found")
 	}
-	imageRef, err := lexer.ProcessWord(imageRef, args)
+	imageRef, _, err := lexer.ProcessWord(imageRef, shell.EnvsFromSlice(args))
 	if err != nil {
 		return nil, fmt.Errorf("processing %q: %w", imageRef, err)
 	}
diff --git a/envbuilder.go b/envbuilder.go
index 295ef3f..14fd004 100644
--- a/envbuilder.go
+++ b/envbuilder.go
@@ -529,6 +529,11 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
 			if opts.CacheRepo != "" {
 				destinations = append(destinations, opts.CacheRepo)
 			}
+
+			buildSecrets := options.GetBuildSecrets(os.Environ())
+			// Ensure that build secrets do not make it into the runtime environment or the setup script:
+			options.ClearBuildSecretsFromProcessEnvironment()
+
 			kOpts := &config.KanikoOptions{
 				// Boilerplate!
 				CustomPlatform:     platforms.Format(platforms.Normalize(platforms.DefaultSpec())),
@@ -553,6 +558,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
 				},
 				ForceUnpack:       true,
 				BuildArgs:         buildParams.BuildArgs,
+				BuildSecrets:      buildSecrets,
 				CacheRepo:         opts.CacheRepo,
 				Cache:             opts.CacheRepo != "" || opts.BaseImageCacheDir != "",
 				DockerfilePath:    buildParams.DockerfilePath,
diff --git a/go.mod b/go.mod
index 9fa1d69..c0ad412 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.22.4
 
 // There are a few options we need added to Kaniko!
 // See: https://github.com/GoogleContainerTools/kaniko/compare/main...coder:kaniko:main
-replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20240925122543-caa18967f374
+replace github.com/GoogleContainerTools/kaniko => github.com/coder/kaniko v0.0.0-20241028054616-350cbb820e05
 
 // Required to import codersdk due to gvisor dependency.
 replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240702054557-aa558fbe5374
@@ -19,8 +19,8 @@ require (
 	github.com/coder/serpent v0.7.0
 	github.com/containerd/platforms v0.2.1
 	github.com/distribution/distribution/v3 v3.0.0-alpha.1
-	github.com/docker/cli v27.2.0+incompatible
-	github.com/docker/docker v26.1.5+incompatible
+	github.com/docker/cli v27.2.1+incompatible
+	github.com/docker/docker v27.3.1+incompatible
 	github.com/fatih/color v1.17.0
 	github.com/gliderlabs/ssh v0.3.7
 	github.com/go-git/go-billy/v5 v5.5.0
@@ -31,7 +31,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
 	github.com/mattn/go-isatty v0.0.20
-	github.com/moby/buildkit v0.13.1
+	github.com/moby/buildkit v0.16.0
 	github.com/otiai10/copy v1.14.0
 	github.com/prometheus/procfs v0.15.1
 	github.com/sirupsen/logrus v1.9.3
@@ -100,14 +100,12 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/charmbracelet/lipgloss v0.8.0 // indirect
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect
-	github.com/cilium/ebpf v0.12.3 // indirect
 	github.com/cloudflare/circl v1.3.7 // indirect
 	github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect
 	github.com/coder/quartz v0.1.0 // indirect
 	github.com/coder/terraform-provider-coder v0.23.0 // indirect
 	github.com/containerd/cgroups v1.1.0 // indirect
-	github.com/containerd/cgroups/v3 v3.0.2 // indirect
-	github.com/containerd/containerd v1.7.19 // indirect
+	github.com/containerd/containerd v1.7.21 // indirect
 	github.com/containerd/containerd/api v1.7.19 // indirect
 	github.com/containerd/continuity v0.4.3 // indirect
 	github.com/containerd/errdefs v0.1.0 // indirect
@@ -115,10 +113,9 @@ require (
 	github.com/containerd/log v0.1.0 // indirect
 	github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
 	github.com/containerd/ttrpc v1.2.5 // indirect
-	github.com/containerd/typeurl/v2 v2.1.1 // indirect
+	github.com/containerd/typeurl/v2 v2.2.0 // indirect
 	github.com/coreos/go-iptables v0.6.0 // indirect
 	github.com/coreos/go-oidc/v3 v3.10.0 // indirect
-	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
@@ -151,7 +148,7 @@ require (
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/btree v1.1.2 // indirect
 	github.com/google/nftables v0.2.0 // indirect
-	github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect
+	github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
 	github.com/gorilla/handlers v1.5.1 // indirect
 	github.com/gorilla/mux v1.8.1 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
@@ -164,7 +161,7 @@ require (
 	github.com/hashicorp/go-version v1.7.0 // indirect
 	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
-	github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/hashicorp/hcl/v2 v2.21.0 // indirect
 	github.com/hashicorp/logutils v1.0.0 // indirect
 	github.com/hashicorp/terraform-plugin-go v0.12.0 // indirect
@@ -202,11 +199,12 @@ require (
 	github.com/moby/patternmatcher v0.6.0 // indirect
 	github.com/moby/swarmkit/v2 v2.0.0-20230315203717-e28e8ba9bc83 // indirect
 	github.com/moby/sys/mount v0.3.3 // indirect
-	github.com/moby/sys/mountinfo v0.7.1 // indirect
+	github.com/moby/sys/mountinfo v0.7.2 // indirect
 	github.com/moby/sys/sequential v0.5.0 // indirect
-	github.com/moby/sys/signal v0.7.0 // indirect
+	github.com/moby/sys/signal v0.7.1 // indirect
 	github.com/moby/sys/symlink v0.2.0 // indirect
-	github.com/moby/sys/user v0.1.0 // indirect
+	github.com/moby/sys/user v0.3.0 // indirect
+	github.com/moby/sys/userns v0.1.0 // indirect
 	github.com/moby/term v0.5.0 // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/muesli/reflow v0.3.0 // indirect
@@ -245,6 +243,7 @@ require (
 	github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 // indirect
 	github.com/tcnksm/go-httpstat v0.2.0 // indirect
 	github.com/tinylib/msgp v1.1.8 // indirect
+	github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
 	github.com/twpayne/go-vfs/v5 v5.0.4 // indirect
 	github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect
 	github.com/valyala/fasthttp v1.55.0 // indirect
diff --git a/go.sum b/go.sum
index 07dc01d..b117caa 100644
--- a/go.sum
+++ b/go.sum
@@ -171,8 +171,8 @@ github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoC
 github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
 github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352 h1:L/EjCuZxs5tOcqqCaASj/nu65TRYEFcTt8qRQfHZXX0=
 github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352/go.mod h1:P1KoQSgnKEAG6Mnd3YlGzAophty+yKA9VV48LpfNRvo=
-github.com/coder/kaniko v0.0.0-20240925122543-caa18967f374 h1:/cyXf0vTSwFh7evQqeWHXXl14aRfC4CsNIYxOenJytQ=
-github.com/coder/kaniko v0.0.0-20240925122543-caa18967f374/go.mod h1:XoTDIhNF0Ll4tLmRYdOn31udU9w5zFrY2PME/crSRCA=
+github.com/coder/kaniko v0.0.0-20241028054616-350cbb820e05 h1:KZc6vG/WnSWG8RtUevGrCdZbF7XJaaZ32ocig6sZLQk=
+github.com/coder/kaniko v0.0.0-20241028054616-350cbb820e05/go.mod h1:3rM/KOQ4LgF8mE+O1P6pLDa/E57mzxIxNdUOMKi1qpg=
 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
 github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ=
@@ -187,10 +187,8 @@ github.com/coder/terraform-provider-coder v0.23.0 h1:DuNLWxhnGlXyG0g+OCAZRI6xd8+
 github.com/coder/terraform-provider-coder v0.23.0/go.mod h1:wMun9UZ9HT2CzF6qPPBup1odzBpVUc0/xSFoXgdI3tk=
 github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
 github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
-github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
-github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
-github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE=
-github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc=
+github.com/containerd/containerd v1.7.21 h1:USGXRK1eOC/SX0L195YgxTHb0a00anxajOzgfN0qrCA=
+github.com/containerd/containerd v1.7.21/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g=
 github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA=
 github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig=
 github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
@@ -207,15 +205,13 @@ github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G
 github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
 github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU=
 github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
-github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
-github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
+github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
+github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g=
 github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
 github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
 github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
 github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
 github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -236,12 +232,12 @@ github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiU
 github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY=
 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v27.2.0+incompatible h1:yHD1QEB1/0vr5eBNpu8tncu8gWxg8EydFPOSKHzXSMM=
-github.com/docker/cli v27.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70=
+github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
 github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
-github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
+github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
 github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -394,8 +390,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8=
 github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
-github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo=
-github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -439,8 +435,8 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP
 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
 github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
-github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
-github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk=
 github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA=
 github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14=
@@ -556,8 +552,8 @@ github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374
 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/moby/buildkit v0.13.1 h1:L8afOFhPq2RPJJSr/VyzbufwID7jquZVB7oFHbPRcPE=
-github.com/moby/buildkit v0.13.1/go.mod h1:aNmNQKLBFYAOFuzQjR3VA27/FijlvtBD1pjNwTSN37k=
+github.com/moby/buildkit v0.16.0 h1:wOVBj1o5YNVad/txPQNXUXdelm7Hs/i0PUFjzbK0VKE=
+github.com/moby/buildkit v0.16.0/go.mod h1:Xqx/5GlrqE1yIRORk0NSCVDFpQAU1WjlT6KHYZdisIQ=
 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
 github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
@@ -569,16 +565,18 @@ github.com/moby/swarmkit/v2 v2.0.0-20230315203717-e28e8ba9bc83/go.mod h1:GvjR7mC
 github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs=
 github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0=
 github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
-github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g=
-github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
+github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
+github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
 github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
 github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
-github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=
-github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
+github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
+github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
 github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc=
 github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
-github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
-github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
+github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
+github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
 github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -729,6 +727,8 @@ github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ0
 github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
 github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
 github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
+github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
+github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/twpayne/go-vfs/v5 v5.0.4 h1:/ne3h+rW7f5YOyOFguz+3ztfUwzOLR0Vts3y0mMAitg=
diff --git a/options/secrets.go b/options/secrets.go
new file mode 100644
index 0000000..9896fb3
--- /dev/null
+++ b/options/secrets.go
@@ -0,0 +1,46 @@
+package options
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/coder/serpent"
+)
+
+var buildSecretPrefix = fmt.Sprintf("%sBUILD_SECRET_", envPrefix)
+
+// EnvWithBuildSecretPrefix returns a string in the format of a build secret environment variable.
+func EnvWithBuildSecretPrefix(secretName, secretValue string) string {
+	return fmt.Sprintf("%s%s=%s", buildSecretPrefix, secretName, secretValue)
+}
+
+// GetBuildSecrets sources build secrets from the given environment.
+//
+// In a normal docker build, build secrets would be passed in via the
+// `docker build --secret` flag. envbuilder is more analogous to a
+// `docker run` that just happens to build its own container. It doesn't have
+// access to the `--secret` flag. As an alternative, we source these from the
+// envbuilder process environment.
+func GetBuildSecrets(environ []string) []string {
+	buildSecrets := serpent.ParseEnviron(environ, buildSecretPrefix).ToOS()
+	return buildSecrets
+}
+
+// ClearBuildSecretsFromProcessEnvironment unsets all build secrets from the process environment.
+// NOTE: This does not remove them from /proc/self/environ. They are still visible
+// there unless execve(2) is called.
+//
+// Unlike runtime secrets in the devcontainer spec or orchestration systems like
+// Kubernetes, build secrets should not be available at run time. envbuilder blurs
+// the line between build time and run time by transitioning from one to the other
+// within the same process in the same container.
+//
+// These build secrets should not make it into the runtime environment of the runtime
+// container init process. It is therefore useful to unset build secret environment
+// variables to ensure they aren't accidentally passed into the exec call.
+func ClearBuildSecretsFromProcessEnvironment() {
+	buildSecrets := serpent.ParseEnviron(os.Environ(), buildSecretPrefix)
+	for _, secret := range buildSecrets {
+		os.Unsetenv(buildSecretPrefix + secret.Name)
+	}
+}
diff --git a/scripts/Dockerfile b/scripts/Dockerfile
index 6259407..a8ba694 100644
--- a/scripts/Dockerfile
+++ b/scripts/Dockerfile
@@ -3,6 +3,6 @@ ARG TARGETARCH
 
 COPY envbuilder-${TARGETARCH} /.envbuilder/bin/envbuilder
 
-ENV KANIKO_DIR /.envbuilder
+ENV KANIKO_DIR=/.envbuilder
 
 ENTRYPOINT ["/.envbuilder/bin/envbuilder"]


Test patch:
diff --git a/integration/integration_test.go b/integration/integration_test.go
index bb900a0..c07bc42 100644
--- a/integration/integration_test.go
+++ b/integration/integration_test.go
@@ -37,7 +37,6 @@ import (
 	gossh "golang.org/x/crypto/ssh"
 
 	clitypes "github.com/docker/cli/cli/config/types"
-	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/image"
@@ -981,6 +980,82 @@ func TestUnsetOptionsEnv(t *testing.T) {
 	}
 }
 
+func TestUnsetSecretEnvs(t *testing.T) {
+	t.Parallel()
+
+	// Ensures that a Git repository with a devcontainer.json is cloned and built.
+	srv := gittest.CreateGitServer(t, gittest.Options{
+		Files: map[string]string{
+			".devcontainer/devcontainer.json": `{
+				"name": "Test",
+				"build": {
+					"dockerfile": "Dockerfile"
+				},
+			}`,
+			".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nENV FROM_DOCKERFILE=foo",
+		},
+	})
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
+		envbuilderEnv("GIT_URL", srv.URL),
+		envbuilderEnv("GIT_PASSWORD", "supersecret"),
+		options.EnvWithBuildSecretPrefix("FOO", "foo"),
+		envbuilderEnv("INIT_SCRIPT", "env > /root/env.txt && sleep infinity"),
+	}})
+	require.NoError(t, err)
+
+	output := execContainer(t, ctr, "cat /root/env.txt")
+	envsAvailableToInitScript := strings.Split(strings.TrimSpace(output), "\n")
+
+	leftoverBuildSecrets := options.GetBuildSecrets(envsAvailableToInitScript)
+	require.Empty(t, leftoverBuildSecrets, "build secrets should not be available to init script")
+}
+
+func TestBuildSecrets(t *testing.T) {
+	t.Parallel()
+
+	buildSecretVal := "foo"
+
+	srv := gittest.CreateGitServer(t, gittest.Options{
+		Files: map[string]string{
+			".devcontainer/devcontainer.json": `{
+				"name": "Test",
+				"build": {
+					"dockerfile": "Dockerfile"
+				},
+			}`,
+			".devcontainer/Dockerfile": "FROM " + testImageAlpine +
+				// Test whether build secrets are written to the default location
+				"\nRUN --mount=type=secret,id=FOO cat /run/secrets/FOO > /foo_from_file" +
+				// Test whether:
+				// * build secrets are written to env
+				// * build secrets are written to a custom target
+				// * build secrets are both written to env and target if both are specified
+				"\nRUN --mount=type=secret,id=FOO,env=FOO,target=/etc/foo echo $FOO > /foo_from_env && cat /etc/foo > /foo_from_custom_target" +
+				// Test what happens when you specify the same secret twice
+				"\nRUN --mount=type=secret,id=FOO,target=/etc/duplicate_foo --mount=type=secret,id=FOO,target=/etc/duplicate_foo cat /etc/duplicate_foo > /duplicate_foo_from_custom_target",
+		},
+	})
+
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
+		envbuilderEnv("GIT_URL", srv.URL),
+		envbuilderEnv("GIT_PASSWORD", "supersecret"),
+		options.EnvWithBuildSecretPrefix("FOO", buildSecretVal),
+	}})
+	require.NoError(t, err)
+
+	output := execContainer(t, ctr, "cat /foo_from_file")
+	assert.Equal(t, buildSecretVal, strings.TrimSpace(output))
+
+	output = execContainer(t, ctr, "cat /foo_from_env")
+	assert.Equal(t, buildSecretVal, strings.TrimSpace(output))
+
+	output = execContainer(t, ctr, "cat /foo_from_custom_target")
+	assert.Equal(t, buildSecretVal, strings.TrimSpace(output))
+
+	output = execContainer(t, ctr, "cat /duplicate_foo_from_custom_target")
+	assert.Equal(t, buildSecretVal, strings.TrimSpace(output))
+}
+
 func TestLifecycleScripts(t *testing.T) {
 	t.Parallel()
 
@@ -2397,14 +2472,14 @@ func execContainer(t *testing.T, containerID, command string) string {
 	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
 	require.NoError(t, err)
 	defer cli.Close()
-	execConfig := types.ExecConfig{
+	execConfig := container.ExecOptions{
 		AttachStdout: true,
 		AttachStderr: true,
 		Cmd:          []string{"/bin/sh", "-c", command},
 	}
 	execID, err := cli.ContainerExecCreate(ctx, containerID, execConfig)
 	require.NoError(t, err)
-	resp, err := cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{})
+	resp, err := cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
 	require.NoError(t, err)
 	defer resp.Close()
 	var buf bytes.Buffer
diff --git a/options/secrets_test.go b/options/secrets_test.go
new file mode 100644
index 0000000..69193d9
--- /dev/null
+++ b/options/secrets_test.go
@@ -0,0 +1,138 @@
+package options_test
+
+import (
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/coder/envbuilder/options"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetBuildSecrets(t *testing.T) {
+	// This test cannot be run in parallel, because it needs to modify the OS environment
+	tests := []struct {
+		name            string
+		envVars         map[string]string
+		expectedSecrets []string
+	}{
+		{
+			name:            "no secrets set",
+			envVars:         map[string]string{},
+			expectedSecrets: []string{},
+		},
+		{
+			name: "single secret",
+			envVars: map[string]string{
+				"ENVBUILDER_BUILD_SECRET_FOO": "bar",
+			},
+			expectedSecrets: []string{"FOO=bar"},
+		},
+		{
+			name: "multiple secrets",
+			envVars: map[string]string{
+				"ENVBUILDER_BUILD_SECRET_FOO": "bar",
+				"NOT_A_SECRET":                "baz",
+				"ENVBUILDER_BUILD_SECRET_BAZ": "qux",
+			},
+			expectedSecrets: []string{"FOO=bar", "BAZ=qux"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			preserveEnv(t)
+			os.Clearenv()
+
+			options.ClearBuildSecretsFromProcessEnvironment()
+			require.Empty(t, options.GetBuildSecrets(os.Environ()))
+
+			// Set environment variables for the test case
+			for key, value := range tt.envVars {
+				t.Setenv(key, value)
+			}
+
+			// when
+			secrets := options.GetBuildSecrets(os.Environ())
+
+			// then
+			assert.ElementsMatch(t, tt.expectedSecrets, secrets)
+		})
+	}
+}
+
+func TestClearBuildSecrets(t *testing.T) {
+	// This test cannot be run in parallel, because it needs to modify the OS environment
+	tests := []struct {
+		name                       string
+		initialEnvVars             map[string]string
+		expectedSecretsBeforeClear []string
+		expectedEnvironAfterClear  []string
+	}{
+		{
+			name: "single secret",
+			initialEnvVars: map[string]string{
+				"ENVBUILDER_BUILD_SECRET_FOO": "bar",
+			},
+			expectedSecretsBeforeClear: []string{"FOO=bar"},
+		},
+		{
+			name: "multiple secrets",
+			initialEnvVars: map[string]string{
+				"ENVBUILDER_BUILD_SECRET_FOO": "bar",
+				"ENVBUILDER_BUILD_SECRET_BAZ": "qux",
+			},
+			expectedSecretsBeforeClear: []string{"FOO=bar", "BAZ=qux"},
+		},
+		{
+			name: "only build secrets are cleared",
+			initialEnvVars: map[string]string{
+				"ENVBUILDER_BUILD_SECRET_FOO": "foo",
+				"BAR":                         "bar",
+			},
+			expectedSecretsBeforeClear: []string{"FOO=foo"},
+			expectedEnvironAfterClear:  []string{"BAR=bar"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			preserveEnv(t)
+			os.Clearenv()
+
+			// Set environment variables for the test case
+			for key, value := range tt.initialEnvVars {
+				t.Setenv(key, value)
+			}
+
+			// Verify secrets before clearing
+			secrets := options.GetBuildSecrets(os.Environ())
+			assert.ElementsMatch(t, tt.expectedSecretsBeforeClear, secrets)
+
+			// Clear the secrets
+			options.ClearBuildSecretsFromProcessEnvironment()
+
+			// Verify secrets after clearing
+			environ := os.Environ()
+			secrets = options.GetBuildSecrets(environ)
+			assert.Empty(t, secrets)
+		})
+	}
+}
+
+// preserveEnv takes a snapshot of the current process environment and restores it after the current
+// test to ensure that we don't cause flakes by modifying the environment for other tests.
+func preserveEnv(t *testing.T) {
+	envSnapshot := make(map[string]string)
+	for _, envVar := range os.Environ() {
+		parts := strings.SplitN(envVar, "=", 2)
+		envSnapshot[parts[0]] = parts[1]
+	}
+	t.Cleanup(func() {
+		os.Clearenv()
+		for key, value := range envSnapshot {
+			os.Setenv(key, value)
+		}
+	})
+}