Skip to content

Commit

Permalink
feat: add option to specify additional certs (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
sreya authored Sep 4, 2024
1 parent afa9825 commit 7f3cd37
Show file tree
Hide file tree
Showing 10 changed files with 661 additions and 54 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The environment variables can be used to configure various aspects of the inner
| `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 |
| `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 |
| `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 |
| `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 |

## Coder Template

Expand Down
4 changes: 3 additions & 1 deletion buildlog/coder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"

Expand Down Expand Up @@ -143,9 +144,10 @@ func newAgentClientV2(ctx context.Context, logger slog.Logger, client *agentsdk.
}, nil
}

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

resp, err := client.SDK.BuildInfo(ctx)
if err != nil {
Expand Down
11 changes: 10 additions & 1 deletion cli/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/coder/envbox/dockerutil"
"github.com/coder/envbox/slogkubeterminate"
"github.com/coder/envbox/sysboxutil"
"github.com/coder/envbox/xhttp"
"github.com/coder/envbox/xunix"
)

Expand Down Expand Up @@ -101,6 +102,7 @@ var (
EnvDockerConfig = "CODER_DOCKER_CONFIG"
EnvDebug = "CODER_DEBUG"
EnvDisableIDMappedMount = "CODER_DISABLE_IDMAPPED_MOUNT"
EnvExtraCertsPath = "CODER_EXTRA_CERTS_PATH"
)

