mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
72f35e1cd3
Injects user secrets into workspace agents at runtime via the agent manifest. Secrets with an environment variable name are set as environment variables in every agent session and startup script. Secrets with a file path are written to disk before startup scripts run. - Fetch user secrets in GetManifest and convert to proto - Defensively strip secrets from manifests received by the agent to avoid accidental leakage - Add WorkspaceSecret type and proto conversion helpers to agentsdk - Write secret files eagerly on manifest fetch (0600 perms, 0700 dirs) - Inject secret env vars per-session in updateCommandEnv - Expand ~/paths using caller-resolved home directory - Log file write errors without blocking workspace startup
186 lines
5.4 KiB
Go
186 lines
5.4 KiB
Go
package agent //nolint:testpackage // Exercises internal agent secrets handling.
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestWriteSecretFiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("AbsolutePath", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
writeSecretFiles(ctx, logger, fs, "/home/coder", []agentsdk.WorkspaceSecret{
|
|
{FilePath: "/etc/myapp/config.json", Value: []byte(`{"key":"val"}`)},
|
|
})
|
|
|
|
content, err := afero.ReadFile(fs, "/etc/myapp/config.json")
|
|
require.NoError(t, err)
|
|
require.Equal(t, `{"key":"val"}`, string(content))
|
|
|
|
fi, err := fs.Stat("/etc/myapp/config.json")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0o600, int(fi.Mode().Perm()))
|
|
|
|
di, err := fs.Stat("/etc/myapp")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0o700, int(di.Mode().Perm()))
|
|
})
|
|
|
|
t.Run("TildePath", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
writeSecretFiles(ctx, logger, fs, "/home/coder", []agentsdk.WorkspaceSecret{
|
|
{FilePath: "~/.ssh/id_rsa", Value: []byte("private-key")},
|
|
})
|
|
|
|
content, err := afero.ReadFile(fs, "/home/coder/.ssh/id_rsa")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "private-key", string(content))
|
|
|
|
fi, err := fs.Stat("/home/coder/.ssh/id_rsa")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0o600, int(fi.Mode().Perm()))
|
|
|
|
di, err := fs.Stat("/home/coder/.ssh")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0o700, int(di.Mode().Perm()))
|
|
})
|
|
|
|
t.Run("TildePathNoHomeDir", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
writeSecretFiles(ctx, logger, fs, "", []agentsdk.WorkspaceSecret{
|
|
{FilePath: "~/.config/token", Value: []byte("token")},
|
|
})
|
|
|
|
empty, err := afero.IsEmpty(fs, "/")
|
|
require.NoError(t, err)
|
|
require.True(t, empty, "no file should be written when home dir is unknown")
|
|
})
|
|
|
|
t.Run("EmptyFilePathSkipped", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
writeSecretFiles(ctx, logger, fs, "/home/coder", []agentsdk.WorkspaceSecret{
|
|
{EnvName: "MY_TOKEN", Value: []byte("token")},
|
|
})
|
|
|
|
// Nothing should be written.
|
|
empty, err := afero.IsEmpty(fs, "/")
|
|
require.NoError(t, err)
|
|
require.True(t, empty)
|
|
})
|
|
|
|
t.Run("MultipleSecrets", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
writeSecretFiles(ctx, logger, fs, "/home/coder", []agentsdk.WorkspaceSecret{
|
|
{FilePath: "/etc/secret-a", Value: []byte("aaa")},
|
|
{FilePath: "~/.secret-b", Value: []byte("bbb")},
|
|
{EnvName: "SKIP_ME", Value: []byte("env-only")},
|
|
})
|
|
|
|
a, err := afero.ReadFile(fs, "/etc/secret-a")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "aaa", string(a))
|
|
|
|
b, err := afero.ReadFile(fs, "/home/coder/.secret-b")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "bbb", string(b))
|
|
})
|
|
|
|
t.Run("OverwritesExisting", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
require.NoError(t, afero.WriteFile(fs, "/secret", []byte("old"), 0o644))
|
|
|
|
writeSecretFiles(ctx, logger, fs, "", []agentsdk.WorkspaceSecret{
|
|
{FilePath: "/secret", Value: []byte("new")},
|
|
})
|
|
|
|
content, err := afero.ReadFile(fs, "/secret")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "new", string(content))
|
|
|
|
// Pre-existing file permissions are intentionally preserved.
|
|
// The file may not have been created by us (e.g. a template
|
|
// provisioned it), so we should not alter its permissions.
|
|
fi, err := fs.Stat("/secret")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0o644, int(fi.Mode().Perm()))
|
|
})
|
|
|
|
t.Run("PathCollisionAfterTildeResolution", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
// "~/collide" and "/home/coder/collide" resolve to the same
|
|
// absolute path. The later secret should win.
|
|
writeSecretFiles(ctx, logger, fs, "/home/coder", []agentsdk.WorkspaceSecret{
|
|
{FilePath: "~/collide", Value: []byte("first")},
|
|
{FilePath: "/home/coder/collide", Value: []byte("second")},
|
|
})
|
|
|
|
content, err := afero.ReadFile(fs, "/home/coder/collide")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "second", string(content))
|
|
})
|
|
|
|
t.Run("EmptySlice", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
writeSecretFiles(ctx, logger, fs, "/home/coder", nil)
|
|
|
|
empty, err := afero.IsEmpty(fs, "/")
|
|
require.NoError(t, err)
|
|
require.True(t, empty)
|
|
})
|
|
|
|
t.Run("BinaryContent", func(t *testing.T) {
|
|
t.Parallel()
|
|
fs := afero.NewMemMapFs()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
logger := slogtest.Make(t, nil)
|
|
|
|
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
|
|
writeSecretFiles(ctx, logger, fs, "", []agentsdk.WorkspaceSecret{
|
|
{FilePath: "/cert.der", Value: binaryData},
|
|
})
|
|
|
|
content, err := afero.ReadFile(fs, "/cert.der")
|
|
require.NoError(t, err)
|
|
require.Equal(t, binaryData, content)
|
|
})
|
|
}
|