Skip to content

Commit 7f3cd37

Browse files
authored
feat: add option to specify additional certs (#98)
1 parent afa9825 commit 7f3cd37

File tree

10 files changed

+661
-54
lines changed

10 files changed

+661
-54
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The environment variables can be used to configure various aspects of the inner
2727
| `CODER_CPUS` | Dictates the number of CPUs to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false |
2828
| `CODER_MEMORY` | Dictates the max memory (in bytes) to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false |
2929
| `CODER_DISABLE_IDMAPPED_MOUNT` | Disables idmapped mounts in sysbox. For more information, see the [Sysbox Documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/configuration.md#disabling-id-mapped-mounts-on-sysbox). | false |
30+
| `CODER_EXTRA_CERTS_PATH` | A path to a file or directory containing CA certificates that should be made when communicating to external services (e.g. the Coder control plane or a Docker registry) | false |
3031

3132
## Coder Template
3233

buildlog/coder.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"net/http"
78
"net/url"
89
"time"
910

@@ -143,9 +144,10 @@ func newAgentClientV2(ctx context.Context, logger slog.Logger, client *agentsdk.
143144
}, nil
144145
}
145146

146-
func OpenCoderClient(ctx context.Context, accessURL *url.URL, logger slog.Logger, token string) (CoderClient, error) {
147+
func OpenCoderClient(ctx context.Context, logger slog.Logger, accessURL *url.URL, hc *http.Client, token string) (CoderClient, error) {
147148
client := agentsdk.New(accessURL)
148149
client.SetSessionToken(token)
150+
client.SDK.HTTPClient = hc
149151

150152
resp, err := client.SDK.BuildInfo(ctx)
151153
if err != nil {

cli/docker.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/coder/envbox/dockerutil"
2929
"github.com/coder/envbox/slogkubeterminate"
3030
"github.com/coder/envbox/sysboxutil"
31+
"github.com/coder/envbox/xhttp"
3132
"github.com/coder/envbox/xunix"
3233
)
3334

@@ -101,6 +102,7 @@ var (
101102
EnvDockerConfig = "CODER_DOCKER_CONFIG"
102103
EnvDebug = "CODER_DEBUG"
103104
EnvDisableIDMappedMount = "CODER_DISABLE_IDMAPPED_MOUNT"
105+
EnvExtraCertsPath = "CODER_EXTRA_CERTS_PATH"
104106
)
105107

106108
var envboxPrivateMounts = map[string]struct{}{
@@ -138,6 +140,7 @@ type flags struct {
138140
cpus int
139141
memory int
140142
disableIDMappedMount bool
143+
extraCertsPath string
141144

142145
// Test flags.
143146
noStartupLogs bool
@@ -158,6 +161,11 @@ func dockerCmd() *cobra.Command {
158161
blog buildlog.Logger = buildlog.NopLogger{}
159162
)
160163

164+
httpClient, err := xhttp.Client(log, flags.extraCertsPath)
165+
if err != nil {
166+
return xerrors.Errorf("http client: %w", err)
167+
}
168+
161169
if !flags.noStartupLogs {
162170
log = slog.Make(slogjson.Sink(cmd.ErrOrStderr()), slogkubeterminate.Make()).Leveled(slog.LevelDebug)
163171
blog = buildlog.JSONLogger{Encoder: json.NewEncoder(os.Stderr)}
@@ -169,7 +177,7 @@ func dockerCmd() *cobra.Command {
169177
return xerrors.Errorf("parse coder URL %q: %w", flags.coderURL, err)
170178
}
171179

172-
agent, err := buildlog.OpenCoderClient(ctx, coderURL, log, flags.agentToken)
180+
agent, err := buildlog.OpenCoderClient(ctx, log, coderURL, httpClient, flags.agentToken)
173181
if err != nil {
174182
// Don't fail workspace startup on
175183
// an inability to push build logs.
@@ -349,6 +357,7 @@ func dockerCmd() *cobra.Command {
349357
cliflag.IntVarP(cmd.Flags(), &flags.cpus, "cpus", "", EnvCPUs, 0, "Number of CPUs to allocate inner container. e.g. 2")
350358
cliflag.IntVarP(cmd.Flags(), &flags.memory, "memory", "", EnvMemory, 0, "Max memory to allocate to the inner container in bytes.")
351359
cliflag.BoolVarP(cmd.Flags(), &flags.disableIDMappedMount, "disable-idmapped-mount", "", EnvDisableIDMappedMount, false, "Disable idmapped mounts in sysbox. Note that you may need an alternative (e.g. shiftfs).")
360+
cliflag.StringVarP(cmd.Flags(), &flags.extraCertsPath, "extra-certs-path", "", EnvExtraCertsPath, "", "The path to a directory or file containing extra CA certificates.")
352361

353362
// Test flags.
354363
cliflag.BoolVarP(cmd.Flags(), &flags.noStartupLogs, "no-startup-log", "", "", false, "Do not log startup logs. Useful for testing.")

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ go 1.22.4
66
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
77
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240530071520-1ac63d3a4ee3
88

9+
replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20231128192721-70855dedb788
10+
911
require (
1012
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
1113
github.com/coder/coder/v2 v2.12.0

integration/docker_test.go

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package integration_test
55

66
import (
77
"fmt"
8+
"net"
89
"os"
910
"path/filepath"
1011
"strconv"
@@ -37,9 +38,9 @@ func TestDocker(t *testing.T) {
3738
runEnvbox := func() *dockertest.Resource {
3839
// Run the envbox container.
3940
resource := integrationtest.RunEnvbox(t, pool, &integrationtest.CreateDockerCVMConfig{
40-
Image: integrationtest.DockerdImage,
41-
Username: "root",
42-
Binds: binds,
41+
Image: integrationtest.DockerdImage,
42+
Username: "root",
43+
OuterMounts: binds,
4344
})
4445

4546
// Wait for the inner container's docker daemon.
@@ -98,8 +99,8 @@ func TestDocker(t *testing.T) {
9899
require.NoError(t, err)
99100

100101
binds = append(binds,
101-
bindMount(homeDir, "/home/coder", false),
102-
bindMount(secretDir, "/var/secrets", true),
102+
integrationtest.BindMount(homeDir, "/home/coder", false),
103+
integrationtest.BindMount(secretDir, "/var/secrets", true),
103104
)
104105

105106
var (
@@ -144,7 +145,7 @@ func TestDocker(t *testing.T) {
144145
Username: "coder",
145146
InnerEnvFilter: envFilter,
146147
Envs: envs,
147-
Binds: binds,
148+
OuterMounts: binds,
148149
AddFUSE: true,
149150
AddTUN: true,
150151
BootstrapScript: bootstrapScript,
@@ -272,6 +273,83 @@ func TestDocker(t *testing.T) {
272273
require.NoError(t, err)
273274
require.Equal(t, expectedHostname, strings.TrimSpace(string(hostname)))
274275
})
276+
277+
t.Run("SelfSignedCerts", func(t *testing.T) {
278+
t.Parallel()
279+
280+
var (
281+
dir = integrationtest.TmpDir(t)
282+
binds = integrationtest.DefaultBinds(t, dir)
283+
)
284+
285+
pool, err := dockertest.NewPool("")
286+
require.NoError(t, err)
287+
288+
// Create some listeners for the Docker and Coder
289+
// services we'll be running with self signed certs.
290+
bridgeIP := integrationtest.DockerBridgeIP(t)
291+
coderListener, err := net.Listen("tcp", fmt.Sprintf("%s:0", bridgeIP))
292+
require.NoError(t, err)
293+
defer coderListener.Close()
294+
coderAddr := tcpAddr(t, coderListener)
295+
296+
registryListener, err := net.Listen("tcp", fmt.Sprintf("%s:0", bridgeIP))
297+
require.NoError(t, err)
298+
err = registryListener.Close()
299+
require.NoError(t, err)
300+
registryAddr := tcpAddr(t, registryListener)
301+
302+
coderCert := integrationtest.GenerateTLSCertificate(t, "host.docker.internal", coderAddr.IP.String())
303+
dockerCert := integrationtest.GenerateTLSCertificate(t, "host.docker.internal", registryAddr.IP.String())
304+
305+
// Startup our fake Coder "control-plane".
306+
recorder := integrationtest.FakeBuildLogRecorder(t, coderListener, coderCert)
307+
308+
certDir := integrationtest.MkdirAll(t, dir, "certs")
309+
310+
// Write the Coder cert disk.
311+
coderCertPath := filepath.Join(certDir, "coder_cert.pem")
312+
coderKeyPath := filepath.Join(certDir, "coder_key.pem")
313+
integrationtest.WriteCertificate(t, coderCert, coderCertPath, coderKeyPath)
314+
coderCertMount := integrationtest.BindMount(certDir, "/tmp/certs", false)
315+
316+
// Write the Registry cert to disk.
317+
regCertPath := filepath.Join(certDir, "registry_cert.crt")
318+
regKeyPath := filepath.Join(certDir, "registry_key.pem")
319+
integrationtest.WriteCertificate(t, dockerCert, regCertPath, regKeyPath)
320+
321+
// Start up the docker registry and push an image
322+
// to it that we can reference.
323+
image := integrationtest.RunLocalDockerRegistry(t, pool, integrationtest.RegistryConfig{
324+
HostCertPath: regCertPath,
325+
HostKeyPath: regKeyPath,
326+
Image: integrationtest.UbuntuImage,
327+
TLSPort: strconv.Itoa(registryAddr.Port),
328+
})
329+
330+
// Mount the cert into the expected location
331+
// for the Envbox Docker daemon.
332+
regCAPath := filepath.Join("/etc/docker/certs.d", image.Registry(), "ca.crt")
333+
registryCAMount := integrationtest.BindMount(regCertPath, regCAPath, false)
334+
335+
envs := []string{
336+
integrationtest.EnvVar(cli.EnvAgentToken, "faketoken"),
337+
integrationtest.EnvVar(cli.EnvAgentURL, fmt.Sprintf("https://%s:%d", "host.docker.internal", coderAddr.Port)),
338+
integrationtest.EnvVar(cli.EnvExtraCertsPath, "/tmp/certs"),
339+
}
340+
341+
// Run the envbox container.
342+
_ = integrationtest.RunEnvbox(t, pool, &integrationtest.CreateDockerCVMConfig{
343+
Image: image.String(),
344+
Username: "coder",
345+
Envs: envs,
346+
OuterMounts: append(binds, coderCertMount, registryCAMount),
347+
})
348+
349+
// This indicates we've made it all the way to end
350+
// of the logs we attempt to push.
351+
require.True(t, recorder.ContainsLog("Bootstrapping workspace..."))
352+
})
275353
}
276354

277355
func requireSliceNoContains(t *testing.T, ss []string, els ...string) {
@@ -297,9 +375,10 @@ func requireSliceContains(t *testing.T, ss []string, els ...string) {
297375
}
298376
}
299377

300-
func bindMount(src, dest string, ro bool) string {
301-
if ro {
302-
return fmt.Sprintf("%s:%s:%s", src, dest, "ro")
303-
}
304-
return fmt.Sprintf("%s:%s", src, dest)
378+
func tcpAddr(t testing.TB, l net.Listener) *net.TCPAddr {
379+
t.Helper()
380+
381+
tcpAddr, ok := l.Addr().(*net.TCPAddr)
382+
require.True(t, ok)
383+
return tcpAddr
305384
}

integration/integrationtest/certs.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package integrationtest
2+
3+
import (
4+
"bytes"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"crypto/tls"
9+
"crypto/x509"
10+
"crypto/x509/pkix"
11+
"encoding/pem"
12+
"math/big"
13+
"net"
14+
"os"
15+
"testing"
16+
"time"
17+
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
func GenerateTLSCertificate(t testing.TB, commonName string, ipAddr string) tls.Certificate {
22+
t.Helper()
23+
24+
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
25+
require.NoError(t, err)
26+
template := x509.Certificate{
27+
SerialNumber: big.NewInt(1),
28+
Subject: pkix.Name{
29+
Organization: []string{"Acme Co"},
30+
CommonName: commonName,
31+
},
32+
DNSNames: []string{commonName},
33+
NotBefore: time.Now(),
34+
NotAfter: time.Now().Add(time.Hour * 24 * 180),
35+
36+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
37+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
38+
BasicConstraintsValid: true,
39+
IPAddresses: []net.IP{net.ParseIP(ipAddr)},
40+
IsCA: true,
41+
}
42+
43+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
44+
require.NoError(t, err)
45+
var certFile bytes.Buffer
46+
require.NoError(t, err)
47+
_, err = certFile.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}))
48+
require.NoError(t, err)
49+
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
50+
require.NoError(t, err)
51+
var keyFile bytes.Buffer
52+
err = pem.Encode(&keyFile, &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes})
53+
require.NoError(t, err)
54+
cert, err := tls.X509KeyPair(certFile.Bytes(), keyFile.Bytes())
55+
require.NoError(t, err)
56+
return cert
57+
}
58+
59+
func writePEM(t testing.TB, path string, typ string, contents []byte) {
60+
t.Helper()
61+
62+
f, err := os.Create(path)
63+
require.NoError(t, err)
64+
defer f.Close()
65+
66+
err = pem.Encode(f, &pem.Block{
67+
Type: typ,
68+
Bytes: contents,
69+
})
70+
require.NoError(t, err)
71+
}
72+
73+
func WriteCertificate(t testing.TB, c tls.Certificate, certPath, keyPath string) {
74+
require.Len(t, c.Certificate, 1, "expecting 1 certificate")
75+
key, err := x509.MarshalPKCS8PrivateKey(c.PrivateKey)
76+
require.NoError(t, err)
77+
78+
cert := c.Certificate[0]
79+
80+
writePEM(t, keyPath, "PRIVATE KEY", key)
81+
writePEM(t, certPath, "CERTIFICATE", cert)
82+
}

0 commit comments

Comments
 (0)