diff --git a/main.go b/main.go index dd38d8811..c1c34dbc4 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package kvm import ( "context" + "encoding/pem" "fmt" "net/http" "os" @@ -16,9 +17,29 @@ import ( "github.com/rs/zerolog" ) +// caCertBundlePath is the path where the embedded CA certificate bundle is +// written at startup so that child processes (e.g. tailscale) can validate TLS +// certificates even though the device rootfs ships no system CA store. +const caCertBundlePath = "/tmp/jetkvm-cacerts.pem" + var appCtx context.Context var procPrefix string = "jetkvm: [app]" +// writeCABundleFile converts the embedded rootcerts DER certificates to PEM +// and writes them to caCertBundlePath. This allows child processes to use the +// bundle via the SSL_CERT_FILE environment variable. +func writeCABundleFile() error { + var bundle []byte + for _, c := range rootcerts.CertsByTrust(rootcerts.ServerTrustedDelegator) { + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: c.DER, + } + bundle = append(bundle, pem.EncodeToMemory(block)...) + } + return os.WriteFile(caCertBundlePath, bundle, 0644) //nolint:gosec +} + func setProcTitle(status string) { if status != "" { status = " " + status @@ -81,6 +102,12 @@ func Main() { Int("ca_certs_loaded", len(rootcerts.Certs())). Msg("loaded Root CA certificates") + // Write the embedded CA bundle to disk so child processes (tailscale, etc.) + // can validate TLS certificates via SSL_CERT_FILE. + if werr := writeCABundleFile(); werr != nil { + logger.Warn().Err(werr).Msg("failed to write CA certificate bundle to disk") + } + initOta() http.DefaultClient.Timeout = 1 * time.Minute diff --git a/tailscale.go b/tailscale.go index 5d4b213e1..2e8f486cd 100644 --- a/tailscale.go +++ b/tailscale.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "os" "os/exec" "strings" "time" @@ -54,11 +55,20 @@ func isTailscaleInstalled() bool { // execTailscaleStatus runs `tailscale status --json` and returns the raw output. // This is a package-level var to allow test substitution. +// newTailscaleCommand creates an exec.Cmd for a tailscale subcommand with the +// SSL_CERT_FILE environment variable set so that the tailscale binary can +// validate TLS certificates using the embedded CA bundle written at startup. +func newTailscaleCommand(ctx context.Context, args ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, "tailscale", args...) + cmd.Env = append(os.Environ(), "SSL_CERT_FILE="+caCertBundlePath) + return cmd +} + var execTailscaleStatus = func() ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), tailscaleCommandTimeout) defer cancel() - output, err := exec.CommandContext(ctx, "tailscale", "status", "--json").CombinedOutput() + output, err := newTailscaleCommand(ctx, "status", "--json").CombinedOutput() if err != nil { return nil, fmt.Errorf("tailscale status: %w: %s", err, strings.TrimSpace(string(output))) }