var envboxPrivateMounts = map[string]struct{}{
Expand Down Expand Up @@ -138,6 +140,7 @@ type flags struct {
cpus int
memory int
disableIDMappedMount bool
extraCertsPath string

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

httpClient, err := xhttp.Client(log, flags.extraCertsPath)
if err != nil {
return xerrors.Errorf("http client: %w", err)
}

if !flags.noStartupLogs {
log = slog.Make(slogjson.Sink(cmd.ErrOrStderr()), slogkubeterminate.Make()).Leveled(slog.LevelDebug)
blog = buildlog.JSONLogger{Encoder: json.NewEncoder(os.Stderr)}
Expand All @@ -169,7 +177,7 @@ func dockerCmd() *cobra.Command {
return xerrors.Errorf("parse coder URL %q: %w", flags.coderURL, err)
}

agent, err := buildlog.OpenCoderClient(ctx, coderURL, log, flags.agentToken)
agent, err := buildlog.OpenCoderClient(ctx, log, coderURL, httpClient, flags.agentToken)
if err != nil {
// Don't fail workspace startup on
// an inability to push build logs.
Expand Down Expand Up @@ -349,6 +357,7 @@ func dockerCmd() *cobra.Command {
cliflag.IntVarP(cmd.Flags(), &flags.cpus, "cpus", "", EnvCPUs, 0, "Number of CPUs to allocate inner container. e.g. 2")
cliflag.IntVarP(cmd.Flags(), &flags.memory, "memory", "", EnvMemory, 0, "Max memory to allocate to the inner container in bytes.")
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).")
cliflag.StringVarP(cmd.Flags(), &flags.extraCertsPath, "extra-certs-path", "", EnvExtraCertsPath, "", "The path to a directory or file containing extra CA certificates.")

// Test flags.
cliflag.BoolVarP(cmd.Flags(), &flags.noStartupLogs, "no-startup-log", "", "", false, "Do not log startup logs. Useful for testing.")
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ go 1.22.4
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240530071520-1ac63d3a4ee3

replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20231128192721-70855dedb788

require (
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
github.com/coder/coder/v2 v2.12.0
Expand Down
101 changes: 90 additions & 11 deletions integration/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package integration_test

import (
"fmt"
"net"
"os"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -37,9 +38,9 @@ func TestDocker(t *testing.T) {
runEnvbox := func() *dockertest.Resource {
// Run the envbox container.
resource := integrationtest.RunEnvbox(t, pool, &integrationtest.CreateDockerCVMConfig{
Image: integrationtest.DockerdImage,
Username: "root",
Binds: binds,
Image: integrationtest.DockerdImage,
Username: "root",
OuterMounts: binds,
})

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

binds = append(binds,
bindMount(homeDir, "/home/coder", false),
bindMount(secretDir, "/var/secrets", true),
integrationtest.BindMount(homeDir, "/home/coder", false),
integrationtest.BindMount(secretDir, "/var/secrets", true),
)

var (
Expand Down Expand Up @@ -144,7 +145,7 @@ func TestDocker(t *testing.T) {
Username: "coder",
InnerEnvFilter: envFilter,
Envs: envs,
Binds: binds,
OuterMounts: binds,
AddFUSE: true,
AddTUN: true,
BootstrapScript: bootstrapScript,
Expand Down Expand Up @@ -272,6 +273,83 @@ func TestDocker(t *testing.T) {
require.NoError(t, err)
require.Equal(t, expectedHostname, strings.TrimSpace(string(hostname)))
})

t.Run("SelfSignedCerts", func(t *testing.T) {
t.Parallel()

var (
dir = integrationtest.TmpDir(t)
binds = integrationtest.DefaultBinds(t, dir)
)

pool, err := dockertest.NewPool("")
require.NoError(t, err)

// Create some listeners for the Docker and Coder
// services we'll be running with self signed certs.
bridgeIP := integrationtest.DockerBridgeIP(t)
coderListener, err := net.Listen("tcp", fmt.Sprintf("%s:0", bridgeIP))
require.NoError(t, err)
defer coderListener.Close()
coderAddr := tcpAddr(t, coderListener)

registryListener, err := net.Listen("tcp", fmt.Sprintf("%s:0", bridgeIP))
require.NoError(t, err)
err = registryListener.Close()
require.NoError(t, err)
registryAddr := tcpAddr(t, registryListener)

coderCert := integrationtest.GenerateTLSCertificate(t, "host.docker.internal", coderAddr.IP.String())
dockerCert := integrationtest.GenerateTLSCertificate(t, "host.docker.internal", registryAddr.IP.String())

// Startup our fake Coder "control-plane".
recorder := integrationtest.FakeBuildLogRecorder(t, coderListener, coderCert)

certDir := integrationtest.MkdirAll(t, dir, "certs")

// Write the Coder cert disk.
coderCertPath := filepath.Join(certDir, "coder_cert.pem")
coderKeyPath := filepath.Join(certDir, "coder_key.pem")
integrationtest.WriteCertificate(t, coderCert, coderCertPath, coderKeyPath)
coderCertMount := integrationtest.BindMount(certDir, "/tmp/certs", false)

// Write the Registry cert to disk.
regCertPath := filepath.Join(certDir, "registry_cert.crt")
regKeyPath := filepath.Join(certDir, "registry_key.pem")
integrationtest.WriteCertificate(t, dockerCert, regCertPath, regKeyPath)

// Start up the docker registry and push an image
// to it that we can reference.
image := integrationtest.RunLocalDockerRegistry(t, pool, integrationtest.RegistryConfig{
HostCertPath: regCertPath,
HostKeyPath: regKeyPath,
Image: integrationtest.UbuntuImage,
TLSPort: strconv.Itoa(registryAddr.Port),
})

// Mount the cert into the expected location
// for the Envbox Docker daemon.
regCAPath := filepath.Join("/etc/docker/certs.d", image.Registry(), "ca.crt")
registryCAMount := integrationtest.BindMount(regCertPath, regCAPath, false)

envs := []string{
integrationtest.EnvVar(cli.EnvAgentToken, "faketoken"),
integrationtest.EnvVar(cli.EnvAgentURL, fmt.Sprintf("https://%s:%d", "host.docker.internal", coderAddr.Port)),
integrationtest.EnvVar(cli.EnvExtraCertsPath, "/tmp/certs"),
}

// Run the envbox container.
_ = integrationtest.RunEnvbox(t, pool, &integrationtest.CreateDockerCVMConfig{
Image: image.String(),
Username: "coder",
Envs: envs,
OuterMounts: append(binds, coderCertMount, registryCAMount),
})

// This indicates we've made it all the way to end
// of the logs we attempt to push.
require.True(t, recorder.ContainsLog("Bootstrapping workspace..."))
})
}

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

func bindMount(src, dest string, ro bool) string {
if ro {
return fmt.Sprintf("%s:%s:%s", src, dest, "ro")
}
return fmt.Sprintf("%s:%s", src, dest)
func tcpAddr(t testing.TB, l net.Listener) *net.TCPAddr {
t.Helper()

tcpAddr, ok := l.Addr().(*net.TCPAddr)
require.True(t, ok)
return tcpAddr
}
82 changes: 82 additions & 0 deletions integration/integrationtest/certs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package integrationtest

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func GenerateTLSCertificate(t testing.TB, commonName string, ipAddr string) tls.Certificate {
t.Helper()

privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Acme Co"},
CommonName: commonName,
},
DNSNames: []string{commonName},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 180),

KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IPAddresses: []net.IP{net.ParseIP(ipAddr)},
IsCA: true,
}

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
var certFile bytes.Buffer
require.NoError(t, err)
_, err = certFile.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}))
require.NoError(t, err)
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
require.NoError(t, err)
var keyFile bytes.Buffer
err = pem.Encode(&keyFile, &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes})
require.NoError(t, err)
cert, err := tls.X509KeyPair(certFile.Bytes(), keyFile.Bytes())
require.NoError(t, err)
return cert
}

func writePEM(t testing.TB, path string, typ string, contents []byte) {
t.Helper()

f, err := os.Create(path)
require.NoError(t, err)
defer f.Close()

err = pem.Encode(f, &pem.Block{
Type: typ,
Bytes: contents,
})
require.NoError(t, err)
}

func WriteCertificate(t testing.TB, c tls.Certificate, certPath, keyPath string) {
require.Len(t, c.Certificate, 1, "expecting 1 certificate")
key, err := x509.MarshalPKCS8PrivateKey(c.PrivateKey)
require.NoError(t, err)

cert := c.Certificate[0]

writePEM(t, keyPath, "PRIVATE KEY", key)
writePEM(t, certPath, "CERTIFICATE", cert)
}
Loading

0 comments on commit 7f3cd37

Please sign in to comment.