diff --git a/pkg/commands/server.go b/pkg/commands/server.go index 2c12db6..b032e41 100644 --- a/pkg/commands/server.go +++ b/pkg/commands/server.go @@ -3,6 +3,7 @@ package commands import ( "context" "fmt" + "log/slog" "os" "os/signal" "strings" @@ -11,6 +12,7 @@ import ( "github.com/urfave/cli/v3" bridgev1 "github.com/vercel/bridge/api/go/bridge/v1" + "github.com/vercel/bridge/pkg/fsmount" "github.com/vercel/bridge/pkg/proxy" "google.golang.org/protobuf/encoding/protojson" ) @@ -88,6 +90,18 @@ func runServer(ctx context.Context, c *cli.Command) error { } } + // Auto-discover webhook-injected secret mounts (EKS Pod Identity, + // kube-api SA auto-mount, Azure Workload Identity, etc.) — they're + // added at pod-admission time and aren't reflected in the Deployment + // spec, so the --mount-roots argument from k8s/resources misses them. + discovered, err := fsmount.DiscoverSecretMounts() + if err != nil { + slog.Warn("Failed to discover secret mounts; webhook-injected projected volumes may be inaccessible", "error", err) + } else if len(discovered) > 0 { + slog.Info("Discovered webhook-injected secret mounts", "paths", discovered) + mountRoots = append(mountRoots, discovered...) + } + grpcServer := proxy.NewGRPCServer(proxy.Config{ Addr: addr, ListenPorts: listenPorts, diff --git a/pkg/fsmount/discover.go b/pkg/fsmount/discover.go new file mode 100644 index 0000000..27849b9 --- /dev/null +++ b/pkg/fsmount/discover.go @@ -0,0 +1,85 @@ +package fsmount + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +const procSelfMountinfo = "/proc/self/mountinfo" + +// DiscoverSecretMounts returns the mount points of webhook-injected +// secret/token projected volumes visible to the current process, read from +// /proc/self/mountinfo. +// +// It returns every tmpfs mount whose mount point starts with /run/secrets/ +// or /var/run/secrets/ — the canonical paths used by the kubelet auto-mount, +// EKS Pod Identity, Azure Workload Identity, and similar webhooks. These +// mounts are added at pod-admission time and are not present in the +// Deployment's PodSpec, so callers that derive --mount-roots from the spec +// miss them entirely. +// +// /var/run is a symlink to /run on most distros, so the kernel records the +// canonical /run/secrets/... form even when the workload spec uses +// /var/run/secrets/.... For each discovered mount, both forms are returned so +// authorize() prefix-matches whichever form a client uses. +func DiscoverSecretMounts() ([]string, error) { + f, err := os.Open(procSelfMountinfo) + if err != nil { + return nil, fmt.Errorf("open %s: %w", procSelfMountinfo, err) + } + defer f.Close() + + var paths []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + mp, ok := parseSecretMountLine(scanner.Text()) + if !ok { + continue + } + paths = append(paths, mp) + if alias, ok := strings.CutPrefix(mp, "/run/"); ok { + paths = append(paths, "/var/run/"+alias) + } else if alias, ok := strings.CutPrefix(mp, "/var/run/"); ok { + paths = append(paths, "/run/"+alias) + } + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("scan %s: %w", procSelfMountinfo, err) + } + return paths, nil +} + +// parseSecretMountLine extracts the mount point from one /proc/self/mountinfo +// line if it represents a tmpfs mount under /run/secrets/ or +// /var/run/secrets/. Returns ("", false) for any other line. +// +// mountinfo format (one line per mount): +// +// [...] - +// +// The fixed fields are space-separated up to a literal " - " separator; after +// the separator the filesystem type is the first token. See +// https://www.kernel.org/doc/Documentation/filesystems/proc.txt section 3.5. +func parseSecretMountLine(line string) (string, bool) { + const sep = " - " + sepIdx := strings.Index(line, sep) + if sepIdx < 0 { + return "", false + } + head := strings.Fields(line[:sepIdx]) + tail := strings.Fields(line[sepIdx+len(sep):]) + if len(head) < 5 || len(tail) < 1 { + return "", false + } + mountPoint, fsType := head[4], tail[0] + if fsType != "tmpfs" { + return "", false + } + if !strings.HasPrefix(mountPoint, "/run/secrets/") && + !strings.HasPrefix(mountPoint, "/var/run/secrets/") { + return "", false + } + return mountPoint, true +} diff --git a/pkg/fsmount/discover_test.go b/pkg/fsmount/discover_test.go new file mode 100644 index 0000000..5055e25 --- /dev/null +++ b/pkg/fsmount/discover_test.go @@ -0,0 +1,71 @@ +package fsmount + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseSecretMountLine(t *testing.T) { + cases := []struct { + name string + line string + want string + ok bool + }{ + { + name: "eks pod identity token", + line: "2381 2334 0:272 / /run/secrets/eks.amazonaws.com/serviceaccount ro,relatime - tmpfs tmpfs rw,size=61656332k", + want: "/run/secrets/eks.amazonaws.com/serviceaccount", + ok: true, + }, + { + name: "kubelet service-account auto-mount", + line: "2380 2334 0:273 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw,size=61656332k", + want: "/run/secrets/kubernetes.io/serviceaccount", + ok: true, + }, + { + name: "azure workload identity token", + line: "2377 2334 0:271 / /run/secrets/azure/tokens ro,relatime - tmpfs tmpfs rw,size=61656332k", + want: "/run/secrets/azure/tokens", + ok: true, + }, + { + name: "var/run alias form (some distros don't symlink /var/run to /run)", + line: "100 1 0:99 / /var/run/secrets/eks.amazonaws.com/serviceaccount ro,relatime - tmpfs tmpfs rw", + want: "/var/run/secrets/eks.amazonaws.com/serviceaccount", + ok: true, + }, + { + name: "rejects non-tmpfs mount under same prefix", + line: "100 1 259:1 / /run/secrets/something rw - xfs /dev/nvme0n1p1 rw", + ok: false, + }, + { + name: "rejects tmpfs mount outside /run/secrets", + line: "100 1 0:99 / /etc/bridge/tls ro,relatime - tmpfs tmpfs rw", + ok: false, + }, + { + name: "rejects malformed line (no separator)", + line: "100 1 0:99 / /run/secrets/x ro,relatime tmpfs tmpfs rw", + ok: false, + }, + { + name: "handles optional fields before separator", + line: "2381 2334 0:272 / /run/secrets/eks.amazonaws.com/serviceaccount ro,relatime master:1 - tmpfs tmpfs rw,size=61656332k", + want: "/run/secrets/eks.amazonaws.com/serviceaccount", + ok: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, ok := parseSecretMountLine(tc.line) + assert.Equal(t, tc.ok, ok) + if tc.ok { + assert.Equal(t, tc.want, got) + } + }) + } +}