From 9bad15c0ccf02236a9b6a6a3477736778b62f27b Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 13:24:37 -0400 Subject: [PATCH 1/9] Add host capabilities endpoint and secret-safe diagnostics Add an authenticated GET /capabilities endpoint that reports machine-readable host capabilities so clients can gate behavior without selecting or naming a concrete hypervisor: - server build version and API contract version (from the embedded spec) - host OS/architecture - effective default-runtime features (snapshot, standby, pause, memory hotplug, balloon, vsock, GPU passthrough, disk I/O limit, disk resize) - guest networking model (bridge/NAT), guest-visible host gateway and subnet, and guest-to-guest reachability - supported image platforms (including Rosetta-emulated linux/amd64 on Apple Silicon macOS) and the default platform - stable feature IDs derived from the effective default runtime Runtime-derived capabilities no longer overstate macOS support: vz snapshot/standby is now gated on the probed macOS version (Apple Silicon + macOS 14+) instead of a static arm64 check, and a failed probe reports no support rather than guessing. Make diagnostics secret-safe: - GET /instances and GET /instances/{id} redact env values by default (keys preserved, values replaced with "[redacted]"); include_env=true opts back into plaintext. Env updates that round-trip the sentinel never overwrite real values. - Instance metadata.json, guest config disks, and build config/metadata files are written 0600; legacy 0644 instance metadata is tightened at startup; overly-permissive server config files trigger a startup warning. Health and resources endpoints are unchanged. --- cmd/api/api/backward_compat_test.go | 51 + cmd/api/api/capabilities.go | 233 ++++ cmd/api/api/capabilities_test.go | 177 +++ cmd/api/api/instances.go | 26 +- cmd/api/api/instances_redact_test.go | 128 ++ cmd/api/config/config.go | 26 + cmd/api/config/config_test.go | 50 + lib/builds/manager_test.go | 4 + lib/builds/storage.go | 8 +- lib/hypervisor/vz/client.go | 6 +- lib/hypervisor/vz/save_restore_support.go | 40 + .../vz/save_restore_support_darwin.go | 26 + .../vz/save_restore_support_darwin_test.go | 25 + .../vz/save_restore_support_test.go | 33 + lib/instances/configdisk.go | 7 +- lib/instances/manager.go | 13 + lib/instances/metadata_permissions_test.go | 112 ++ lib/instances/storage.go | 33 +- lib/instances/update.go | 27 +- lib/instances/wait_test.go | 4 + lib/middleware/access_log_test.go | 35 + lib/network/default_network_test.go | 51 + lib/network/manager.go | 14 + lib/network/model_darwin.go | 16 + lib/network/model_linux.go | 16 + lib/oapi/oapi.go | 1038 ++++++++++++----- lib/redact/redact.go | 31 + lib/redact/redact_test.go | 37 + lib/scopes/scopes.go | 3 +- openapi.yaml | 192 ++- 30 files changed, 2128 insertions(+), 334 deletions(-) create mode 100644 cmd/api/api/backward_compat_test.go create mode 100644 cmd/api/api/capabilities.go create mode 100644 cmd/api/api/capabilities_test.go create mode 100644 cmd/api/api/instances_redact_test.go create mode 100644 lib/hypervisor/vz/save_restore_support.go create mode 100644 lib/hypervisor/vz/save_restore_support_darwin.go create mode 100644 lib/hypervisor/vz/save_restore_support_darwin_test.go create mode 100644 lib/hypervisor/vz/save_restore_support_test.go create mode 100644 lib/instances/metadata_permissions_test.go create mode 100644 lib/middleware/access_log_test.go create mode 100644 lib/network/default_network_test.go create mode 100644 lib/network/model_darwin.go create mode 100644 lib/network/model_linux.go create mode 100644 lib/redact/redact.go create mode 100644 lib/redact/redact_test.go diff --git a/cmd/api/api/backward_compat_test.go b/cmd/api/api/backward_compat_test.go new file mode 100644 index 000000000..ec090f5b7 --- /dev/null +++ b/cmd/api/api/backward_compat_test.go @@ -0,0 +1,51 @@ +package api + +import ( + "encoding/json" + "testing" + + "github.com/kernel/hypeman/lib/oapi" + "github.com/stretchr/testify/require" +) + +// TestHealthBackwardCompatible pins the health endpoint contract: it must +// keep returning exactly {"status":"ok"}. +func TestHealthBackwardCompatible(t *testing.T) { + t.Parallel() + svc := newTestService(t) + + resp, err := svc.GetHealth(ctx(), oapi.GetHealthRequestObject{}) + require.NoError(t, err) + okResp, ok := resp.(oapi.GetHealth200JSONResponse) + require.True(t, ok, "expected 200 response, got %T", resp) + + raw, err := json.Marshal(okResp) + require.NoError(t, err) + require.JSONEq(t, `{"status":"ok"}`, string(raw)) +} + +// TestResourcesBackwardCompatible pins the resources endpoint contract: the +// top-level fields existing clients consume must remain present. +func TestResourcesBackwardCompatible(t *testing.T) { + t.Parallel() + svc := newTestService(t) + + // Wire the resource manager the same way production providers do. + svc.ResourceManager.SetImageLister(svc.ImageManager) + svc.ResourceManager.SetInstanceLister(svc.InstanceManager) + svc.ResourceManager.SetVolumeLister(svc.VolumeManager) + require.NoError(t, svc.ResourceManager.Initialize(ctx())) + + resp, err := svc.GetResources(ctx(), oapi.GetResourcesRequestObject{}) + require.NoError(t, err) + okResp, ok := resp.(oapi.GetResources200JSONResponse) + require.True(t, ok, "expected 200 response, got %T", resp) + + raw, err := json.Marshal(okResp) + require.NoError(t, err) + var decoded map[string]any + require.NoError(t, json.Unmarshal(raw, &decoded)) + for _, field := range []string{"cpu", "memory", "disk", "network", "allocations"} { + require.Contains(t, decoded, field) + } +} diff --git a/cmd/api/api/capabilities.go b/cmd/api/api/capabilities.go new file mode 100644 index 000000000..7be4d5bb0 --- /dev/null +++ b/cmd/api/api/capabilities.go @@ -0,0 +1,233 @@ +package api + +import ( + "context" + "runtime" + "sync" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/logger" + "github.com/kernel/hypeman/lib/network" + "github.com/kernel/hypeman/lib/oapi" +) + +// Stable feature IDs reported by the capabilities endpoint. Clients gate +// behavior on these (and the structured runtime booleans) rather than on +// hypervisor names. +const ( + featureInstances = "instances" + featureImages = "images" + featureBuilds = "builds" + featureVolumes = "volumes" + featureIngress = "ingress" + featureExec = "exec" + featureLogs = "logs" + featureStandby = "standby" + featureSnapshots = "snapshots" + featureFork = "fork" + featurePause = "pause" + featureHotplugMemory = "hotplug-memory" + featureBalloonControl = "balloon-control" + featureVsock = "vsock" + featureGPUPassthrough = "gpu-passthrough" + featureDiskIOLimit = "disk-io-limit" + featureDiskResize = "disk-resize" + featureDevices = "devices" + featureRosettaEmulation = "rosetta-emulation" +) + +// apiVersion is the API contract version from the embedded OpenAPI document. +// The decoded spec is cached: decoding it per request is needlessly expensive. +var apiVersion = sync.OnceValue(func() string { + spec, err := oapi.GetSwagger() + if err != nil || spec.Info == nil { + return "unknown" + } + return spec.Info.Version +}) + +// GetCapabilities reports host, runtime, network, and image capabilities. +func (s *ApiService) GetCapabilities(ctx context.Context, _ oapi.GetCapabilitiesRequestObject) (oapi.GetCapabilitiesResponseObject, error) { + log := logger.FromContext(ctx) + + defaultRuntime := hypervisor.TypeCloudHypervisor + if s.InstanceManager != nil { + defaultRuntime = s.InstanceManager.DefaultHypervisor() + } + caps, capsKnown := hypervisor.CapabilitiesForType(defaultRuntime) + if !capsKnown { + // The configured default runtime is not available on this platform; + // report zeroed features rather than guessing. + log.WarnContext(ctx, "default runtime has no registered capabilities on this host", + "runtime", string(defaultRuntime)) + } + + emulation := emulationSupported(runtime.GOOS, runtime.GOARCH, defaultRuntime) + + networkCaps, err := s.networkCapabilities(ctx) + if err != nil { + log.ErrorContext(ctx, "failed to resolve network capabilities", "error", err) + return oapi.GetCapabilities500JSONResponse{ + Code: "internal_error", + Message: "failed to resolve network capabilities", + }, nil + } + + resp := oapi.Capabilities{ + Server: oapi.CapabilitiesServer{ + Version: s.Config.Version, + ApiVersion: apiVersion(), + }, + Host: oapi.CapabilitiesHost{ + Os: runtime.GOOS, + Arch: runtime.GOARCH, + }, + Runtime: oapi.CapabilitiesRuntime{ + Default: string(defaultRuntime), + Supported: supportedRuntimes(runtime.GOOS), + Snapshot: caps.SupportsSnapshot, + Standby: standbySupported(caps), + Pause: caps.SupportsPause, + HotplugMemory: caps.SupportsHotplugMemory, + BalloonControl: caps.SupportsBalloonControl, + Vsock: caps.SupportsVsock, + GpuPassthrough: caps.SupportsGPUPassthrough, + DiskIoLimit: caps.SupportsDiskIOLimit, + DiskResize: caps.SupportsDiskResize, + }, + Network: *networkCaps, + Images: oapi.CapabilitiesImages{ + Platforms: imagePlatforms(runtime.GOARCH, emulation), + DefaultPlatform: images.HostPlatformString(), + }, + Features: assembleFeatures(runtime.GOOS, caps, emulation), + } + + return oapi.GetCapabilities200JSONResponse(resp), nil +} + +// networkCapabilities resolves the guest networking model and the +// guest-visible host gateway from the network manager's effective default +// network. +func (s *ApiService) networkCapabilities(ctx context.Context) (*oapi.CapabilitiesNetwork, error) { + caps := &oapi.CapabilitiesNetwork{ + Model: oapi.CapabilitiesNetworkModel(network.NetworkModel()), + GuestToGuest: false, + } + if s.NetworkManager == nil { + return caps, nil + } + nw, err := s.NetworkManager.DefaultNetwork(ctx) + if err != nil { + return nil, err + } + if nw == nil { + return caps, nil + } + caps.Gateway = nw.Gateway + if nw.Subnet != "" { + subnet := nw.Subnet + caps.Subnet = &subnet + } + caps.GuestToGuest = network.GuestToGuestEnabled(nw) + return caps, nil +} + +// standbySupported reports whether standby (pause + memory snapshot) and +// later restore are supported. Standby requires both snapshot and pause. +func standbySupported(caps hypervisor.Capabilities) bool { + return caps.SupportsSnapshot && caps.SupportsPause +} + +// supportedRuntimes returns the runtime identifiers usable on a host OS. +// This is a platform floor, not a registry listing: a runtime whose package +// registered capabilities but that cannot run on the host (e.g. firecracker +// on macOS) is not listed. +func supportedRuntimes(goos string) []string { + switch goos { + case "darwin": + return []string{string(hypervisor.TypeVZ)} + default: + return []string{ + string(hypervisor.TypeCloudHypervisor), + string(hypervisor.TypeFirecracker), + string(hypervisor.TypeQEMU), + } + } +} + +// emulationSupported reports whether the host can boot images built for the +// other CPU architecture. This mirrors the create-path rule for attaching +// the Rosetta share: vz on Apple Silicon macOS. +func emulationSupported(goos, goarch string, defaultRuntime hypervisor.Type) bool { + return defaultRuntime == hypervisor.TypeVZ && goos == "darwin" && goarch == "arm64" +} + +// imagePlatforms returns the image platforms (os/arch) the host can run: +// the host-native platform plus the emulated one when available. +func imagePlatforms(goarch string, emulation bool) []string { + if goarch == "" { + goarch = runtime.GOARCH + } + platforms := []string{"linux/" + goarch} + if emulation { + switch goarch { + case "arm64": + platforms = append(platforms, "linux/amd64") + case "amd64": + platforms = append(platforms, "linux/arm64") + } + } + return platforms +} + +// assembleFeatures builds the stable feature ID list: always-present base API +// surfaces plus conditional entries derived from the effective default +// runtime's capabilities and the host platform. +func assembleFeatures(goos string, caps hypervisor.Capabilities, emulation bool) []string { + features := []string{ + featureInstances, + featureImages, + featureBuilds, + featureVolumes, + featureIngress, + featureExec, + featureLogs, + } + if standbySupported(caps) { + features = append(features, featureStandby) + } + if caps.SupportsSnapshot { + features = append(features, featureSnapshots, featureFork) + } + if caps.SupportsPause { + features = append(features, featurePause) + } + if caps.SupportsHotplugMemory { + features = append(features, featureHotplugMemory) + } + if caps.SupportsBalloonControl { + features = append(features, featureBalloonControl) + } + if caps.SupportsVsock { + features = append(features, featureVsock) + } + if caps.SupportsGPUPassthrough { + features = append(features, featureGPUPassthrough) + } + if caps.SupportsDiskIOLimit { + features = append(features, featureDiskIOLimit) + } + if caps.SupportsDiskResize { + features = append(features, featureDiskResize) + } + // Device passthrough (GPU/PCI) is only meaningful on Linux hosts. + if goos == "linux" { + features = append(features, featureDevices) + } + if emulation { + features = append(features, featureRosettaEmulation) + } + return features +} diff --git a/cmd/api/api/capabilities_test.go b/cmd/api/api/capabilities_test.go new file mode 100644 index 000000000..a9709bc4c --- /dev/null +++ b/cmd/api/api/capabilities_test.go @@ -0,0 +1,177 @@ +package api + +import ( + "context" + "runtime" + "testing" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/network" + "github.com/kernel/hypeman/lib/oapi" + "github.com/stretchr/testify/require" +) + +// stubCapabilitiesNetworkManager overrides DefaultNetwork with a fixed +// effective network so the handler test is hermetic (no bridge/netlink). +type stubCapabilitiesNetworkManager struct { + network.Manager + nw *network.Network + err error +} + +func (s *stubCapabilitiesNetworkManager) DefaultNetwork(ctx context.Context) (*network.Network, error) { + return s.nw, s.err +} + +func cloudHypervisorCaps(t *testing.T) hypervisor.Capabilities { + t.Helper() + caps, ok := hypervisor.CapabilitiesForType(hypervisor.TypeCloudHypervisor) + require.True(t, ok, "cloud-hypervisor capabilities must be registered") + return caps +} + +func TestGetCapabilities(t *testing.T) { + t.Parallel() + svc := newTestService(t) + svc.Config.Version = "testsha123" + svc.NetworkManager = &stubCapabilitiesNetworkManager{ + nw: &network.Network{ + Name: "default", + Subnet: "10.100.0.0/16", + Gateway: "10.100.0.1", + Isolated: true, + Default: true, + }, + } + + resp, err := svc.GetCapabilities(ctx(), oapi.GetCapabilitiesRequestObject{}) + require.NoError(t, err) + + okResp, ok := resp.(oapi.GetCapabilities200JSONResponse) + require.True(t, ok, "expected 200 response, got %T", resp) + caps := oapi.Capabilities(okResp) + + // Server identity and version serialization + require.Equal(t, "testsha123", caps.Server.Version) + require.Equal(t, apiVersion(), caps.Server.ApiVersion) + require.NotEmpty(t, caps.Server.ApiVersion) + require.NotEqual(t, "unknown", caps.Server.ApiVersion) + + // Host identity + require.Equal(t, runtime.GOOS, caps.Host.Os) + require.Equal(t, runtime.GOARCH, caps.Host.Arch) + + // Effective default runtime and its capabilities + require.Equal(t, string(hypervisor.TypeCloudHypervisor), caps.Runtime.Default) + require.NotEmpty(t, caps.Runtime.Supported) + require.Contains(t, caps.Runtime.Supported, caps.Runtime.Default) + if runtime.GOOS == "linux" { + chCaps := cloudHypervisorCaps(t) + require.Equal(t, chCaps.SupportsSnapshot, caps.Runtime.Snapshot) + require.Equal(t, chCaps.SupportsPause, caps.Runtime.Pause) + require.True(t, caps.Runtime.Standby) + } + + // Network model and guest-visible gateway + require.Equal(t, oapi.CapabilitiesNetworkModel(network.NetworkModel()), caps.Network.Model) + require.Equal(t, "10.100.0.1", caps.Network.Gateway) + require.NotNil(t, caps.Network.Subnet) + require.Equal(t, "10.100.0.0/16", *caps.Network.Subnet) + require.False(t, caps.Network.GuestToGuest, "isolated default network must report guest_to_guest=false") + + // Image platforms always include the host-native guest platform + require.Contains(t, caps.Images.Platforms, caps.Images.DefaultPlatform) + + // Base feature IDs are always present + for _, f := range []string{"instances", "images", "builds", "volumes", "ingress", "exec", "logs"} { + require.Contains(t, caps.Features, f) + } + if runtime.GOOS == "linux" { + require.Contains(t, caps.Features, "standby") + require.Contains(t, caps.Features, "devices") + } +} + +func TestGetCapabilitiesNetworkError(t *testing.T) { + t.Parallel() + svc := newTestService(t) + svc.NetworkManager = &stubCapabilitiesNetworkManager{err: context.DeadlineExceeded} + + resp, err := svc.GetCapabilities(ctx(), oapi.GetCapabilitiesRequestObject{}) + require.NoError(t, err) + _, ok := resp.(oapi.GetCapabilities500JSONResponse) + require.True(t, ok, "expected 500 response when network resolution fails, got %T", resp) +} + +func TestSupportedRuntimes(t *testing.T) { + t.Parallel() + require.Equal(t, + []string{"cloud-hypervisor", "firecracker", "qemu"}, + supportedRuntimes("linux")) + require.Equal(t, []string{"vz"}, supportedRuntimes("darwin")) +} + +func TestEmulationSupported(t *testing.T) { + t.Parallel() + require.True(t, emulationSupported("darwin", "arm64", hypervisor.TypeVZ)) + require.False(t, emulationSupported("darwin", "amd64", hypervisor.TypeVZ)) + require.False(t, emulationSupported("darwin", "arm64", hypervisor.TypeCloudHypervisor)) + require.False(t, emulationSupported("linux", "arm64", hypervisor.TypeVZ)) + require.False(t, emulationSupported("linux", "amd64", hypervisor.TypeCloudHypervisor)) +} + +func TestImagePlatforms(t *testing.T) { + t.Parallel() + require.Equal(t, []string{"linux/amd64"}, imagePlatforms("amd64", false)) + require.Equal(t, []string{"linux/arm64"}, imagePlatforms("arm64", false)) + require.Equal(t, + []string{"linux/arm64", "linux/amd64"}, + imagePlatforms("arm64", true), + "Apple Silicon macOS with vz advertises Rosetta-emulated amd64") +} + +func TestAssembleFeatures(t *testing.T) { + t.Parallel() + + base := []string{"instances", "images", "builds", "volumes", "ingress", "exec", "logs"} + + // Zero capabilities (unknown runtime on this host): base features only. + features := assembleFeatures("linux", hypervisor.Capabilities{}, false) + require.ElementsMatch(t, append(base, "devices"), features) + + // Full Linux cloud-hypervisor capabilities. + features = assembleFeatures("linux", cloudHypervisorCaps(t), false) + require.Subset(t, features, base) + require.Contains(t, features, "standby") + require.Contains(t, features, "snapshots") + require.Contains(t, features, "fork") + require.Contains(t, features, "pause") + require.Contains(t, features, "hotplug-memory") + require.Contains(t, features, "devices") + require.NotContains(t, features, "rosetta-emulation") + + // macOS: no devices feature; emulation adds rosetta. + vzCaps := hypervisor.Capabilities{SupportsSnapshot: true, SupportsPause: true, SupportsBalloonControl: true, SupportsVsock: true} + features = assembleFeatures("darwin", vzCaps, true) + require.NotContains(t, features, "devices") + require.Contains(t, features, "standby") + require.Contains(t, features, "rosetta-emulation") + require.NotContains(t, features, "hotplug-memory") + require.NotContains(t, features, "gpu-passthrough") +} + +func TestStandbyRequiresSnapshotAndPause(t *testing.T) { + t.Parallel() + require.False(t, standbySupported(hypervisor.Capabilities{SupportsSnapshot: true, SupportsPause: false})) + require.False(t, standbySupported(hypervisor.Capabilities{SupportsSnapshot: false, SupportsPause: true})) + require.True(t, standbySupported(hypervisor.Capabilities{SupportsSnapshot: true, SupportsPause: true})) +} + +// TestAPIVersionMatchesSpec guards the version contract: the endpoint must +// serialize the same version as the embedded OpenAPI document. +func TestAPIVersionMatchesSpec(t *testing.T) { + t.Parallel() + spec, err := oapi.GetSwagger() + require.NoError(t, err) + require.Equal(t, spec.Info.Version, apiVersion()) +} diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index d145fa9b4..8b213cfcb 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -19,6 +19,7 @@ import ( mw "github.com/kernel/hypeman/lib/middleware" "github.com/kernel/hypeman/lib/network" "github.com/kernel/hypeman/lib/oapi" + "github.com/kernel/hypeman/lib/redact" "github.com/kernel/hypeman/lib/resources" "github.com/kernel/hypeman/lib/snapshot" "github.com/kernel/hypeman/lib/vm_metrics" @@ -54,6 +55,9 @@ func (s *ApiService) ListInstances(ctx context.Context, request oapi.ListInstanc oapiInsts := make([]oapi.Instance, len(domainInsts)) for i, inst := range domainInsts { oapiInsts[i] = instanceToOAPI(inst) + if !includeEnv(request.Params.IncludeEnv) { + redactInstanceEnv(&oapiInsts[i]) + } } return oapi.ListInstances200JSONResponse(oapiInsts), nil @@ -411,7 +415,11 @@ func (s *ApiService) GetInstance(ctx context.Context, request oapi.GetInstanceRe Message: "resource not resolved", }, nil } - return oapi.GetInstance200JSONResponse(instanceToOAPI(*inst)), nil + oapiInst := instanceToOAPI(*inst) + if !includeEnv(request.Params.IncludeEnv) { + redactInstanceEnv(&oapiInst) + } + return oapi.GetInstance200JSONResponse(oapiInst), nil } // GetInstanceStats returns resource utilization statistics for an instance @@ -1320,3 +1328,19 @@ func parseOptionalDuration(value string, field string) (*time.Duration, error) { } return &duration, nil } + +// includeEnv reports whether the caller opted in to plaintext env values. +func includeEnv(param *bool) bool { + return param != nil && *param +} + +// redactInstanceEnv replaces env values with the redaction sentinel, +// preserving keys. Env values are secrets; read APIs redact them unless the +// caller explicitly passes include_env=true. +func redactInstanceEnv(inst *oapi.Instance) { + if inst.Env == nil { + return + } + redacted := redact.Values(*inst.Env) + inst.Env = &redacted +} diff --git a/cmd/api/api/instances_redact_test.go b/cmd/api/api/instances_redact_test.go new file mode 100644 index 000000000..2bb06784b --- /dev/null +++ b/cmd/api/api/instances_redact_test.go @@ -0,0 +1,128 @@ +package api + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances" + mw "github.com/kernel/hypeman/lib/middleware" + "github.com/kernel/hypeman/lib/oapi" + "github.com/kernel/hypeman/lib/paths" + "github.com/kernel/hypeman/lib/redact" + "github.com/stretchr/testify/require" +) + +const envCanaryValue = "sk-canary-env-value-do-not-leak-7f3a9b" + +// writeInstanceMetadataFixture writes an instance metadata.json directly into +// the data dir so read APIs can be tested without booting a VM. +func writeInstanceMetadataFixture(t *testing.T, dataDir string, meta instances.StoredMetadata) { + t.Helper() + p := paths.New(dataDir) + require.NoError(t, os.MkdirAll(p.InstanceDir(meta.Id), 0755)) + data, err := json.MarshalIndent(meta, "", " ") + require.NoError(t, err) + require.NoError(t, os.WriteFile(p.InstanceMetadata(meta.Id), data, 0600)) +} + +func envCanaryInstance() instances.Instance { + return instances.Instance{ + StoredMetadata: instances.StoredMetadata{ + Id: "inst-env-canary", + Name: "env-canary", + Image: "docker.io/library/alpine:latest", + Env: map[string]string{"API_KEY": envCanaryValue, "PLAIN": "hello"}, + CreatedAt: time.Now(), + HypervisorType: hypervisor.TypeCloudHypervisor, + }, + State: instances.StateStopped, + } +} + +// TestListInstancesRedactsEnvByDefault proves the raw API no longer exposes +// env values on reads: keys are preserved, values are redacted, and the +// canary never appears unless include_env=true is passed. +func TestListInstancesRedactsEnvByDefault(t *testing.T) { + t.Parallel() + svc := newTestService(t) + inst := envCanaryInstance() + writeInstanceMetadataFixture(t, svc.Config.DataDir, inst.StoredMetadata) + + // Default: redacted. + resp, err := svc.ListInstances(ctx(), oapi.ListInstancesRequestObject{}) + require.NoError(t, err) + list, ok := resp.(oapi.ListInstances200JSONResponse) + require.True(t, ok, "expected 200 response, got %T", resp) + require.Len(t, list, 1) + require.NotNil(t, list[0].Env) + require.Equal(t, redact.Sentinel, (*list[0].Env)["API_KEY"]) + require.Equal(t, redact.Sentinel, (*list[0].Env)["PLAIN"]) + raw, err := json.Marshal(list) + require.NoError(t, err) + require.NotContains(t, string(raw), envCanaryValue, "redacted list response leaked env canary") + + // Opt-in: plaintext. + include := true + resp, err = svc.ListInstances(ctx(), oapi.ListInstancesRequestObject{ + Params: oapi.ListInstancesParams{IncludeEnv: &include}, + }) + require.NoError(t, err) + list, ok = resp.(oapi.ListInstances200JSONResponse) + require.True(t, ok, "expected 200 response, got %T", resp) + require.Len(t, list, 1) + require.Equal(t, envCanaryValue, (*list[0].Env)["API_KEY"]) +} + +// TestGetInstanceRedactsEnvByDefault covers the single-instance read path. +func TestGetInstanceRedactsEnvByDefault(t *testing.T) { + t.Parallel() + svc := newTestService(t) + inst := envCanaryInstance() + + resolvedCtx := mw.WithResolvedInstance(ctx(), inst.Id, inst) + + resp, err := svc.GetInstance(resolvedCtx, oapi.GetInstanceRequestObject{Id: inst.Id}) + require.NoError(t, err) + got, ok := resp.(oapi.GetInstance200JSONResponse) + require.True(t, ok, "expected 200 response, got %T", resp) + require.NotNil(t, got.Env) + require.Equal(t, redact.Sentinel, (*got.Env)["API_KEY"]) + raw, err := json.Marshal(got) + require.NoError(t, err) + require.NotContains(t, string(raw), envCanaryValue, "redacted get response leaked env canary") + + include := true + resp, err = svc.GetInstance(resolvedCtx, oapi.GetInstanceRequestObject{ + Id: inst.Id, + Params: oapi.GetInstanceParams{IncludeEnv: &include}, + }) + require.NoError(t, err) + got, ok = resp.(oapi.GetInstance200JSONResponse) + require.True(t, ok, "expected 200 response, got %T", resp) + require.Equal(t, envCanaryValue, (*got.Env)["API_KEY"]) +} + +func TestIncludeEnvParam(t *testing.T) { + t.Parallel() + require.False(t, includeEnv(nil)) + f := false + require.False(t, includeEnv(&f)) + tr := true + require.True(t, includeEnv(&tr)) +} + +// TestMetadataFixturePerms keeps the fixture helper honest: fixtures on disk +// must carry the sentinel value so the redaction assertions above are +// meaningful. +func TestMetadataFixturePerms(t *testing.T) { + t.Parallel() + dir := t.TempDir() + writeInstanceMetadataFixture(t, dir, envCanaryInstance().StoredMetadata) + data, err := os.ReadFile(filepath.Join(dir, "guests", "inst-env-canary", "metadata.json")) + require.NoError(t, err) + require.Contains(t, string(data), envCanaryValue) +} diff --git a/cmd/api/config/config.go b/cmd/api/config/config.go index 366fb4962..c63085503 100644 --- a/cmd/api/config/config.go +++ b/cmd/api/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "log/slog" "os" "path/filepath" "runtime" @@ -516,6 +517,8 @@ func Load(configPath string) (*Config, error) { return nil, fmt.Errorf("failed to load config from %s: %w", configPath, err) } // Auto-discovered path failed — continue with defaults + env + } else { + warnIfConfigFileWorldReadable(configPath) } } @@ -541,6 +544,29 @@ func Load(configPath string) (*Config, error) { return &cfg, nil } +// configFilePermTooOpen reports whether a config file is readable or +// writable by group or others. Config files typically carry jwt_secret and +// other credentials, so they should be owner-only (0600). +func configFilePermTooOpen(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return info.Mode().Perm()&0o077 != 0 +} + +// warnIfConfigFileWorldReadable logs a warning when the config file (which +// typically carries jwt_secret and other credentials) is accessible by group +// or others. Hypeman never chmods operator-owned files; it only warns. +func warnIfConfigFileWorldReadable(path string) { + if !configFilePermTooOpen(path) { + return + } + info, _ := os.Stat(path) + slog.Warn("config file is accessible by group or others; it may contain secrets — consider chmod 0600", + "path", path, "mode", info.Mode().Perm().String()) +} + func (c *Config) expandPathFields() { c.DataDir = expandHomePath(c.DataDir) c.Build.SecretsDir = expandHomePath(c.Build.SecretsDir) diff --git a/cmd/api/config/config_test.go b/cmd/api/config/config_test.go index 93c236c3e..0a02535c7 100644 --- a/cmd/api/config/config_test.go +++ b/cmd/api/config/config_test.go @@ -539,3 +539,53 @@ func TestValidateAllowsDisabledSnapshotCompressionDefaultWithoutValidAlgorithm(t t.Fatalf("expected disabled snapshot compression default to ignore algorithm/level, got %v", err) } } + +func TestConfigFilePermTooOpen(t *testing.T) { + tmp := t.TempDir() + cfgPath := filepath.Join(tmp, "config.yaml") + + write := func(mode os.FileMode) { + if err := os.WriteFile(cfgPath, []byte("jwt_secret: test-secret\n"), mode); err != nil { + t.Fatalf("write temp config: %v", err) + } + // WriteFile does not change perms of an existing file; chmod explicitly. + if err := os.Chmod(cfgPath, mode); err != nil { + t.Fatalf("chmod temp config: %v", err) + } + } + + write(0600) + if configFilePermTooOpen(cfgPath) { + t.Fatalf("0600 config file must not be flagged") + } + write(0640) + if !configFilePermTooOpen(cfgPath) { + t.Fatalf("0640 config file must be flagged") + } + write(0644) + if !configFilePermTooOpen(cfgPath) { + t.Fatalf("0644 config file must be flagged") + } + if configFilePermTooOpen(filepath.Join(tmp, "missing.yaml")) { + t.Fatalf("missing file must not be flagged") + } +} + +func TestLoadSucceedsWithWorldReadableConfigFile(t *testing.T) { + clearPathEnvOverrides(t) + + tmp := t.TempDir() + cfgPath := filepath.Join(tmp, "config.yaml") + if err := os.WriteFile(cfgPath, []byte("jwt_secret: test-secret\n"), 0644); err != nil { + t.Fatalf("write temp config: %v", err) + } + + // A too-open config file only warns; it must not break loading. + cfg, err := Load(cfgPath) + if err != nil { + t.Fatalf("load config: %v", err) + } + if cfg.JwtSecret != "test-secret" { + t.Fatalf("expected jwt_secret from config file, got %q", cfg.JwtSecret) + } +} diff --git a/lib/builds/manager_test.go b/lib/builds/manager_test.go index c26295eae..63c82f562 100644 --- a/lib/builds/manager_test.go +++ b/lib/builds/manager_test.go @@ -74,6 +74,10 @@ func (m *mockInstanceManager) CreateInstance(ctx context.Context, req instances. return inst, nil } +func (m *mockInstanceManager) DefaultHypervisor() hypervisor.Type { + return hypervisor.TypeCloudHypervisor +} + func (m *mockInstanceManager) GetInstance(ctx context.Context, id string) (*instances.Instance, error) { if m.getFunc != nil { return m.getFunc(ctx, id) diff --git a/lib/builds/storage.go b/lib/builds/storage.go index ef03b4838..93e55f6a5 100644 --- a/lib/builds/storage.go +++ b/lib/builds/storage.go @@ -58,9 +58,10 @@ func writeMetadata(p *paths.Paths, meta *buildMetadata) error { return fmt.Errorf("marshal metadata: %w", err) } - // Write atomically via temp file + // Write atomically via temp file. Build metadata embeds the original + // request (build args, secret references), so keep it owner-only. tempPath := p.BuildMetadata(meta.ID) + ".tmp" - if err := os.WriteFile(tempPath, data, 0644); err != nil { + if err := os.WriteFile(tempPath, data, 0600); err != nil { return fmt.Errorf("write temp metadata: %w", err) } @@ -232,7 +233,8 @@ func writeBuildConfig(p *paths.Paths, id string, config *BuildConfig) error { } configPath := p.BuildConfig(id) - if err := os.WriteFile(configPath, data, 0644); err != nil { + // The build config carries the registry push token and build args. + if err := os.WriteFile(configPath, data, 0600); err != nil { return fmt.Errorf("write build config: %w", err) } diff --git a/lib/hypervisor/vz/client.go b/lib/hypervisor/vz/client.go index 2fb0a62af..f0116bb97 100644 --- a/lib/hypervisor/vz/client.go +++ b/lib/hypervisor/vz/client.go @@ -10,7 +10,6 @@ import ( "io" "net" "net/http" - "runtime" "time" "github.com/kernel/hypeman/lib/hypervisor" @@ -84,7 +83,10 @@ func (c *Client) Capabilities() hypervisor.Capabilities { func capabilities() hypervisor.Capabilities { return hypervisor.Capabilities{ - SupportsSnapshot: runtime.GOARCH == "arm64", + // Snapshot/standby support is runtime-derived: it requires Apple + // Silicon AND macOS 14+. An arm64-only check would overstate + // support on macOS 13 hosts. + SupportsSnapshot: saveRestoreSupported(), SupportsHotplugMemory: false, SupportsBalloonControl: true, SupportsPause: true, diff --git a/lib/hypervisor/vz/save_restore_support.go b/lib/hypervisor/vz/save_restore_support.go new file mode 100644 index 000000000..a62644183 --- /dev/null +++ b/lib/hypervisor/vz/save_restore_support.go @@ -0,0 +1,40 @@ +package vz + +import ( + "strconv" + "strings" +) + +// saveRestoreMinMacOSMajor is the minimum macOS major version whose +// Virtualization.framework exposes VM save/restore (VZVirtualMachine +// saveMachineStateToURL / restoreMachineStateFromURL). +const saveRestoreMinMacOSMajor = 14 + +// SaveRestoreSupported reports whether Virtualization.framework VM +// save/restore (snapshots, and therefore standby) is available on a host with +// the given GOOS, GOARCH, and macOS product version (e.g. "14.5.1"). The +// product version is only consulted on darwin/arm64; an empty or unparsable +// version is treated as unsupported so a failed probe never overstates +// support. +func SaveRestoreSupported(goos, goarch, productVersion string) bool { + if goos != "darwin" || goarch != "arm64" { + return false + } + major, ok := parseMacOSMajorVersion(productVersion) + return ok && major >= saveRestoreMinMacOSMajor +} + +// parseMacOSMajorVersion extracts the major component from a macOS product +// version string like "14", "14.5", or "14.5.1". +func parseMacOSMajorVersion(productVersion string) (int, bool) { + productVersion = strings.TrimSpace(productVersion) + if productVersion == "" { + return 0, false + } + majorStr, _, _ := strings.Cut(productVersion, ".") + major, err := strconv.Atoi(majorStr) + if err != nil || major < 0 { + return 0, false + } + return major, true +} diff --git a/lib/hypervisor/vz/save_restore_support_darwin.go b/lib/hypervisor/vz/save_restore_support_darwin.go new file mode 100644 index 000000000..6b8833e60 --- /dev/null +++ b/lib/hypervisor/vz/save_restore_support_darwin.go @@ -0,0 +1,26 @@ +//go:build darwin + +package vz + +import ( + "runtime" + + "golang.org/x/sys/unix" +) + +// macOSProductVersion returns the host macOS product version (e.g. "14.5.1"). +// It is a variable so tests can stub the probe. +var macOSProductVersion = func() string { + v, err := unix.Sysctl("kern.osproductversion") + if err != nil { + return "" + } + return v +} + +// saveRestoreSupported reports whether this host supports VZ VM save/restore. +// Detection is runtime-derived (Apple Silicon + macOS 14+) so static +// capabilities never overstate snapshot/standby support on older macOS. +func saveRestoreSupported() bool { + return SaveRestoreSupported(runtime.GOOS, runtime.GOARCH, macOSProductVersion()) +} diff --git a/lib/hypervisor/vz/save_restore_support_darwin_test.go b/lib/hypervisor/vz/save_restore_support_darwin_test.go new file mode 100644 index 000000000..5e180be6b --- /dev/null +++ b/lib/hypervisor/vz/save_restore_support_darwin_test.go @@ -0,0 +1,25 @@ +//go:build darwin + +package vz + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestCapabilitiesDeriveSnapshotFromHost proves the hypervisor's advertised +// capabilities track the probed macOS version instead of a static arch check. +func TestCapabilitiesDeriveSnapshotFromHost(t *testing.T) { + orig := macOSProductVersion + t.Cleanup(func() { macOSProductVersion = orig }) + + macOSProductVersion = func() string { return "13.6" } + require.False(t, capabilities().SupportsSnapshot, "macOS 13 must not advertise snapshot/standby support") + + macOSProductVersion = func() string { return "14.5" } + require.Equal(t, saveRestoreSupported(), capabilities().SupportsSnapshot) + + macOSProductVersion = func() string { return "" } + require.False(t, capabilities().SupportsSnapshot, "failed version probe must not advertise snapshot support") +} diff --git a/lib/hypervisor/vz/save_restore_support_test.go b/lib/hypervisor/vz/save_restore_support_test.go new file mode 100644 index 000000000..6b3d4c687 --- /dev/null +++ b/lib/hypervisor/vz/save_restore_support_test.go @@ -0,0 +1,33 @@ +package vz + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSaveRestoreSupported(t *testing.T) { + tests := []struct { + name string + goos string + goarch string + productVersion string + want bool + }{ + {name: "linux arm64 is never vz", goos: "linux", goarch: "arm64", productVersion: "14.5", want: false}, + {name: "darwin amd64 unsupported", goos: "darwin", goarch: "amd64", productVersion: "14.5", want: false}, + {name: "macOS 13 overstates nothing", goos: "darwin", goarch: "arm64", productVersion: "13.6", want: false}, + {name: "macOS 14 minimum", goos: "darwin", goarch: "arm64", productVersion: "14.0", want: true}, + {name: "macOS 14 patch", goos: "darwin", goarch: "arm64", productVersion: "14.5.1", want: true}, + {name: "macOS 15", goos: "darwin", goarch: "arm64", productVersion: "15.2", want: true}, + {name: "macOS 26", goos: "darwin", goarch: "arm64", productVersion: "26.0", want: true}, + {name: "empty version is unsupported", goos: "darwin", goarch: "arm64", productVersion: "", want: false}, + {name: "unparsable version is unsupported", goos: "darwin", goarch: "arm64", productVersion: "unknown", want: false}, + {name: "major-only version", goos: "darwin", goarch: "arm64", productVersion: "14", want: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, SaveRestoreSupported(tt.goos, tt.goarch, tt.productVersion)) + }) + } +} diff --git a/lib/instances/configdisk.go b/lib/instances/configdisk.go index 6b1d74f0b..1471c2562 100644 --- a/lib/instances/configdisk.go +++ b/lib/instances/configdisk.go @@ -32,7 +32,7 @@ func (m *manager) createConfigDisk(ctx context.Context, inst *Instance, imageInf return fmt.Errorf("marshal config: %w", err) } configPath := filepath.Join(tmpDir, "config.json") - if err := os.WriteFile(configPath, configData, 0644); err != nil { + if err := os.WriteFile(configPath, configData, 0600); err != nil { return fmt.Errorf("write config.json: %w", err) } @@ -43,6 +43,11 @@ func (m *manager) createConfigDisk(ctx context.Context, inst *Instance, imageInf return fmt.Errorf("create config disk: %w", err) } + // The config disk embeds config.json including environment values. + if err := os.Chmod(diskPath, 0600); err != nil { + return fmt.Errorf("chmod config disk: %w", err) + } + return nil } diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 8e8e25f3f..2452d921d 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -27,6 +27,9 @@ import ( type Manager interface { ListInstances(ctx context.Context, filter *ListInstancesFilter) ([]Instance, error) + // DefaultHypervisor returns the effective default hypervisor type used for + // launches that do not specify one. + DefaultHypervisor() hypervisor.Type ListSnapshots(ctx context.Context, filter *ListSnapshotsFilter) ([]Snapshot, error) GetSnapshot(ctx context.Context, snapshotID string) (*Snapshot, error) CreateInstance(ctx context.Context, req CreateInstanceRequest) (*Instance, error) @@ -305,6 +308,10 @@ func NewManagerWithConfigE(p *paths.Paths, imageManager images.Manager, systemMa logger.FromContext(context.Background()).WarnContext(context.Background(), "failed to recover pending standby compression jobs", "error", err) } + // Restrict permissions on metadata written by older versions (may be 0644 + // and contains env values / credential bindings). + m.tightenMetadataPermissions() + return m, nil } @@ -690,6 +697,12 @@ func (m *manager) UpdateInstance(ctx context.Context, id string, req UpdateInsta return inst, err } +// DefaultHypervisor returns the effective default hypervisor type used for +// launches that do not specify one. +func (m *manager) DefaultHypervisor() hypervisor.Type { + return m.defaultHypervisor +} + // ListInstances returns instances, optionally filtered by the given criteria. // Pass nil to return all instances. func (m *manager) ListInstances(ctx context.Context, filter *ListInstancesFilter) ([]Instance, error) { diff --git a/lib/instances/metadata_permissions_test.go b/lib/instances/metadata_permissions_test.go new file mode 100644 index 000000000..ccd726ce2 --- /dev/null +++ b/lib/instances/metadata_permissions_test.go @@ -0,0 +1,112 @@ +package instances + +import ( + "encoding/json" + "os" + "testing" + "time" + + "github.com/kernel/hypeman/cmd/api/config" + "github.com/kernel/hypeman/lib/devices" + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/network" + "github.com/kernel/hypeman/lib/paths" + "github.com/kernel/hypeman/lib/redact" + "github.com/kernel/hypeman/lib/system" + "github.com/kernel/hypeman/lib/volumes" + "github.com/stretchr/testify/require" +) + +// newPermTestManager builds a minimal manager for metadata permission tests. +// It does not initialize networking or boot anything. +func newPermTestManager(t *testing.T, dataDir string) *manager { + t.Helper() + cfg := &config.Config{DataDir: dataDir} + p := paths.New(dataDir) + imageManager, err := images.NewManager(p, 1, nil) + require.NoError(t, err) + systemManager := system.NewManager(p) + networkManager := network.NewManager(p, cfg, nil) + deviceManager := devices.NewManager(p) + volumeManager := volumes.NewManager(p, 0, nil) + mgr := NewManager(p, imageManager, systemManager, networkManager, deviceManager, volumeManager, + ResourceLimits{}, "", SnapshotPolicy{}, nil, nil).(*manager) + return mgr +} + +func metadataFixture(id string) *metadata { + return &metadata{ + StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + Image: "docker.io/library/alpine:latest", + Env: map[string]string{"API_KEY": "secret-canary-value"}, + CreatedAt: time.Now(), + HypervisorType: hypervisor.TypeCloudHypervisor, + }, + } +} + +// TestSaveMetadataRestrictivePermissions proves instance metadata (which +// contains env values and credential bindings) is written owner-only. +func TestSaveMetadataRestrictivePermissions(t *testing.T) { + t.Parallel() + mgr := newPermTestManager(t, t.TempDir()) + meta := metadataFixture("inst-perms-save") + + require.NoError(t, mgr.ensureDirectories(meta.Id)) + require.NoError(t, mgr.saveMetadata(meta)) + + info, err := os.Stat(mgr.paths.InstanceMetadata(meta.Id)) + require.NoError(t, err) + require.Equal(t, os.FileMode(0600), info.Mode().Perm(), + "instance metadata must be owner-only; it contains env values") +} + +// TestManagerTightensLegacyMetadataPermissions proves the startup sweep +// upgrades metadata files written by older versions (mode 0644) to 0600. +func TestManagerTightensLegacyMetadataPermissions(t *testing.T) { + t.Parallel() + dataDir := t.TempDir() + p := paths.New(dataDir) + + // Simulate legacy metadata written with world-readable permissions. + meta := metadataFixture("inst-perms-legacy") + require.NoError(t, os.MkdirAll(p.InstanceDir(meta.Id), 0755)) + data, err := json.MarshalIndent(meta, "", " ") + require.NoError(t, err) + require.NoError(t, os.WriteFile(p.InstanceMetadata(meta.Id), data, 0644)) + + newPermTestManager(t, dataDir) + + info, err := os.Stat(p.InstanceMetadata(meta.Id)) + require.NoError(t, err) + require.Equal(t, os.FileMode(0600), info.Mode().Perm(), + "legacy 0644 metadata must be tightened to 0600 at startup") +} + +// TestMergeEnvUpdateSkipsRedactionSentinel proves a redacted read response +// round-tripped into an env update cannot clobber real secret values. +func TestMergeEnvUpdateSkipsRedactionSentinel(t *testing.T) { + t.Parallel() + prev := map[string]string{"API_KEY": "real-secret", "PLAIN": "keep"} + + next := mergeEnvUpdate(prev, map[string]string{ + "API_KEY": redact.Sentinel, // redacted placeholder, must not overwrite + "PLAIN": "updated", + "NEW": "added", + }) + + require.Equal(t, map[string]string{ + "API_KEY": "real-secret", + "PLAIN": "updated", + "NEW": "added", + }, next) + // Previous map must not be mutated. + require.Equal(t, "keep", prev["PLAIN"]) + + // Nil previous env still yields a usable map. + next = mergeEnvUpdate(nil, map[string]string{"A": "1"}) + require.Equal(t, map[string]string{"A": "1"}, next) +} diff --git a/lib/instances/storage.go b/lib/instances/storage.go index f33a59621..f5925f7a0 100644 --- a/lib/instances/storage.go +++ b/lib/instances/storage.go @@ -1,6 +1,7 @@ package instances import ( + "context" "encoding/json" "errors" "fmt" @@ -13,6 +14,7 @@ import ( "github.com/kernel/hypeman/lib/healthcheck" "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/logger" ) const ( @@ -106,7 +108,9 @@ func (m *manager) saveMetadata(meta *metadata) error { tmp.Close() return fmt.Errorf("write temporary metadata: %w", err) } - if err := tmp.Chmod(0644); err != nil { + // Instance metadata contains environment variable values and credential + // bindings, so it must never be world-readable. + if err := tmp.Chmod(0600); err != nil { tmp.Close() return fmt.Errorf("chmod temporary metadata: %w", err) } @@ -123,6 +127,33 @@ func (m *manager) saveMetadata(meta *metadata) error { return nil } +// tightenMetadataPermissions restricts existing instance metadata files to +// owner-only access. Metadata written before restrictive permissions were +// introduced may be mode 0644; it contains env values and credential +// bindings. Best-effort: individual failures are logged, not fatal. +func (m *manager) tightenMetadataPermissions() { + log := logger.FromContext(context.Background()) + entries, err := os.ReadDir(m.paths.GuestsDir()) + if err != nil { + return // no guests directory yet + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + metaPath := m.paths.InstanceMetadata(entry.Name()) + info, err := os.Stat(metaPath) + if err != nil { + continue + } + if info.Mode().Perm() != 0600 { + if err := os.Chmod(metaPath, 0600); err != nil { + log.Warn("failed to tighten instance metadata permissions", "path", metaPath, "error", err) + } + } + } +} + // createOverlayDisk creates a sparse overlay disk for the instance func (m *manager) createOverlayDisk(id string, sizeBytes int64) error { overlayPath := m.paths.InstanceOverlay(id) diff --git a/lib/instances/update.go b/lib/instances/update.go index cdea9e4cf..8ac534d81 100644 --- a/lib/instances/update.go +++ b/lib/instances/update.go @@ -8,6 +8,7 @@ import ( "github.com/kernel/hypeman/lib/egressproxy" "github.com/kernel/hypeman/lib/logger" + "github.com/kernel/hypeman/lib/redact" ) type updateInstanceRulesService interface { @@ -82,13 +83,7 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta } prevEnv := cloneEnvMap(nextMeta.Env) - nextEnv := cloneEnvMap(nextMeta.Env) - if nextEnv == nil { - nextEnv = make(map[string]string) - } - for k, v := range req.Env { - nextEnv[k] = v - } + nextEnv := mergeEnvUpdate(prevEnv, req.Env) if err := validateCredentialEnvBindings(nextMeta.Credentials, nextEnv); err != nil { return nil, err @@ -113,6 +108,24 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta return updated, nil } +// mergeEnvUpdate applies requested env updates onto the previous env. A +// redaction sentinel round-tripped from a read response means "no value +// provided", never a literal secret, so sentinel values are skipped. This +// prevents a redacted read-modify-write cycle from clobbering real secrets. +func mergeEnvUpdate(prev, req map[string]string) map[string]string { + next := cloneEnvMap(prev) + if next == nil { + next = make(map[string]string) + } + for k, v := range req { + if redact.IsSentinel(v) { + continue + } + next[k] = v + } + return next +} + func validateUpdateInstanceRequest(meta *metadata, req UpdateInstanceRequest) error { if len(req.Env) == 0 && req.AutoStandby == nil && req.HealthCheck == nil && !req.RestartPolicySet { return fmt.Errorf("%w: request must include env, auto_standby, health_check, and/or restart_policy", ErrInvalidRequest) diff --git a/lib/instances/wait_test.go b/lib/instances/wait_test.go index bab6f06d7..640a60056 100644 --- a/lib/instances/wait_test.go +++ b/lib/instances/wait_test.go @@ -28,6 +28,10 @@ func (s *stubManager) GetInstance(ctx context.Context, id string) (*Instance, er return nil, ErrNotFound } +func (s *stubManager) DefaultHypervisor() hypervisor.Type { + return hypervisor.TypeCloudHypervisor +} + // Unused interface methods — only GetInstance and SubscribeLifecycleEvents are needed. func (s *stubManager) ListInstances(context.Context, *ListInstancesFilter) ([]Instance, error) { return nil, nil diff --git a/lib/middleware/access_log_test.go b/lib/middleware/access_log_test.go new file mode 100644 index 000000000..3802440b1 --- /dev/null +++ b/lib/middleware/access_log_test.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "bytes" + "log/slog" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestAccessLoggerRedactsAuthorizationCanary proves request diagnostics never +// carry credential material: a request bearing an Authorization canary must +// not appear in access log output. +func TestAccessLoggerRedactsAuthorizationCanary(t *testing.T) { + const canary = "eyJhbGciOiJIUzI1NiJ9.canary-auth-token-do-not-log.signature" + + var buf bytes.Buffer + log := slog.New(slog.NewJSONHandler(&buf, nil)) + + handler := AccessLogger(log)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/instances", nil) + req.Header.Set("Authorization", "Bearer "+canary) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + require.NotEmpty(t, buf.String(), "access logger should emit a record") + require.NotContains(t, buf.String(), canary, "access log leaked authorization token") + require.NotContains(t, buf.String(), "authorization", "access log should not record headers at all") +} diff --git a/lib/network/default_network_test.go b/lib/network/default_network_test.go new file mode 100644 index 000000000..3ce147d38 --- /dev/null +++ b/lib/network/default_network_test.go @@ -0,0 +1,51 @@ +package network + +import ( + "context" + "testing" + + "github.com/kernel/hypeman/cmd/api/config" + "github.com/kernel/hypeman/lib/paths" + "github.com/stretchr/testify/require" +) + +// TestDefaultNetworkPrefersInitializedNetwork proves DefaultNetwork returns +// the effective default network established at Initialize time, including the +// guest-visible gateway, without touching host kernel state. +func TestDefaultNetworkPrefersInitializedNetwork(t *testing.T) { + t.Parallel() + cfg := &config.Config{} + m := NewManager(paths.New(t.TempDir()), cfg, nil).(*manager) + + want := &Network{ + Name: "default", + Subnet: "10.100.0.0/16", + Gateway: "10.100.0.1", + Isolated: true, + Default: true, + } + m.setDefaultNetwork(want) + + nw, err := m.DefaultNetwork(context.Background()) + require.NoError(t, err) + require.Equal(t, "10.100.0.1", nw.Gateway) + require.Equal(t, "10.100.0.0/16", nw.Subnet) + + // Mutating the returned copy must not affect the cached network. + nw.Gateway = " mutated " + again, err := m.DefaultNetwork(context.Background()) + require.NoError(t, err) + require.Equal(t, "10.100.0.1", again.Gateway) +} + +func TestGuestToGuestEnabled(t *testing.T) { + t.Parallel() + require.False(t, GuestToGuestEnabled(&Network{Isolated: true}), + "isolated default network blocks direct guest-to-guest traffic") + if NetworkModel() == "bridge" { + require.True(t, GuestToGuestEnabled(&Network{Isolated: false})) + } else { + require.False(t, GuestToGuestEnabled(&Network{Isolated: false}), + "NAT networking never provides direct guest-to-guest reachability") + } +} diff --git a/lib/network/manager.go b/lib/network/manager.go index bc657117f..1c6f75e78 100644 --- a/lib/network/manager.go +++ b/lib/network/manager.go @@ -35,6 +35,10 @@ type Manager interface { ListAllocations(ctx context.Context) ([]Allocation, error) NameExists(ctx context.Context, name string, excludeInstanceID string) (bool, error) + // DefaultNetwork returns the effective default network, including the + // guest-visible host gateway and subnet for this host's networking model. + DefaultNetwork(ctx context.Context) (*Network, error) + // CleanupOrphanedTAPs removes TAP devices not associated with any preserved // instance. Pass minAge>0 to skip TAPs younger than that, which avoids racing // against in-flight CreateAllocation calls whose metadata hasn't been persisted. @@ -163,6 +167,16 @@ func (m *manager) setDefaultNetwork(network *Network) { m.defaultNetwork = cloneNetwork(network) } +// DefaultNetwork returns the effective default network. It prefers the +// network established during Initialize and falls back to querying live +// host state (kernel bridge on Linux, the vz NAT stub on macOS). +func (m *manager) DefaultNetwork(ctx context.Context) (*Network, error) { + if network := m.cachedDefaultNetwork(); network != nil { + return network, nil + } + return m.getDefaultNetwork(ctx) +} + // getDefaultNetwork gets the default network details from kernel state func (m *manager) getDefaultNetwork(ctx context.Context) (*Network, error) { // Query from kernel diff --git a/lib/network/model_darwin.go b/lib/network/model_darwin.go new file mode 100644 index 000000000..129ffa356 --- /dev/null +++ b/lib/network/model_darwin.go @@ -0,0 +1,16 @@ +//go:build darwin + +package network + +// NetworkModel identifies the guest networking model in use on this host. +// macOS hosts use Virtualization.framework-provided NAT. +func NetworkModel() string { + return "nat" +} + +// GuestToGuestEnabled reports whether direct VM-to-VM traffic is permitted on +// the given default network. Each vz guest sits behind its own NAT context, +// so direct guest-to-guest reachability is not provided. +func GuestToGuestEnabled(_ *Network) bool { + return false +} diff --git a/lib/network/model_linux.go b/lib/network/model_linux.go new file mode 100644 index 000000000..7a74a0f40 --- /dev/null +++ b/lib/network/model_linux.go @@ -0,0 +1,16 @@ +//go:build linux + +package network + +// NetworkModel identifies the guest networking model in use on this host. +// Linux hosts use a bridge with per-VM TAP devices and port isolation. +func NetworkModel() string { + return "bridge" +} + +// GuestToGuestEnabled reports whether direct VM-to-VM traffic is permitted on +// the given default network. Linux default networks use per-TAP port +// isolation, so guests cannot reach each other directly. +func GuestToGuestEnabled(n *Network) bool { + return n != nil && !n.Isolated +} diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index b98a2d004..0b549e321 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -73,6 +73,12 @@ const ( BuildStatusReady BuildStatus = "ready" ) +// Defines values for CapabilitiesNetworkModel. +const ( + Bridge CapabilitiesNetworkModel = "bridge" + Nat CapabilitiesNetworkModel = "nat" +) + // Defines values for CreateInstanceRequestHypervisor. const ( CreateInstanceRequestHypervisorCloudHypervisor CreateInstanceRequestHypervisor = "cloud-hypervisor" @@ -429,6 +435,110 @@ type BuildProvenance struct { // BuildStatus Build job status type BuildStatus string +// Capabilities defines model for Capabilities. +type Capabilities struct { + // Features Stable feature IDs clients can gate behavior on. Base IDs are always + // present: "instances", "images", "builds", "volumes", "ingress", + // "exec", "logs". Conditional IDs reflect the effective default + // runtime and host platform: "standby", "snapshots", "fork", + // "pause", "hotplug-memory", "balloon-control", "vsock", + // "gpu-passthrough", "disk-io-limit", "disk-resize", "devices", + // "rosetta-emulation". + Features []string `json:"features"` + Host CapabilitiesHost `json:"host"` + Images CapabilitiesImages `json:"images"` + Network CapabilitiesNetwork `json:"network"` + Runtime CapabilitiesRuntime `json:"runtime"` + Server CapabilitiesServer `json:"server"` +} + +// CapabilitiesHost defines model for CapabilitiesHost. +type CapabilitiesHost struct { + // Arch Host CPU architecture + Arch string `json:"arch"` + + // Os Host operating system + Os string `json:"os"` +} + +// CapabilitiesImages defines model for CapabilitiesImages. +type CapabilitiesImages struct { + // DefaultPlatform Image platform selected when a create request omits one + DefaultPlatform string `json:"default_platform"` + + // Platforms Image platforms (os/arch) this host can run. Includes emulated + // platforms (e.g. linux/amd64 via Rosetta on Apple Silicon macOS). + Platforms []string `json:"platforms"` +} + +// CapabilitiesNetwork defines model for CapabilitiesNetwork. +type CapabilitiesNetwork struct { + // Gateway Guest-visible host gateway IP. Guests reach host services (including + // host ingress) through this address. + Gateway string `json:"gateway"` + + // GuestToGuest Whether direct VM-to-VM traffic is permitted on the default network + GuestToGuest bool `json:"guest_to_guest"` + + // Model Guest networking model. "bridge" is a Linux bridge with per-VM TAP + // devices; "nat" is hypervisor-provided NAT (macOS). + Model CapabilitiesNetworkModel `json:"model"` + + // Subnet Guest subnet CIDR + Subnet *string `json:"subnet,omitempty"` +} + +// CapabilitiesNetworkModel Guest networking model. "bridge" is a Linux bridge with per-VM TAP +// devices; "nat" is hypervisor-provided NAT (macOS). +type CapabilitiesNetworkModel string + +// CapabilitiesRuntime defines model for CapabilitiesRuntime. +type CapabilitiesRuntime struct { + // BalloonControl Runtime memory balloon control is supported by the effective default runtime + BalloonControl bool `json:"balloon_control"` + + // Default Effective default runtime used for launches that do not name one. + // Reported as a capability-bearing runtime identifier; clients should + // gate behavior on the boolean features below rather than this value. + Default string `json:"default"` + + // DiskIoLimit Disk I/O rate limiting is supported by the effective default runtime + DiskIoLimit bool `json:"disk_io_limit"` + + // DiskResize Live disk resize is supported by the effective default runtime + DiskResize bool `json:"disk_resize"` + + // GpuPassthrough GPU/PCI device passthrough is supported by the effective default runtime + GpuPassthrough bool `json:"gpu_passthrough"` + + // HotplugMemory Live memory hotplug resize is supported by the effective default runtime + HotplugMemory bool `json:"hotplug_memory"` + + // Pause Pause/resume is supported by the effective default runtime + Pause bool `json:"pause"` + + // Snapshot Snapshot/restore is supported by the effective default runtime on this host + Snapshot bool `json:"snapshot"` + + // Standby Standby (pause + memory snapshot) and restore are supported on this host + Standby bool `json:"standby"` + + // Supported Runtime identifiers that can run on this host platform + Supported []string `json:"supported"` + + // Vsock Vsock guest communication is supported by the effective default runtime + Vsock bool `json:"vsock"` +} + +// CapabilitiesServer defines model for CapabilitiesServer. +type CapabilitiesServer struct { + // ApiVersion API contract version (matches the OpenAPI document info version) + ApiVersion string `json:"api_version"` + + // Version Server build version (short git revision, with "-dirty" suffix for uncommitted builds, or "unknown") + Version string `json:"version"` +} + // CreateDeviceRequest defines model for CreateDeviceRequest. type CreateDeviceRequest struct { // Name Optional globally unique device name. If not provided, a name is auto-generated from the PCI address (e.g., "pci-0000-a2-00-0") @@ -992,7 +1102,8 @@ type Instance struct { // DiskIoBps Disk I/O rate limit (human-readable, e.g., "100MB/s") DiskIoBps *string `json:"disk_io_bps,omitempty"` - // Env Environment variables + // Env Environment variables. Values are redacted ("[redacted]") in list/get + // responses unless the request sets `include_env=true`. Env *map[string]string `json:"env,omitempty"` // ExitCode App exit code (null if VM hasn't exited) @@ -1723,6 +1834,19 @@ type ListInstancesParams struct { // ?tags[team]=backend&tags[env]=staging // Multiple entries are ANDed together. All specified key-value pairs must match. Tags *Tags `json:"tags,omitempty"` + + // IncludeEnv Include plaintext environment variable values in the response. + // By default env values are redacted (keys are preserved, values are + // replaced with "[redacted]"). + IncludeEnv *bool `form:"include_env,omitempty" json:"include_env,omitempty"` +} + +// GetInstanceParams defines parameters for GetInstance. +type GetInstanceParams struct { + // IncludeEnv Include plaintext environment variable values in the response. + // By default env values are redacted (keys are preserved, values are + // replaced with "[redacted]"). + IncludeEnv *bool `form:"include_env,omitempty" json:"include_env,omitempty"` } // GetInstanceLogsParams defines parameters for GetInstanceLogs. @@ -1943,6 +2067,9 @@ type ClientInterface interface { // GetBuildEvents request GetBuildEvents(ctx context.Context, id string, params *GetBuildEventsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetCapabilities request + GetCapabilities(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListDevices request ListDevices(ctx context.Context, params *ListDevicesParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2003,7 +2130,7 @@ type ClientInterface interface { DeleteInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) // GetInstance request - GetInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + GetInstance(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*http.Response, error) // UpdateInstanceWithBody request with any body UpdateInstanceWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2178,6 +2305,18 @@ func (c *Client) GetBuildEvents(ctx context.Context, id string, params *GetBuild return c.Client.Do(req) } +func (c *Client) GetCapabilities(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetCapabilitiesRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ListDevices(ctx context.Context, params *ListDevicesParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewListDevicesRequest(c.Server, params) if err != nil { @@ -2430,8 +2569,8 @@ func (c *Client) DeleteInstance(ctx context.Context, id string, reqEditors ...Re return c.Client.Do(req) } -func (c *Client) GetInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetInstanceRequest(c.Server, id) +func (c *Client) GetInstance(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetInstanceRequest(c.Server, id, params) if err != nil { return nil, err } @@ -3136,6 +3275,33 @@ func NewGetBuildEventsRequest(server string, id string, params *GetBuildEventsPa return req, nil } +// NewGetCapabilitiesRequest generates requests for GetCapabilities +func NewGetCapabilitiesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/capabilities") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewListDevicesRequest generates requests for ListDevices func NewListDevicesRequest(server string, params *ListDevicesParams) (*http.Request, error) { var err error @@ -3715,6 +3881,22 @@ func NewListInstancesRequest(server string, params *ListInstancesParams) (*http. } + if params.IncludeEnv != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "include_env", runtime.ParamLocationQuery, *params.IncludeEnv); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + queryURL.RawQuery = queryValues.Encode() } @@ -3801,7 +3983,7 @@ func NewDeleteInstanceRequest(server string, id string) (*http.Request, error) { } // NewGetInstanceRequest generates requests for GetInstance -func NewGetInstanceRequest(server string, id string) (*http.Request, error) { +func NewGetInstanceRequest(server string, id string, params *GetInstanceParams) (*http.Request, error) { var err error var pathParam0 string @@ -3826,6 +4008,28 @@ func NewGetInstanceRequest(server string, id string) (*http.Request, error) { return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.IncludeEnv != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "include_env", runtime.ParamLocationQuery, *params.IncludeEnv); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -5312,6 +5516,9 @@ type ClientWithResponsesInterface interface { // GetBuildEventsWithResponse request GetBuildEventsWithResponse(ctx context.Context, id string, params *GetBuildEventsParams, reqEditors ...RequestEditorFn) (*GetBuildEventsResponse, error) + // GetCapabilitiesWithResponse request + GetCapabilitiesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetCapabilitiesResponse, error) + // ListDevicesWithResponse request ListDevicesWithResponse(ctx context.Context, params *ListDevicesParams, reqEditors ...RequestEditorFn) (*ListDevicesResponse, error) @@ -5372,7 +5579,7 @@ type ClientWithResponsesInterface interface { DeleteInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*DeleteInstanceResponse, error) // GetInstanceWithResponse request - GetInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) + GetInstanceWithResponse(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) // UpdateInstanceWithBodyWithResponse request with any body UpdateInstanceWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateInstanceResponse, error) @@ -5607,6 +5814,29 @@ func (r GetBuildEventsResponse) StatusCode() int { return 0 } +type GetCapabilitiesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Capabilities + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r GetCapabilitiesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetCapabilitiesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ListDevicesResponse struct { Body []byte HTTPResponse *http.Response @@ -6837,6 +7067,15 @@ func (c *ClientWithResponses) GetBuildEventsWithResponse(ctx context.Context, id return ParseGetBuildEventsResponse(rsp) } +// GetCapabilitiesWithResponse request returning *GetCapabilitiesResponse +func (c *ClientWithResponses) GetCapabilitiesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetCapabilitiesResponse, error) { + rsp, err := c.GetCapabilities(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetCapabilitiesResponse(rsp) +} + // ListDevicesWithResponse request returning *ListDevicesResponse func (c *ClientWithResponses) ListDevicesWithResponse(ctx context.Context, params *ListDevicesParams, reqEditors ...RequestEditorFn) (*ListDevicesResponse, error) { rsp, err := c.ListDevices(ctx, params, reqEditors...) @@ -7023,8 +7262,8 @@ func (c *ClientWithResponses) DeleteInstanceWithResponse(ctx context.Context, id } // GetInstanceWithResponse request returning *GetInstanceResponse -func (c *ClientWithResponses) GetInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) { - rsp, err := c.GetInstance(ctx, id, reqEditors...) +func (c *ClientWithResponses) GetInstanceWithResponse(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) { + rsp, err := c.GetInstance(ctx, id, params, reqEditors...) if err != nil { return nil, err } @@ -7589,6 +7828,39 @@ func ParseGetBuildEventsResponse(rsp *http.Response) (*GetBuildEventsResponse, e return response, nil } +// ParseGetCapabilitiesResponse parses an HTTP response from a GetCapabilitiesWithResponse call +func ParseGetCapabilitiesResponse(rsp *http.Response) (*GetCapabilitiesResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetCapabilitiesResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Capabilities + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseListDevicesResponse parses an HTTP response from a ListDevicesWithResponse call func ParseListDevicesResponse(rsp *http.Response) (*ListDevicesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -9757,6 +10029,9 @@ type ServerInterface interface { // Stream build events (SSE) // (GET /builds/{id}/events) GetBuildEvents(w http.ResponseWriter, r *http.Request, id string, params GetBuildEventsParams) + // Get host capabilities + // (GET /capabilities) + GetCapabilities(w http.ResponseWriter, r *http.Request) // List registered devices // (GET /devices) ListDevices(w http.ResponseWriter, r *http.Request, params ListDevicesParams) @@ -9810,7 +10085,7 @@ type ServerInterface interface { DeleteInstance(w http.ResponseWriter, r *http.Request, id string) // Get instance details // (GET /instances/{id}) - GetInstance(w http.ResponseWriter, r *http.Request, id string) + GetInstance(w http.ResponseWriter, r *http.Request, id string, params GetInstanceParams) // Update instance properties // (PATCH /instances/{id}) UpdateInstance(w http.ResponseWriter, r *http.Request, id string) @@ -9937,6 +10212,12 @@ func (_ Unimplemented) GetBuildEvents(w http.ResponseWriter, r *http.Request, id w.WriteHeader(http.StatusNotImplemented) } +// Get host capabilities +// (GET /capabilities) +func (_ Unimplemented) GetCapabilities(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // List registered devices // (GET /devices) func (_ Unimplemented) ListDevices(w http.ResponseWriter, r *http.Request, params ListDevicesParams) { @@ -10041,7 +10322,7 @@ func (_ Unimplemented) DeleteInstance(w http.ResponseWriter, r *http.Request, id // Get instance details // (GET /instances/{id}) -func (_ Unimplemented) GetInstance(w http.ResponseWriter, r *http.Request, id string) { +func (_ Unimplemented) GetInstance(w http.ResponseWriter, r *http.Request, id string, params GetInstanceParams) { w.WriteHeader(http.StatusNotImplemented) } @@ -10391,6 +10672,26 @@ func (siw *ServerInterfaceWrapper) GetBuildEvents(w http.ResponseWriter, r *http handler.ServeHTTP(w, r) } +// GetCapabilities operation middleware +func (siw *ServerInterfaceWrapper) GetCapabilities(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetCapabilities(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListDevices operation middleware func (siw *ServerInterfaceWrapper) ListDevices(w http.ResponseWriter, r *http.Request) { @@ -10800,6 +11101,14 @@ func (siw *ServerInterfaceWrapper) ListInstances(w http.ResponseWriter, r *http. return } + // ------------- Optional query parameter "include_env" ------------- + + err = runtime.BindQueryParameter("form", true, false, "include_env", r.URL.Query(), ¶ms.IncludeEnv) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include_env", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.ListInstances(w, r, params) })) @@ -10882,8 +11191,19 @@ func (siw *ServerInterfaceWrapper) GetInstance(w http.ResponseWriter, r *http.Re r = r.WithContext(ctx) + // Parameter object where we will unmarshal all parameters from the context + var params GetInstanceParams + + // ------------- Optional query parameter "include_env" ------------- + + err = runtime.BindQueryParameter("form", true, false, "include_env", r.URL.Query(), ¶ms.IncludeEnv) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include_env", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetInstance(w, r, id) + siw.Handler.GetInstance(w, r, id, params) })) for _, middleware := range siw.HandlerMiddlewares { @@ -12092,6 +12412,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/builds/{id}/events", wrapper.GetBuildEvents) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/capabilities", wrapper.GetCapabilities) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/devices", wrapper.ListDevices) }) @@ -12443,6 +12766,31 @@ func (response GetBuildEvents500JSONResponse) VisitGetBuildEventsResponse(w http return json.NewEncoder(w).Encode(response) } +type GetCapabilitiesRequestObject struct { +} + +type GetCapabilitiesResponseObject interface { + VisitGetCapabilitiesResponse(w http.ResponseWriter) error +} + +type GetCapabilities200JSONResponse Capabilities + +func (response GetCapabilities200JSONResponse) VisitGetCapabilitiesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetCapabilities500JSONResponse Error + +func (response GetCapabilities500JSONResponse) VisitGetCapabilitiesResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type ListDevicesRequestObject struct { Params ListDevicesParams } @@ -13150,7 +13498,8 @@ func (response DeleteInstance500JSONResponse) VisitDeleteInstanceResponse(w http } type GetInstanceRequestObject struct { - Id string `json:"id"` + Id string `json:"id"` + Params GetInstanceParams } type GetInstanceResponseObject interface { @@ -14525,6 +14874,9 @@ type StrictServerInterface interface { // Stream build events (SSE) // (GET /builds/{id}/events) GetBuildEvents(ctx context.Context, request GetBuildEventsRequestObject) (GetBuildEventsResponseObject, error) + // Get host capabilities + // (GET /capabilities) + GetCapabilities(ctx context.Context, request GetCapabilitiesRequestObject) (GetCapabilitiesResponseObject, error) // List registered devices // (GET /devices) ListDevices(ctx context.Context, request ListDevicesRequestObject) (ListDevicesResponseObject, error) @@ -14836,6 +15188,30 @@ func (sh *strictHandler) GetBuildEvents(w http.ResponseWriter, r *http.Request, } } +// GetCapabilities operation middleware +func (sh *strictHandler) GetCapabilities(w http.ResponseWriter, r *http.Request) { + var request GetCapabilitiesRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetCapabilities(ctx, request.(GetCapabilitiesRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetCapabilities") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetCapabilitiesResponseObject); ok { + if err := validResponse.VisitGetCapabilitiesResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ListDevices operation middleware func (sh *strictHandler) ListDevices(w http.ResponseWriter, r *http.Request, params ListDevicesParams) { var request ListDevicesRequestObject @@ -15295,10 +15671,11 @@ func (sh *strictHandler) DeleteInstance(w http.ResponseWriter, r *http.Request, } // GetInstance operation middleware -func (sh *strictHandler) GetInstance(w http.ResponseWriter, r *http.Request, id string) { +func (sh *strictHandler) GetInstance(w http.ResponseWriter, r *http.Request, id string, params GetInstanceParams) { var request GetInstanceRequestObject request.Id = id + request.Params = params handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.GetInstance(ctx, request.(GetInstanceRequestObject)) @@ -16182,310 +16559,337 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "swEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/redact/redact.go b/lib/redact/redact.go new file mode 100644 index 000000000..34a995af5 --- /dev/null +++ b/lib/redact/redact.go @@ -0,0 +1,31 @@ +// Package redact provides the shared sentinel and helpers for secret-safe +// diagnostics. Read APIs and log/diagnostic output use it to hide secret +// values (environment variables, credentials, tokens) while preserving keys +// so the output remains structurally useful. +package redact + +// Sentinel replaces a secret value in redacted output. Keys are preserved so +// clients can see which variables exist without learning their values. +// +// The sentinel is treated as "no value provided" when it round-trips back +// into a mutation (for example an instance env update), so a redacted read +// response can never overwrite a real secret. +const Sentinel = "[redacted]" + +// Values returns a copy of m with every value replaced by Sentinel. +// A nil input returns nil. +func Values(m map[string]string) map[string]string { + if m == nil { + return nil + } + out := make(map[string]string, len(m)) + for k := range m { + out[k] = Sentinel + } + return out +} + +// IsSentinel reports whether v is the redaction sentinel placeholder. +func IsSentinel(v string) bool { + return v == Sentinel +} diff --git a/lib/redact/redact_test.go b/lib/redact/redact_test.go new file mode 100644 index 000000000..fd8f55816 --- /dev/null +++ b/lib/redact/redact_test.go @@ -0,0 +1,37 @@ +package redact + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValuesRedactsCanary(t *testing.T) { + const canary = "sk-canary-secret-value-12345" + in := map[string]string{ + "API_KEY": canary, + "EMPTY_VALUE": "", + } + + out := Values(in) + + require.Equal(t, map[string]string{ + "API_KEY": Sentinel, + "EMPTY_VALUE": Sentinel, + }, out) + for _, v := range out { + require.NotContains(t, v, canary) + } + // Input map must not be mutated. + require.Equal(t, canary, in["API_KEY"]) +} + +func TestValuesNil(t *testing.T) { + require.Nil(t, Values(nil)) +} + +func TestIsSentinel(t *testing.T) { + require.True(t, IsSentinel(Sentinel)) + require.False(t, IsSentinel("")) + require.False(t, IsSentinel("real-value")) +} diff --git a/lib/scopes/scopes.go b/lib/scopes/scopes.go index ef0bb85dd..5588327ee 100644 --- a/lib/scopes/scopes.go +++ b/lib/scopes/scopes.go @@ -209,8 +209,9 @@ var RouteScopes = map[string]Scope{ "DELETE /devices/{id}": DeviceDelete, "GET /devices/{id}": DeviceRead, - // Health & Resources + // Health, Capabilities & Resources "GET /health": ResourceRead, + "GET /capabilities": ResourceRead, "GET /resources": ResourceRead, "POST /resources/memory/reclaim": ResourceWrite, diff --git a/openapi.yaml b/openapi.yaml index 14b0eed93..d69fa3593 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1027,7 +1027,9 @@ components: type: object additionalProperties: type: string - description: Environment variables + description: | + Environment variables. Values are redacted ("[redacted]") in list/get + responses unless the request sets `include_env=true`. tags: $ref: "#/components/schemas/Tags" network: @@ -1422,6 +1424,145 @@ components: type: string enum: [ok] example: ok + + Capabilities: + type: object + required: [server, host, runtime, network, images, features] + properties: + server: + $ref: "#/components/schemas/CapabilitiesServer" + host: + $ref: "#/components/schemas/CapabilitiesHost" + runtime: + $ref: "#/components/schemas/CapabilitiesRuntime" + network: + $ref: "#/components/schemas/CapabilitiesNetwork" + images: + $ref: "#/components/schemas/CapabilitiesImages" + features: + type: array + items: + type: string + description: | + Stable feature IDs clients can gate behavior on. Base IDs are always + present: "instances", "images", "builds", "volumes", "ingress", + "exec", "logs". Conditional IDs reflect the effective default + runtime and host platform: "standby", "snapshots", "fork", + "pause", "hotplug-memory", "balloon-control", "vsock", + "gpu-passthrough", "disk-io-limit", "disk-resize", "devices", + "rosetta-emulation". + + CapabilitiesServer: + type: object + required: [version, api_version] + properties: + version: + type: string + description: Server build version (short git revision, with "-dirty" suffix for uncommitted builds, or "unknown") + example: 76a2fdb3 + api_version: + type: string + description: API contract version (matches the OpenAPI document info version) + example: 0.2.0 + + CapabilitiesHost: + type: object + required: [os, arch] + properties: + os: + type: string + description: Host operating system + example: linux + arch: + type: string + description: Host CPU architecture + example: arm64 + + CapabilitiesRuntime: + type: object + required: [default, supported, snapshot, standby, pause, hotplug_memory, balloon_control, vsock, gpu_passthrough, disk_io_limit, disk_resize] + properties: + default: + type: string + description: | + Effective default runtime used for launches that do not name one. + Reported as a capability-bearing runtime identifier; clients should + gate behavior on the boolean features below rather than this value. + example: cloud-hypervisor + supported: + type: array + items: + type: string + description: Runtime identifiers that can run on this host platform + example: [cloud-hypervisor, firecracker, qemu] + snapshot: + type: boolean + description: Snapshot/restore is supported by the effective default runtime on this host + standby: + type: boolean + description: Standby (pause + memory snapshot) and restore are supported on this host + pause: + type: boolean + description: Pause/resume is supported by the effective default runtime + hotplug_memory: + type: boolean + description: Live memory hotplug resize is supported by the effective default runtime + balloon_control: + type: boolean + description: Runtime memory balloon control is supported by the effective default runtime + vsock: + type: boolean + description: Vsock guest communication is supported by the effective default runtime + gpu_passthrough: + type: boolean + description: GPU/PCI device passthrough is supported by the effective default runtime + disk_io_limit: + type: boolean + description: Disk I/O rate limiting is supported by the effective default runtime + disk_resize: + type: boolean + description: Live disk resize is supported by the effective default runtime + + CapabilitiesNetwork: + type: object + required: [model, gateway, guest_to_guest] + properties: + model: + type: string + enum: [bridge, nat] + description: | + Guest networking model. "bridge" is a Linux bridge with per-VM TAP + devices; "nat" is hypervisor-provided NAT (macOS). + gateway: + type: string + description: | + Guest-visible host gateway IP. Guests reach host services (including + host ingress) through this address. + example: 10.100.0.1 + subnet: + type: string + description: Guest subnet CIDR + example: 10.100.0.0/16 + guest_to_guest: + type: boolean + description: Whether direct VM-to-VM traffic is permitted on the default network + + CapabilitiesImages: + type: object + required: [platforms, default_platform] + properties: + platforms: + type: array + items: + type: string + description: | + Image platforms (os/arch) this host can run. Includes emulated + platforms (e.g. linux/amd64 via Rosetta on Apple Silicon macOS). + example: [linux/arm64, linux/amd64] + default_platform: + type: string + description: Image platform selected when a create request omits one + example: linux/arm64 IngressMatch: type: object @@ -2077,6 +2218,35 @@ paths: schema: $ref: "#/components/schemas/Health" + /capabilities: + get: + summary: Get host capabilities + description: | + Returns machine-readable host capabilities: server and API version, host + OS/architecture, effective default-runtime features, guest networking model + and host gateway, supported image platforms, and stable feature IDs. + + Runtime-derived values reflect the actual host (for example, snapshot and + standby support on macOS is gated on the host OS version), so clients can + gate behavior on capabilities without selecting or naming a concrete + hypervisor. + operationId: getCapabilities + security: + - bearerAuth: [] + responses: + 200: + description: Host capabilities + content: + application/json: + schema: + $ref: "#/components/schemas/Capabilities" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /resources: get: summary: Get host resource capacity and allocations @@ -2312,6 +2482,16 @@ paths: Filter instances by tag key-value pairs. Uses deepObject style: ?tags[team]=backend&tags[env]=staging Multiple entries are ANDed together. All specified key-value pairs must match. + - name: include_env + in: query + required: false + schema: + type: boolean + default: false + description: | + Include plaintext environment variable values in the response. + By default env values are redacted (keys are preserved, values are + replaced with "[redacted]"). responses: 200: description: List of instances @@ -2401,6 +2581,16 @@ paths: schema: type: string description: Instance ID or name + - name: include_env + in: query + required: false + schema: + type: boolean + default: false + description: | + Include plaintext environment variable values in the response. + By default env values are redacted (keys are preserved, values are + replaced with "[redacted]"). responses: 200: description: Instance details From 56ba0f062c9eeeb7485bacc95c0aae51b6d0e7de Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 13:31:36 -0400 Subject: [PATCH 2/9] Zero runtime features when the default runtime can't run on the host If the configured default runtime is not usable on the host platform (e.g. cloud-hypervisor configured on macOS), report zeroed capability booleans and base-only feature IDs instead of advertising a runtime that cannot launch there. Also make the world-readable config file test robust to a JWT_SECRET environment override in CI. --- cmd/api/api/capabilities.go | 17 ++++++++++++----- cmd/api/api/capabilities_test.go | 17 +++++++++++++---- cmd/api/config/config_test.go | 2 ++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/cmd/api/api/capabilities.go b/cmd/api/api/capabilities.go index 7be4d5bb0..e661d2ad2 100644 --- a/cmd/api/api/capabilities.go +++ b/cmd/api/api/capabilities.go @@ -3,6 +3,7 @@ package api import ( "context" "runtime" + "slices" "sync" "github.com/kernel/hypeman/lib/hypervisor" @@ -55,12 +56,18 @@ func (s *ApiService) GetCapabilities(ctx context.Context, _ oapi.GetCapabilities if s.InstanceManager != nil { defaultRuntime = s.InstanceManager.DefaultHypervisor() } + supported := supportedRuntimes(runtime.GOOS) caps, capsKnown := hypervisor.CapabilitiesForType(defaultRuntime) + if capsKnown && !slices.Contains(supported, string(defaultRuntime)) { + // The configured default runtime cannot run on this host platform; + // advertising its features would overstate support. + capsKnown = false + } if !capsKnown { - // The configured default runtime is not available on this platform; - // report zeroed features rather than guessing. - log.WarnContext(ctx, "default runtime has no registered capabilities on this host", - "runtime", string(defaultRuntime)) + // Report zeroed features rather than guessing. + log.WarnContext(ctx, "default runtime is not usable on this host platform", + "runtime", string(defaultRuntime), "host_os", runtime.GOOS) + caps = hypervisor.Capabilities{} } emulation := emulationSupported(runtime.GOOS, runtime.GOARCH, defaultRuntime) @@ -85,7 +92,7 @@ func (s *ApiService) GetCapabilities(ctx context.Context, _ oapi.GetCapabilities }, Runtime: oapi.CapabilitiesRuntime{ Default: string(defaultRuntime), - Supported: supportedRuntimes(runtime.GOOS), + Supported: supported, Snapshot: caps.SupportsSnapshot, Standby: standbySupported(caps), Pause: caps.SupportsPause, diff --git a/cmd/api/api/capabilities_test.go b/cmd/api/api/capabilities_test.go index a9709bc4c..2a2e9ba54 100644 --- a/cmd/api/api/capabilities_test.go +++ b/cmd/api/api/capabilities_test.go @@ -61,11 +61,17 @@ func TestGetCapabilities(t *testing.T) { require.Equal(t, runtime.GOOS, caps.Host.Os) require.Equal(t, runtime.GOARCH, caps.Host.Arch) - // Effective default runtime and its capabilities + // Effective default runtime and its capabilities. The test-service manager + // defaults to cloud-hypervisor; on macOS that default cannot run, so the + // endpoint must report zeroed runtime features rather than overstate. require.Equal(t, string(hypervisor.TypeCloudHypervisor), caps.Runtime.Default) require.NotEmpty(t, caps.Runtime.Supported) - require.Contains(t, caps.Runtime.Supported, caps.Runtime.Default) - if runtime.GOOS == "linux" { + if runtime.GOOS == "darwin" { + require.Equal(t, []string{string(hypervisor.TypeVZ)}, caps.Runtime.Supported) + require.False(t, caps.Runtime.Standby, "unusable default runtime must not advertise standby") + require.False(t, caps.Runtime.Snapshot) + } else { + require.Contains(t, caps.Runtime.Supported, caps.Runtime.Default) chCaps := cloudHypervisorCaps(t) require.Equal(t, chCaps.SupportsSnapshot, caps.Runtime.Snapshot) require.Equal(t, chCaps.SupportsPause, caps.Runtime.Pause) @@ -86,7 +92,10 @@ func TestGetCapabilities(t *testing.T) { for _, f := range []string{"instances", "images", "builds", "volumes", "ingress", "exec", "logs"} { require.Contains(t, caps.Features, f) } - if runtime.GOOS == "linux" { + if runtime.GOOS == "darwin" { + require.NotContains(t, caps.Features, "devices") + require.NotContains(t, caps.Features, "standby") + } else { require.Contains(t, caps.Features, "standby") require.Contains(t, caps.Features, "devices") } diff --git a/cmd/api/config/config_test.go b/cmd/api/config/config_test.go index 0a02535c7..a0fd14838 100644 --- a/cmd/api/config/config_test.go +++ b/cmd/api/config/config_test.go @@ -573,6 +573,8 @@ func TestConfigFilePermTooOpen(t *testing.T) { func TestLoadSucceedsWithWorldReadableConfigFile(t *testing.T) { clearPathEnvOverrides(t) + // Env overrides beat the config file; clear JWT_SECRET so the file value wins. + t.Setenv("JWT_SECRET", "") tmp := t.TempDir() cfgPath := filepath.Join(tmp, "config.yaml") From 8223b51bf4c6af56fb1aad9732dafa990cfb8ae6 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 13:42:18 -0400 Subject: [PATCH 3/9] Enforce owner-only build configs on rewrite and at startup os.WriteFile does not change permissions of an existing file, so token refresh rewrites of build config.json (which carries the registry push token) would leave legacy 0644 files world-readable. Chmod after every write and tighten legacy build config/metadata files at manager startup. Addresses Cursor Bugbot feedback on PR #338. --- lib/builds/manager.go | 35 +++++++++++++++ lib/builds/storage.go | 5 +++ lib/builds/storage_permissions_test.go | 59 ++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 lib/builds/storage_permissions_test.go diff --git a/lib/builds/manager.go b/lib/builds/manager.go index 87f21dfec..63ae7b694 100644 --- a/lib/builds/manager.go +++ b/lib/builds/manager.go @@ -168,9 +168,44 @@ func NewManager( m.metrics = metrics } + // Restrict permissions on build files written by older versions (may be + // 0644 and carry registry tokens / build args). + m.tightenBuildFilePermissions() + return m, nil } +// tightenBuildFilePermissions restricts existing build config and metadata +// files to owner-only access. Files written before restrictive permissions +// were introduced may be mode 0644; build configs carry the registry push +// token and metadata embeds the original request. Best-effort: individual +// failures are logged, not fatal. +func (m *manager) tightenBuildFilePermissions() { + entries, err := os.ReadDir(m.paths.BuildsDir()) + if err != nil { + return // no builds directory yet + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + for _, path := range []string{ + m.paths.BuildConfig(entry.Name()), + m.paths.BuildMetadata(entry.Name()), + } { + info, err := os.Stat(path) + if err != nil { + continue + } + if info.Mode().Perm() != 0600 { + if err := os.Chmod(path, 0600); err != nil { + m.logger.Warn("failed to tighten build file permissions", "path", path, "error", err) + } + } + } + } +} + // Start starts the build manager's background services func (m *manager) Start(ctx context.Context) error { go func() { diff --git a/lib/builds/storage.go b/lib/builds/storage.go index 93e55f6a5..0b49fa4fd 100644 --- a/lib/builds/storage.go +++ b/lib/builds/storage.go @@ -237,6 +237,11 @@ func writeBuildConfig(p *paths.Paths, id string, config *BuildConfig) error { if err := os.WriteFile(configPath, data, 0600); err != nil { return fmt.Errorf("write build config: %w", err) } + // WriteFile does not change permissions of an existing file; token + // refresh rewrites the config in place, so enforce owner-only access. + if err := os.Chmod(configPath, 0600); err != nil { + return fmt.Errorf("chmod build config: %w", err) + } return nil } diff --git a/lib/builds/storage_permissions_test.go b/lib/builds/storage_permissions_test.go new file mode 100644 index 000000000..edfb871b7 --- /dev/null +++ b/lib/builds/storage_permissions_test.go @@ -0,0 +1,59 @@ +package builds + +import ( + "os" + "testing" + + "github.com/kernel/hypeman/lib/paths" + "github.com/stretchr/testify/require" +) + +// TestWriteBuildConfigRestrictivePermissions proves build configs (which +// carry the registry push token) are owner-only, including when an existing +// world-readable file is rewritten in place (token refresh path). +func TestWriteBuildConfigRestrictivePermissions(t *testing.T) { + t.Parallel() + p := paths.New(t.TempDir()) + + cfg := &BuildConfig{ + JobID: "job-1", + RegistryURL: "localhost:8080", + RegistryToken: "token-canary", + SourcePath: "/src", + NetworkMode: "isolated", + } + + require.NoError(t, writeBuildConfig(p, "build-perms", cfg)) + info, err := os.Stat(p.BuildConfig("build-perms")) + require.NoError(t, err) + require.Equal(t, os.FileMode(0600), info.Mode().Perm()) + + // Simulate a legacy world-readable config, then rewrite it (token + // refresh). WriteFile alone would keep 0644; the chmod must tighten it. + require.NoError(t, os.Chmod(p.BuildConfig("build-perms"), 0644)) + require.NoError(t, writeBuildConfig(p, "build-perms", cfg)) + info, err = os.Stat(p.BuildConfig("build-perms")) + require.NoError(t, err) + require.Equal(t, os.FileMode(0600), info.Mode().Perm(), + "rewritten build config must be tightened to 0600") +} + +// TestManagerTightensLegacyBuildFilePermissions proves the startup sweep +// upgrades build config/metadata files written by older versions to 0600. +func TestManagerTightensLegacyBuildFilePermissions(t *testing.T) { + t.Parallel() + p := paths.New(t.TempDir()) + + require.NoError(t, os.MkdirAll(p.BuildDir("build-legacy"), 0755)) + require.NoError(t, os.WriteFile(p.BuildConfig("build-legacy"), []byte(`{"job_id":"j"}`), 0644)) + require.NoError(t, os.WriteFile(p.BuildMetadata("build-legacy"), []byte(`{"id":"build-legacy"}`), 0644)) + + m := &manager{paths: p} + m.tightenBuildFilePermissions() + + for _, path := range []string{p.BuildConfig("build-legacy"), p.BuildMetadata("build-legacy")} { + info, err := os.Stat(path) + require.NoError(t, err) + require.Equal(t, os.FileMode(0600), info.Mode().Perm(), "%s must be tightened to 0600", path) + } +} From 62db5d30121b585960d3fad580e116ff3a7df240 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 13:56:26 -0400 Subject: [PATCH 4/9] Treat all-sentinel env patches as no env update Strip redaction sentinels at the top of updateInstance so a read-modify-write that round-trips only redacted env values does not count as an env mutation: it no longer requires the instance to be running and no longer routes through the egress-proxy credential path. Addresses Cursor Bugbot feedback on PR #338. --- lib/instances/metadata_permissions_test.go | 21 +++++++++++++++++ lib/instances/update.go | 26 ++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/lib/instances/metadata_permissions_test.go b/lib/instances/metadata_permissions_test.go index ccd726ce2..d0e4c326a 100644 --- a/lib/instances/metadata_permissions_test.go +++ b/lib/instances/metadata_permissions_test.go @@ -110,3 +110,24 @@ func TestMergeEnvUpdateSkipsRedactionSentinel(t *testing.T) { next = mergeEnvUpdate(nil, map[string]string{"A": "1"}) require.Equal(t, map[string]string{"A": "1"}, next) } + +// TestWithoutRedactionSentinels proves an all-sentinel env patch collapses to +// no env update (so it skips the running-state and egress-proxy paths), while +// mixed patches keep only real values. +func TestWithoutRedactionSentinels(t *testing.T) { + t.Parallel() + + require.Nil(t, withoutRedactionSentinels(map[string]string{ + "API_KEY": redact.Sentinel, + "OTHER": redact.Sentinel, + }), "all-sentinel patch must not count as an env update") + + require.Equal(t, map[string]string{"PLAIN": "new"}, + withoutRedactionSentinels(map[string]string{ + "API_KEY": redact.Sentinel, + "PLAIN": "new", + })) + + require.Nil(t, withoutRedactionSentinels(nil)) + require.Empty(t, withoutRedactionSentinels(map[string]string{})) +} diff --git a/lib/instances/update.go b/lib/instances/update.go index 8ac534d81..4218f9d54 100644 --- a/lib/instances/update.go +++ b/lib/instances/update.go @@ -21,6 +21,11 @@ type updateInstanceRulesService interface { func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInstanceRequest) (*Instance, error) { log := logger.FromContext(ctx) + // Drop redaction sentinels up front: a read-modify-write that round-trips + // only redacted env values is not an env mutation, so it must not trigger + // the running-state requirement or the egress-proxy path. + req.Env = withoutRedactionSentinels(req.Env) + // 1. Load and validate current state meta, err := m.loadMetadata(id) if err != nil { @@ -108,6 +113,27 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta return updated, nil } +// withoutRedactionSentinels returns the env update with redaction-sentinel +// values removed. A sentinel round-tripped from a redacted read response +// means "no value provided", never a literal secret. Returns nil when every +// entry is a sentinel so all-sentinel patches behave as no env update at all. +func withoutRedactionSentinels(env map[string]string) map[string]string { + if len(env) == 0 { + return env + } + out := make(map[string]string, len(env)) + for k, v := range env { + if redact.IsSentinel(v) { + continue + } + out[k] = v + } + if len(out) == 0 { + return nil + } + return out +} + // mergeEnvUpdate applies requested env updates onto the previous env. A // redaction sentinel round-tripped from a read response means "no value // provided", never a literal secret, so sentinel values are skipped. This From ba280a68d8dbd4a59ac204db258a96c0bd297fd9 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 14:14:46 -0400 Subject: [PATCH 5/9] Sweep legacy guest config disks to owner-only at startup Legacy config.ext4 files embed env values and could remain 0644 after upgrade until the instance is recreated. Extend the startup permission sweep to cover them alongside metadata.json. Addresses Cursor Bugbot feedback on PR #338. --- lib/instances/metadata_permissions_test.go | 16 +++++++----- lib/instances/storage.go | 29 +++++++++++++--------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/instances/metadata_permissions_test.go b/lib/instances/metadata_permissions_test.go index d0e4c326a..1bb53f421 100644 --- a/lib/instances/metadata_permissions_test.go +++ b/lib/instances/metadata_permissions_test.go @@ -65,25 +65,29 @@ func TestSaveMetadataRestrictivePermissions(t *testing.T) { } // TestManagerTightensLegacyMetadataPermissions proves the startup sweep -// upgrades metadata files written by older versions (mode 0644) to 0600. +// upgrades metadata and config disk files written by older versions (mode +// 0644) to 0600. func TestManagerTightensLegacyMetadataPermissions(t *testing.T) { t.Parallel() dataDir := t.TempDir() p := paths.New(dataDir) - // Simulate legacy metadata written with world-readable permissions. + // Simulate legacy files written with world-readable permissions. meta := metadataFixture("inst-perms-legacy") require.NoError(t, os.MkdirAll(p.InstanceDir(meta.Id), 0755)) data, err := json.MarshalIndent(meta, "", " ") require.NoError(t, err) require.NoError(t, os.WriteFile(p.InstanceMetadata(meta.Id), data, 0644)) + require.NoError(t, os.WriteFile(p.InstanceConfigDisk(meta.Id), []byte("ext4-bytes"), 0644)) newPermTestManager(t, dataDir) - info, err := os.Stat(p.InstanceMetadata(meta.Id)) - require.NoError(t, err) - require.Equal(t, os.FileMode(0600), info.Mode().Perm(), - "legacy 0644 metadata must be tightened to 0600 at startup") + for _, path := range []string{p.InstanceMetadata(meta.Id), p.InstanceConfigDisk(meta.Id)} { + info, err := os.Stat(path) + require.NoError(t, err) + require.Equal(t, os.FileMode(0600), info.Mode().Perm(), + "legacy 0644 file must be tightened to 0600 at startup: %s", path) + } } // TestMergeEnvUpdateSkipsRedactionSentinel proves a redacted read response diff --git a/lib/instances/storage.go b/lib/instances/storage.go index f5925f7a0..58c0d4b6f 100644 --- a/lib/instances/storage.go +++ b/lib/instances/storage.go @@ -127,10 +127,11 @@ func (m *manager) saveMetadata(meta *metadata) error { return nil } -// tightenMetadataPermissions restricts existing instance metadata files to -// owner-only access. Metadata written before restrictive permissions were -// introduced may be mode 0644; it contains env values and credential -// bindings. Best-effort: individual failures are logged, not fatal. +// tightenMetadataPermissions restricts existing instance metadata and guest +// config disk files to owner-only access. Files written before restrictive +// permissions were introduced may be mode 0644; both embed env values (and +// metadata also carries credential bindings). Best-effort: individual +// failures are logged, not fatal. func (m *manager) tightenMetadataPermissions() { log := logger.FromContext(context.Background()) entries, err := os.ReadDir(m.paths.GuestsDir()) @@ -141,14 +142,18 @@ func (m *manager) tightenMetadataPermissions() { if !entry.IsDir() { continue } - metaPath := m.paths.InstanceMetadata(entry.Name()) - info, err := os.Stat(metaPath) - if err != nil { - continue - } - if info.Mode().Perm() != 0600 { - if err := os.Chmod(metaPath, 0600); err != nil { - log.Warn("failed to tighten instance metadata permissions", "path", metaPath, "error", err) + for _, path := range []string{ + m.paths.InstanceMetadata(entry.Name()), + m.paths.InstanceConfigDisk(entry.Name()), + } { + info, err := os.Stat(path) + if err != nil { + continue + } + if info.Mode().Perm() != 0600 { + if err := os.Chmod(path, 0600); err != nil { + log.Warn("failed to tighten instance file permissions", "path", path, "error", err) + } } } } From 0f6f74e41430cd26a4fed9f3604df65521b0a131 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 14:27:34 -0400 Subject: [PATCH 6/9] Report the vz NAT gateway for macOS capabilities On macOS the configured subnet/gateway are ignored (guests use vz NAT), so the Initialize-cached network is never guest-visible. DefaultNetwork now prefers live host state per platform: the config-mirroring cache on Linux, the vz NAT stub (192.168.64.1, 192.168.64.0/24) on macOS. This is reporting-only; allocation behavior is unchanged. Addresses Cursor Bugbot feedback on PR #338. --- lib/network/default_network_test.go | 30 ++++++++++++++++++----------- lib/network/manager.go | 14 +++++++++----- lib/network/model_darwin.go | 8 ++++++++ lib/network/model_linux.go | 7 +++++++ 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/lib/network/default_network_test.go b/lib/network/default_network_test.go index 3ce147d38..ba7b04e07 100644 --- a/lib/network/default_network_test.go +++ b/lib/network/default_network_test.go @@ -9,10 +9,11 @@ import ( "github.com/stretchr/testify/require" ) -// TestDefaultNetworkPrefersInitializedNetwork proves DefaultNetwork returns -// the effective default network established at Initialize time, including the -// guest-visible gateway, without touching host kernel state. -func TestDefaultNetworkPrefersInitializedNetwork(t *testing.T) { +// TestDefaultNetworkEffectiveGateway proves DefaultNetwork reports the +// guest-visible gateway for the host's networking model: the initialized +// (config-derived) network on Linux, and the vz NAT stub on macOS, where the +// configured subnet/gateway are ignored. +func TestDefaultNetworkEffectiveGateway(t *testing.T) { t.Parallel() cfg := &config.Config{} m := NewManager(paths.New(t.TempDir()), cfg, nil).(*manager) @@ -28,14 +29,21 @@ func TestDefaultNetworkPrefersInitializedNetwork(t *testing.T) { nw, err := m.DefaultNetwork(context.Background()) require.NoError(t, err) - require.Equal(t, "10.100.0.1", nw.Gateway) - require.Equal(t, "10.100.0.0/16", nw.Subnet) + if preferCachedDefaultNetwork() { + require.Equal(t, "10.100.0.1", nw.Gateway) + require.Equal(t, "10.100.0.0/16", nw.Subnet) - // Mutating the returned copy must not affect the cached network. - nw.Gateway = " mutated " - again, err := m.DefaultNetwork(context.Background()) - require.NoError(t, err) - require.Equal(t, "10.100.0.1", again.Gateway) + // Mutating the returned copy must not affect the cached network. + nw.Gateway = " mutated " + again, err := m.DefaultNetwork(context.Background()) + require.NoError(t, err) + require.Equal(t, "10.100.0.1", again.Gateway) + } else { + // macOS: the vz NAT stub is authoritative; the config-derived cache + // is never guest-visible. + require.Equal(t, "192.168.64.1", nw.Gateway) + require.Equal(t, "192.168.64.0/24", nw.Subnet) + } } func TestGuestToGuestEnabled(t *testing.T) { diff --git a/lib/network/manager.go b/lib/network/manager.go index 1c6f75e78..7cca0f6b2 100644 --- a/lib/network/manager.go +++ b/lib/network/manager.go @@ -167,12 +167,16 @@ func (m *manager) setDefaultNetwork(network *Network) { m.defaultNetwork = cloneNetwork(network) } -// DefaultNetwork returns the effective default network. It prefers the -// network established during Initialize and falls back to querying live -// host state (kernel bridge on Linux, the vz NAT stub on macOS). +// DefaultNetwork returns the effective default network. On Linux it prefers +// the network established during Initialize (bridge state mirrors config). +// On macOS the configured subnet/gateway are ignored — guests use vz NAT — +// so the live NAT stub from queryNetworkState is authoritative and the +// config-derived cache is never guest-visible. func (m *manager) DefaultNetwork(ctx context.Context) (*Network, error) { - if network := m.cachedDefaultNetwork(); network != nil { - return network, nil + if preferCachedDefaultNetwork() { + if network := m.cachedDefaultNetwork(); network != nil { + return network, nil + } } return m.getDefaultNetwork(ctx) } diff --git a/lib/network/model_darwin.go b/lib/network/model_darwin.go index 129ffa356..db222e7c0 100644 --- a/lib/network/model_darwin.go +++ b/lib/network/model_darwin.go @@ -14,3 +14,11 @@ func NetworkModel() string { func GuestToGuestEnabled(_ *Network) bool { return false } + +// preferCachedDefaultNetwork reports whether DefaultNetwork should trust the +// network cached during Initialize. On macOS the configured subnet/gateway +// are ignored (vz NAT assigns 192.168.64.0/24 with host gateway 192.168.64.1), +// so the cache is not guest-visible and live NAT state must win. +func preferCachedDefaultNetwork() bool { + return false +} diff --git a/lib/network/model_linux.go b/lib/network/model_linux.go index 7a74a0f40..8afa248af 100644 --- a/lib/network/model_linux.go +++ b/lib/network/model_linux.go @@ -14,3 +14,10 @@ func NetworkModel() string { func GuestToGuestEnabled(n *Network) bool { return n != nil && !n.Isolated } + +// preferCachedDefaultNetwork reports whether DefaultNetwork should trust the +// network cached during Initialize. On Linux the cache mirrors the bridge +// built from config, so it is authoritative. +func preferCachedDefaultNetwork() bool { + return true +} From 63ca479079dc6a4fb2c10df5ab7105a8cb53d04f Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 14:36:49 -0400 Subject: [PATCH 7/9] Chmod build metadata temp file before rename A leftover 0644 temp file from an older write would otherwise keep its permissions through WriteFile and be renamed into place world-readable. Addresses Cursor Bugbot feedback on PR #338. --- lib/builds/storage.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/builds/storage.go b/lib/builds/storage.go index 0b49fa4fd..dbe6caf69 100644 --- a/lib/builds/storage.go +++ b/lib/builds/storage.go @@ -64,6 +64,12 @@ func writeMetadata(p *paths.Paths, meta *buildMetadata) error { if err := os.WriteFile(tempPath, data, 0600); err != nil { return fmt.Errorf("write temp metadata: %w", err) } + // WriteFile does not change permissions of an existing file; a leftover + // temp from an older 0644 write must not survive the rename readable. + if err := os.Chmod(tempPath, 0600); err != nil { + os.Remove(tempPath) + return fmt.Errorf("chmod temp metadata: %w", err) + } finalPath := p.BuildMetadata(meta.ID) if err := os.Rename(tempPath, finalPath); err != nil { From 76320894ee8c2e77b8929a1867c01b17d4892c46 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 18:23:06 -0400 Subject: [PATCH 8/9] Narrow PR to host-capability contract only Remove the secret-storage and diagnostics hardening from this change so the PR contains only the authenticated GET /capabilities endpoint and what it needs to be truthful: - Drop include_env query params and default instance-response env redaction - Drop lib/redact and redaction-sentinel PATCH semantics - Drop metadata/config-disk/build permission tightening and startup sweeps - Drop server-config permission warnings and access-log canary tests GET /instances and GET /instances/{id} response behavior and generated SDK signatures are byte-identical to main. The hardening work is split out into a follow-up ticket. --- cmd/api/api/instances.go | 26 +- cmd/api/api/instances_redact_test.go | 128 ---- cmd/api/config/config.go | 26 - cmd/api/config/config_test.go | 52 -- lib/builds/manager.go | 35 -- lib/builds/storage.go | 19 +- lib/builds/storage_permissions_test.go | 59 -- lib/instances/configdisk.go | 7 +- lib/instances/manager.go | 4 - lib/instances/metadata_permissions_test.go | 137 ----- lib/instances/storage.go | 38 +- lib/instances/update.go | 53 +- lib/middleware/access_log_test.go | 35 -- lib/oapi/oapi.go | 660 +++++++++------------ lib/redact/redact.go | 31 - lib/redact/redact_test.go | 37 -- openapi.yaml | 24 +- 17 files changed, 306 insertions(+), 1065 deletions(-) delete mode 100644 cmd/api/api/instances_redact_test.go delete mode 100644 lib/builds/storage_permissions_test.go delete mode 100644 lib/instances/metadata_permissions_test.go delete mode 100644 lib/middleware/access_log_test.go delete mode 100644 lib/redact/redact.go delete mode 100644 lib/redact/redact_test.go diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index 8b213cfcb..d145fa9b4 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -19,7 +19,6 @@ import ( mw "github.com/kernel/hypeman/lib/middleware" "github.com/kernel/hypeman/lib/network" "github.com/kernel/hypeman/lib/oapi" - "github.com/kernel/hypeman/lib/redact" "github.com/kernel/hypeman/lib/resources" "github.com/kernel/hypeman/lib/snapshot" "github.com/kernel/hypeman/lib/vm_metrics" @@ -55,9 +54,6 @@ func (s *ApiService) ListInstances(ctx context.Context, request oapi.ListInstanc oapiInsts := make([]oapi.Instance, len(domainInsts)) for i, inst := range domainInsts { oapiInsts[i] = instanceToOAPI(inst) - if !includeEnv(request.Params.IncludeEnv) { - redactInstanceEnv(&oapiInsts[i]) - } } return oapi.ListInstances200JSONResponse(oapiInsts), nil @@ -415,11 +411,7 @@ func (s *ApiService) GetInstance(ctx context.Context, request oapi.GetInstanceRe Message: "resource not resolved", }, nil } - oapiInst := instanceToOAPI(*inst) - if !includeEnv(request.Params.IncludeEnv) { - redactInstanceEnv(&oapiInst) - } - return oapi.GetInstance200JSONResponse(oapiInst), nil + return oapi.GetInstance200JSONResponse(instanceToOAPI(*inst)), nil } // GetInstanceStats returns resource utilization statistics for an instance @@ -1328,19 +1320,3 @@ func parseOptionalDuration(value string, field string) (*time.Duration, error) { } return &duration, nil } - -// includeEnv reports whether the caller opted in to plaintext env values. -func includeEnv(param *bool) bool { - return param != nil && *param -} - -// redactInstanceEnv replaces env values with the redaction sentinel, -// preserving keys. Env values are secrets; read APIs redact them unless the -// caller explicitly passes include_env=true. -func redactInstanceEnv(inst *oapi.Instance) { - if inst.Env == nil { - return - } - redacted := redact.Values(*inst.Env) - inst.Env = &redacted -} diff --git a/cmd/api/api/instances_redact_test.go b/cmd/api/api/instances_redact_test.go deleted file mode 100644 index 2bb06784b..000000000 --- a/cmd/api/api/instances_redact_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package api - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - "time" - - "github.com/kernel/hypeman/lib/hypervisor" - "github.com/kernel/hypeman/lib/instances" - mw "github.com/kernel/hypeman/lib/middleware" - "github.com/kernel/hypeman/lib/oapi" - "github.com/kernel/hypeman/lib/paths" - "github.com/kernel/hypeman/lib/redact" - "github.com/stretchr/testify/require" -) - -const envCanaryValue = "sk-canary-env-value-do-not-leak-7f3a9b" - -// writeInstanceMetadataFixture writes an instance metadata.json directly into -// the data dir so read APIs can be tested without booting a VM. -func writeInstanceMetadataFixture(t *testing.T, dataDir string, meta instances.StoredMetadata) { - t.Helper() - p := paths.New(dataDir) - require.NoError(t, os.MkdirAll(p.InstanceDir(meta.Id), 0755)) - data, err := json.MarshalIndent(meta, "", " ") - require.NoError(t, err) - require.NoError(t, os.WriteFile(p.InstanceMetadata(meta.Id), data, 0600)) -} - -func envCanaryInstance() instances.Instance { - return instances.Instance{ - StoredMetadata: instances.StoredMetadata{ - Id: "inst-env-canary", - Name: "env-canary", - Image: "docker.io/library/alpine:latest", - Env: map[string]string{"API_KEY": envCanaryValue, "PLAIN": "hello"}, - CreatedAt: time.Now(), - HypervisorType: hypervisor.TypeCloudHypervisor, - }, - State: instances.StateStopped, - } -} - -// TestListInstancesRedactsEnvByDefault proves the raw API no longer exposes -// env values on reads: keys are preserved, values are redacted, and the -// canary never appears unless include_env=true is passed. -func TestListInstancesRedactsEnvByDefault(t *testing.T) { - t.Parallel() - svc := newTestService(t) - inst := envCanaryInstance() - writeInstanceMetadataFixture(t, svc.Config.DataDir, inst.StoredMetadata) - - // Default: redacted. - resp, err := svc.ListInstances(ctx(), oapi.ListInstancesRequestObject{}) - require.NoError(t, err) - list, ok := resp.(oapi.ListInstances200JSONResponse) - require.True(t, ok, "expected 200 response, got %T", resp) - require.Len(t, list, 1) - require.NotNil(t, list[0].Env) - require.Equal(t, redact.Sentinel, (*list[0].Env)["API_KEY"]) - require.Equal(t, redact.Sentinel, (*list[0].Env)["PLAIN"]) - raw, err := json.Marshal(list) - require.NoError(t, err) - require.NotContains(t, string(raw), envCanaryValue, "redacted list response leaked env canary") - - // Opt-in: plaintext. - include := true - resp, err = svc.ListInstances(ctx(), oapi.ListInstancesRequestObject{ - Params: oapi.ListInstancesParams{IncludeEnv: &include}, - }) - require.NoError(t, err) - list, ok = resp.(oapi.ListInstances200JSONResponse) - require.True(t, ok, "expected 200 response, got %T", resp) - require.Len(t, list, 1) - require.Equal(t, envCanaryValue, (*list[0].Env)["API_KEY"]) -} - -// TestGetInstanceRedactsEnvByDefault covers the single-instance read path. -func TestGetInstanceRedactsEnvByDefault(t *testing.T) { - t.Parallel() - svc := newTestService(t) - inst := envCanaryInstance() - - resolvedCtx := mw.WithResolvedInstance(ctx(), inst.Id, inst) - - resp, err := svc.GetInstance(resolvedCtx, oapi.GetInstanceRequestObject{Id: inst.Id}) - require.NoError(t, err) - got, ok := resp.(oapi.GetInstance200JSONResponse) - require.True(t, ok, "expected 200 response, got %T", resp) - require.NotNil(t, got.Env) - require.Equal(t, redact.Sentinel, (*got.Env)["API_KEY"]) - raw, err := json.Marshal(got) - require.NoError(t, err) - require.NotContains(t, string(raw), envCanaryValue, "redacted get response leaked env canary") - - include := true - resp, err = svc.GetInstance(resolvedCtx, oapi.GetInstanceRequestObject{ - Id: inst.Id, - Params: oapi.GetInstanceParams{IncludeEnv: &include}, - }) - require.NoError(t, err) - got, ok = resp.(oapi.GetInstance200JSONResponse) - require.True(t, ok, "expected 200 response, got %T", resp) - require.Equal(t, envCanaryValue, (*got.Env)["API_KEY"]) -} - -func TestIncludeEnvParam(t *testing.T) { - t.Parallel() - require.False(t, includeEnv(nil)) - f := false - require.False(t, includeEnv(&f)) - tr := true - require.True(t, includeEnv(&tr)) -} - -// TestMetadataFixturePerms keeps the fixture helper honest: fixtures on disk -// must carry the sentinel value so the redaction assertions above are -// meaningful. -func TestMetadataFixturePerms(t *testing.T) { - t.Parallel() - dir := t.TempDir() - writeInstanceMetadataFixture(t, dir, envCanaryInstance().StoredMetadata) - data, err := os.ReadFile(filepath.Join(dir, "guests", "inst-env-canary", "metadata.json")) - require.NoError(t, err) - require.Contains(t, string(data), envCanaryValue) -} diff --git a/cmd/api/config/config.go b/cmd/api/config/config.go index c63085503..366fb4962 100644 --- a/cmd/api/config/config.go +++ b/cmd/api/config/config.go @@ -2,7 +2,6 @@ package config import ( "fmt" - "log/slog" "os" "path/filepath" "runtime" @@ -517,8 +516,6 @@ func Load(configPath string) (*Config, error) { return nil, fmt.Errorf("failed to load config from %s: %w", configPath, err) } // Auto-discovered path failed — continue with defaults + env - } else { - warnIfConfigFileWorldReadable(configPath) } } @@ -544,29 +541,6 @@ func Load(configPath string) (*Config, error) { return &cfg, nil } -// configFilePermTooOpen reports whether a config file is readable or -// writable by group or others. Config files typically carry jwt_secret and -// other credentials, so they should be owner-only (0600). -func configFilePermTooOpen(path string) bool { - info, err := os.Stat(path) - if err != nil { - return false - } - return info.Mode().Perm()&0o077 != 0 -} - -// warnIfConfigFileWorldReadable logs a warning when the config file (which -// typically carries jwt_secret and other credentials) is accessible by group -// or others. Hypeman never chmods operator-owned files; it only warns. -func warnIfConfigFileWorldReadable(path string) { - if !configFilePermTooOpen(path) { - return - } - info, _ := os.Stat(path) - slog.Warn("config file is accessible by group or others; it may contain secrets — consider chmod 0600", - "path", path, "mode", info.Mode().Perm().String()) -} - func (c *Config) expandPathFields() { c.DataDir = expandHomePath(c.DataDir) c.Build.SecretsDir = expandHomePath(c.Build.SecretsDir) diff --git a/cmd/api/config/config_test.go b/cmd/api/config/config_test.go index a0fd14838..93c236c3e 100644 --- a/cmd/api/config/config_test.go +++ b/cmd/api/config/config_test.go @@ -539,55 +539,3 @@ func TestValidateAllowsDisabledSnapshotCompressionDefaultWithoutValidAlgorithm(t t.Fatalf("expected disabled snapshot compression default to ignore algorithm/level, got %v", err) } } - -func TestConfigFilePermTooOpen(t *testing.T) { - tmp := t.TempDir() - cfgPath := filepath.Join(tmp, "config.yaml") - - write := func(mode os.FileMode) { - if err := os.WriteFile(cfgPath, []byte("jwt_secret: test-secret\n"), mode); err != nil { - t.Fatalf("write temp config: %v", err) - } - // WriteFile does not change perms of an existing file; chmod explicitly. - if err := os.Chmod(cfgPath, mode); err != nil { - t.Fatalf("chmod temp config: %v", err) - } - } - - write(0600) - if configFilePermTooOpen(cfgPath) { - t.Fatalf("0600 config file must not be flagged") - } - write(0640) - if !configFilePermTooOpen(cfgPath) { - t.Fatalf("0640 config file must be flagged") - } - write(0644) - if !configFilePermTooOpen(cfgPath) { - t.Fatalf("0644 config file must be flagged") - } - if configFilePermTooOpen(filepath.Join(tmp, "missing.yaml")) { - t.Fatalf("missing file must not be flagged") - } -} - -func TestLoadSucceedsWithWorldReadableConfigFile(t *testing.T) { - clearPathEnvOverrides(t) - // Env overrides beat the config file; clear JWT_SECRET so the file value wins. - t.Setenv("JWT_SECRET", "") - - tmp := t.TempDir() - cfgPath := filepath.Join(tmp, "config.yaml") - if err := os.WriteFile(cfgPath, []byte("jwt_secret: test-secret\n"), 0644); err != nil { - t.Fatalf("write temp config: %v", err) - } - - // A too-open config file only warns; it must not break loading. - cfg, err := Load(cfgPath) - if err != nil { - t.Fatalf("load config: %v", err) - } - if cfg.JwtSecret != "test-secret" { - t.Fatalf("expected jwt_secret from config file, got %q", cfg.JwtSecret) - } -} diff --git a/lib/builds/manager.go b/lib/builds/manager.go index 63ae7b694..87f21dfec 100644 --- a/lib/builds/manager.go +++ b/lib/builds/manager.go @@ -168,44 +168,9 @@ func NewManager( m.metrics = metrics } - // Restrict permissions on build files written by older versions (may be - // 0644 and carry registry tokens / build args). - m.tightenBuildFilePermissions() - return m, nil } -// tightenBuildFilePermissions restricts existing build config and metadata -// files to owner-only access. Files written before restrictive permissions -// were introduced may be mode 0644; build configs carry the registry push -// token and metadata embeds the original request. Best-effort: individual -// failures are logged, not fatal. -func (m *manager) tightenBuildFilePermissions() { - entries, err := os.ReadDir(m.paths.BuildsDir()) - if err != nil { - return // no builds directory yet - } - for _, entry := range entries { - if !entry.IsDir() { - continue - } - for _, path := range []string{ - m.paths.BuildConfig(entry.Name()), - m.paths.BuildMetadata(entry.Name()), - } { - info, err := os.Stat(path) - if err != nil { - continue - } - if info.Mode().Perm() != 0600 { - if err := os.Chmod(path, 0600); err != nil { - m.logger.Warn("failed to tighten build file permissions", "path", path, "error", err) - } - } - } - } -} - // Start starts the build manager's background services func (m *manager) Start(ctx context.Context) error { go func() { diff --git a/lib/builds/storage.go b/lib/builds/storage.go index dbe6caf69..ef03b4838 100644 --- a/lib/builds/storage.go +++ b/lib/builds/storage.go @@ -58,18 +58,11 @@ func writeMetadata(p *paths.Paths, meta *buildMetadata) error { return fmt.Errorf("marshal metadata: %w", err) } - // Write atomically via temp file. Build metadata embeds the original - // request (build args, secret references), so keep it owner-only. + // Write atomically via temp file tempPath := p.BuildMetadata(meta.ID) + ".tmp" - if err := os.WriteFile(tempPath, data, 0600); err != nil { + if err := os.WriteFile(tempPath, data, 0644); err != nil { return fmt.Errorf("write temp metadata: %w", err) } - // WriteFile does not change permissions of an existing file; a leftover - // temp from an older 0644 write must not survive the rename readable. - if err := os.Chmod(tempPath, 0600); err != nil { - os.Remove(tempPath) - return fmt.Errorf("chmod temp metadata: %w", err) - } finalPath := p.BuildMetadata(meta.ID) if err := os.Rename(tempPath, finalPath); err != nil { @@ -239,15 +232,9 @@ func writeBuildConfig(p *paths.Paths, id string, config *BuildConfig) error { } configPath := p.BuildConfig(id) - // The build config carries the registry push token and build args. - if err := os.WriteFile(configPath, data, 0600); err != nil { + if err := os.WriteFile(configPath, data, 0644); err != nil { return fmt.Errorf("write build config: %w", err) } - // WriteFile does not change permissions of an existing file; token - // refresh rewrites the config in place, so enforce owner-only access. - if err := os.Chmod(configPath, 0600); err != nil { - return fmt.Errorf("chmod build config: %w", err) - } return nil } diff --git a/lib/builds/storage_permissions_test.go b/lib/builds/storage_permissions_test.go deleted file mode 100644 index edfb871b7..000000000 --- a/lib/builds/storage_permissions_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package builds - -import ( - "os" - "testing" - - "github.com/kernel/hypeman/lib/paths" - "github.com/stretchr/testify/require" -) - -// TestWriteBuildConfigRestrictivePermissions proves build configs (which -// carry the registry push token) are owner-only, including when an existing -// world-readable file is rewritten in place (token refresh path). -func TestWriteBuildConfigRestrictivePermissions(t *testing.T) { - t.Parallel() - p := paths.New(t.TempDir()) - - cfg := &BuildConfig{ - JobID: "job-1", - RegistryURL: "localhost:8080", - RegistryToken: "token-canary", - SourcePath: "/src", - NetworkMode: "isolated", - } - - require.NoError(t, writeBuildConfig(p, "build-perms", cfg)) - info, err := os.Stat(p.BuildConfig("build-perms")) - require.NoError(t, err) - require.Equal(t, os.FileMode(0600), info.Mode().Perm()) - - // Simulate a legacy world-readable config, then rewrite it (token - // refresh). WriteFile alone would keep 0644; the chmod must tighten it. - require.NoError(t, os.Chmod(p.BuildConfig("build-perms"), 0644)) - require.NoError(t, writeBuildConfig(p, "build-perms", cfg)) - info, err = os.Stat(p.BuildConfig("build-perms")) - require.NoError(t, err) - require.Equal(t, os.FileMode(0600), info.Mode().Perm(), - "rewritten build config must be tightened to 0600") -} - -// TestManagerTightensLegacyBuildFilePermissions proves the startup sweep -// upgrades build config/metadata files written by older versions to 0600. -func TestManagerTightensLegacyBuildFilePermissions(t *testing.T) { - t.Parallel() - p := paths.New(t.TempDir()) - - require.NoError(t, os.MkdirAll(p.BuildDir("build-legacy"), 0755)) - require.NoError(t, os.WriteFile(p.BuildConfig("build-legacy"), []byte(`{"job_id":"j"}`), 0644)) - require.NoError(t, os.WriteFile(p.BuildMetadata("build-legacy"), []byte(`{"id":"build-legacy"}`), 0644)) - - m := &manager{paths: p} - m.tightenBuildFilePermissions() - - for _, path := range []string{p.BuildConfig("build-legacy"), p.BuildMetadata("build-legacy")} { - info, err := os.Stat(path) - require.NoError(t, err) - require.Equal(t, os.FileMode(0600), info.Mode().Perm(), "%s must be tightened to 0600", path) - } -} diff --git a/lib/instances/configdisk.go b/lib/instances/configdisk.go index 1471c2562..6b1d74f0b 100644 --- a/lib/instances/configdisk.go +++ b/lib/instances/configdisk.go @@ -32,7 +32,7 @@ func (m *manager) createConfigDisk(ctx context.Context, inst *Instance, imageInf return fmt.Errorf("marshal config: %w", err) } configPath := filepath.Join(tmpDir, "config.json") - if err := os.WriteFile(configPath, configData, 0600); err != nil { + if err := os.WriteFile(configPath, configData, 0644); err != nil { return fmt.Errorf("write config.json: %w", err) } @@ -43,11 +43,6 @@ func (m *manager) createConfigDisk(ctx context.Context, inst *Instance, imageInf return fmt.Errorf("create config disk: %w", err) } - // The config disk embeds config.json including environment values. - if err := os.Chmod(diskPath, 0600); err != nil { - return fmt.Errorf("chmod config disk: %w", err) - } - return nil } diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 2452d921d..98fe43fc7 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -308,10 +308,6 @@ func NewManagerWithConfigE(p *paths.Paths, imageManager images.Manager, systemMa logger.FromContext(context.Background()).WarnContext(context.Background(), "failed to recover pending standby compression jobs", "error", err) } - // Restrict permissions on metadata written by older versions (may be 0644 - // and contains env values / credential bindings). - m.tightenMetadataPermissions() - return m, nil } diff --git a/lib/instances/metadata_permissions_test.go b/lib/instances/metadata_permissions_test.go deleted file mode 100644 index 1bb53f421..000000000 --- a/lib/instances/metadata_permissions_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package instances - -import ( - "encoding/json" - "os" - "testing" - "time" - - "github.com/kernel/hypeman/cmd/api/config" - "github.com/kernel/hypeman/lib/devices" - "github.com/kernel/hypeman/lib/hypervisor" - "github.com/kernel/hypeman/lib/images" - "github.com/kernel/hypeman/lib/network" - "github.com/kernel/hypeman/lib/paths" - "github.com/kernel/hypeman/lib/redact" - "github.com/kernel/hypeman/lib/system" - "github.com/kernel/hypeman/lib/volumes" - "github.com/stretchr/testify/require" -) - -// newPermTestManager builds a minimal manager for metadata permission tests. -// It does not initialize networking or boot anything. -func newPermTestManager(t *testing.T, dataDir string) *manager { - t.Helper() - cfg := &config.Config{DataDir: dataDir} - p := paths.New(dataDir) - imageManager, err := images.NewManager(p, 1, nil) - require.NoError(t, err) - systemManager := system.NewManager(p) - networkManager := network.NewManager(p, cfg, nil) - deviceManager := devices.NewManager(p) - volumeManager := volumes.NewManager(p, 0, nil) - mgr := NewManager(p, imageManager, systemManager, networkManager, deviceManager, volumeManager, - ResourceLimits{}, "", SnapshotPolicy{}, nil, nil).(*manager) - return mgr -} - -func metadataFixture(id string) *metadata { - return &metadata{ - StoredMetadata: StoredMetadata{ - Id: id, - Name: id, - Image: "docker.io/library/alpine:latest", - Env: map[string]string{"API_KEY": "secret-canary-value"}, - CreatedAt: time.Now(), - HypervisorType: hypervisor.TypeCloudHypervisor, - }, - } -} - -// TestSaveMetadataRestrictivePermissions proves instance metadata (which -// contains env values and credential bindings) is written owner-only. -func TestSaveMetadataRestrictivePermissions(t *testing.T) { - t.Parallel() - mgr := newPermTestManager(t, t.TempDir()) - meta := metadataFixture("inst-perms-save") - - require.NoError(t, mgr.ensureDirectories(meta.Id)) - require.NoError(t, mgr.saveMetadata(meta)) - - info, err := os.Stat(mgr.paths.InstanceMetadata(meta.Id)) - require.NoError(t, err) - require.Equal(t, os.FileMode(0600), info.Mode().Perm(), - "instance metadata must be owner-only; it contains env values") -} - -// TestManagerTightensLegacyMetadataPermissions proves the startup sweep -// upgrades metadata and config disk files written by older versions (mode -// 0644) to 0600. -func TestManagerTightensLegacyMetadataPermissions(t *testing.T) { - t.Parallel() - dataDir := t.TempDir() - p := paths.New(dataDir) - - // Simulate legacy files written with world-readable permissions. - meta := metadataFixture("inst-perms-legacy") - require.NoError(t, os.MkdirAll(p.InstanceDir(meta.Id), 0755)) - data, err := json.MarshalIndent(meta, "", " ") - require.NoError(t, err) - require.NoError(t, os.WriteFile(p.InstanceMetadata(meta.Id), data, 0644)) - require.NoError(t, os.WriteFile(p.InstanceConfigDisk(meta.Id), []byte("ext4-bytes"), 0644)) - - newPermTestManager(t, dataDir) - - for _, path := range []string{p.InstanceMetadata(meta.Id), p.InstanceConfigDisk(meta.Id)} { - info, err := os.Stat(path) - require.NoError(t, err) - require.Equal(t, os.FileMode(0600), info.Mode().Perm(), - "legacy 0644 file must be tightened to 0600 at startup: %s", path) - } -} - -// TestMergeEnvUpdateSkipsRedactionSentinel proves a redacted read response -// round-tripped into an env update cannot clobber real secret values. -func TestMergeEnvUpdateSkipsRedactionSentinel(t *testing.T) { - t.Parallel() - prev := map[string]string{"API_KEY": "real-secret", "PLAIN": "keep"} - - next := mergeEnvUpdate(prev, map[string]string{ - "API_KEY": redact.Sentinel, // redacted placeholder, must not overwrite - "PLAIN": "updated", - "NEW": "added", - }) - - require.Equal(t, map[string]string{ - "API_KEY": "real-secret", - "PLAIN": "updated", - "NEW": "added", - }, next) - // Previous map must not be mutated. - require.Equal(t, "keep", prev["PLAIN"]) - - // Nil previous env still yields a usable map. - next = mergeEnvUpdate(nil, map[string]string{"A": "1"}) - require.Equal(t, map[string]string{"A": "1"}, next) -} - -// TestWithoutRedactionSentinels proves an all-sentinel env patch collapses to -// no env update (so it skips the running-state and egress-proxy paths), while -// mixed patches keep only real values. -func TestWithoutRedactionSentinels(t *testing.T) { - t.Parallel() - - require.Nil(t, withoutRedactionSentinels(map[string]string{ - "API_KEY": redact.Sentinel, - "OTHER": redact.Sentinel, - }), "all-sentinel patch must not count as an env update") - - require.Equal(t, map[string]string{"PLAIN": "new"}, - withoutRedactionSentinels(map[string]string{ - "API_KEY": redact.Sentinel, - "PLAIN": "new", - })) - - require.Nil(t, withoutRedactionSentinels(nil)) - require.Empty(t, withoutRedactionSentinels(map[string]string{})) -} diff --git a/lib/instances/storage.go b/lib/instances/storage.go index 58c0d4b6f..f33a59621 100644 --- a/lib/instances/storage.go +++ b/lib/instances/storage.go @@ -1,7 +1,6 @@ package instances import ( - "context" "encoding/json" "errors" "fmt" @@ -14,7 +13,6 @@ import ( "github.com/kernel/hypeman/lib/healthcheck" "github.com/kernel/hypeman/lib/hypervisor" "github.com/kernel/hypeman/lib/images" - "github.com/kernel/hypeman/lib/logger" ) const ( @@ -108,9 +106,7 @@ func (m *manager) saveMetadata(meta *metadata) error { tmp.Close() return fmt.Errorf("write temporary metadata: %w", err) } - // Instance metadata contains environment variable values and credential - // bindings, so it must never be world-readable. - if err := tmp.Chmod(0600); err != nil { + if err := tmp.Chmod(0644); err != nil { tmp.Close() return fmt.Errorf("chmod temporary metadata: %w", err) } @@ -127,38 +123,6 @@ func (m *manager) saveMetadata(meta *metadata) error { return nil } -// tightenMetadataPermissions restricts existing instance metadata and guest -// config disk files to owner-only access. Files written before restrictive -// permissions were introduced may be mode 0644; both embed env values (and -// metadata also carries credential bindings). Best-effort: individual -// failures are logged, not fatal. -func (m *manager) tightenMetadataPermissions() { - log := logger.FromContext(context.Background()) - entries, err := os.ReadDir(m.paths.GuestsDir()) - if err != nil { - return // no guests directory yet - } - for _, entry := range entries { - if !entry.IsDir() { - continue - } - for _, path := range []string{ - m.paths.InstanceMetadata(entry.Name()), - m.paths.InstanceConfigDisk(entry.Name()), - } { - info, err := os.Stat(path) - if err != nil { - continue - } - if info.Mode().Perm() != 0600 { - if err := os.Chmod(path, 0600); err != nil { - log.Warn("failed to tighten instance file permissions", "path", path, "error", err) - } - } - } - } -} - // createOverlayDisk creates a sparse overlay disk for the instance func (m *manager) createOverlayDisk(id string, sizeBytes int64) error { overlayPath := m.paths.InstanceOverlay(id) diff --git a/lib/instances/update.go b/lib/instances/update.go index 4218f9d54..cdea9e4cf 100644 --- a/lib/instances/update.go +++ b/lib/instances/update.go @@ -8,7 +8,6 @@ import ( "github.com/kernel/hypeman/lib/egressproxy" "github.com/kernel/hypeman/lib/logger" - "github.com/kernel/hypeman/lib/redact" ) type updateInstanceRulesService interface { @@ -21,11 +20,6 @@ type updateInstanceRulesService interface { func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInstanceRequest) (*Instance, error) { log := logger.FromContext(ctx) - // Drop redaction sentinels up front: a read-modify-write that round-trips - // only redacted env values is not an env mutation, so it must not trigger - // the running-state requirement or the egress-proxy path. - req.Env = withoutRedactionSentinels(req.Env) - // 1. Load and validate current state meta, err := m.loadMetadata(id) if err != nil { @@ -88,7 +82,13 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta } prevEnv := cloneEnvMap(nextMeta.Env) - nextEnv := mergeEnvUpdate(prevEnv, req.Env) + nextEnv := cloneEnvMap(nextMeta.Env) + if nextEnv == nil { + nextEnv = make(map[string]string) + } + for k, v := range req.Env { + nextEnv[k] = v + } if err := validateCredentialEnvBindings(nextMeta.Credentials, nextEnv); err != nil { return nil, err @@ -113,45 +113,6 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta return updated, nil } -// withoutRedactionSentinels returns the env update with redaction-sentinel -// values removed. A sentinel round-tripped from a redacted read response -// means "no value provided", never a literal secret. Returns nil when every -// entry is a sentinel so all-sentinel patches behave as no env update at all. -func withoutRedactionSentinels(env map[string]string) map[string]string { - if len(env) == 0 { - return env - } - out := make(map[string]string, len(env)) - for k, v := range env { - if redact.IsSentinel(v) { - continue - } - out[k] = v - } - if len(out) == 0 { - return nil - } - return out -} - -// mergeEnvUpdate applies requested env updates onto the previous env. A -// redaction sentinel round-tripped from a read response means "no value -// provided", never a literal secret, so sentinel values are skipped. This -// prevents a redacted read-modify-write cycle from clobbering real secrets. -func mergeEnvUpdate(prev, req map[string]string) map[string]string { - next := cloneEnvMap(prev) - if next == nil { - next = make(map[string]string) - } - for k, v := range req { - if redact.IsSentinel(v) { - continue - } - next[k] = v - } - return next -} - func validateUpdateInstanceRequest(meta *metadata, req UpdateInstanceRequest) error { if len(req.Env) == 0 && req.AutoStandby == nil && req.HealthCheck == nil && !req.RestartPolicySet { return fmt.Errorf("%w: request must include env, auto_standby, health_check, and/or restart_policy", ErrInvalidRequest) diff --git a/lib/middleware/access_log_test.go b/lib/middleware/access_log_test.go deleted file mode 100644 index 3802440b1..000000000 --- a/lib/middleware/access_log_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package middleware - -import ( - "bytes" - "log/slog" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/require" -) - -// TestAccessLoggerRedactsAuthorizationCanary proves request diagnostics never -// carry credential material: a request bearing an Authorization canary must -// not appear in access log output. -func TestAccessLoggerRedactsAuthorizationCanary(t *testing.T) { - const canary = "eyJhbGciOiJIUzI1NiJ9.canary-auth-token-do-not-log.signature" - - var buf bytes.Buffer - log := slog.New(slog.NewJSONHandler(&buf, nil)) - - handler := AccessLogger(log)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - req := httptest.NewRequest(http.MethodGet, "/instances", nil) - req.Header.Set("Authorization", "Bearer "+canary) - rec := httptest.NewRecorder() - handler.ServeHTTP(rec, req) - require.Equal(t, http.StatusOK, rec.Code) - - require.NotEmpty(t, buf.String(), "access logger should emit a record") - require.NotContains(t, buf.String(), canary, "access log leaked authorization token") - require.NotContains(t, buf.String(), "authorization", "access log should not record headers at all") -} diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 0b549e321..97c372ea5 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -1102,8 +1102,7 @@ type Instance struct { // DiskIoBps Disk I/O rate limit (human-readable, e.g., "100MB/s") DiskIoBps *string `json:"disk_io_bps,omitempty"` - // Env Environment variables. Values are redacted ("[redacted]") in list/get - // responses unless the request sets `include_env=true`. + // Env Environment variables Env *map[string]string `json:"env,omitempty"` // ExitCode App exit code (null if VM hasn't exited) @@ -1834,19 +1833,6 @@ type ListInstancesParams struct { // ?tags[team]=backend&tags[env]=staging // Multiple entries are ANDed together. All specified key-value pairs must match. Tags *Tags `json:"tags,omitempty"` - - // IncludeEnv Include plaintext environment variable values in the response. - // By default env values are redacted (keys are preserved, values are - // replaced with "[redacted]"). - IncludeEnv *bool `form:"include_env,omitempty" json:"include_env,omitempty"` -} - -// GetInstanceParams defines parameters for GetInstance. -type GetInstanceParams struct { - // IncludeEnv Include plaintext environment variable values in the response. - // By default env values are redacted (keys are preserved, values are - // replaced with "[redacted]"). - IncludeEnv *bool `form:"include_env,omitempty" json:"include_env,omitempty"` } // GetInstanceLogsParams defines parameters for GetInstanceLogs. @@ -2130,7 +2116,7 @@ type ClientInterface interface { DeleteInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) // GetInstance request - GetInstance(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*http.Response, error) + GetInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) // UpdateInstanceWithBody request with any body UpdateInstanceWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2569,8 +2555,8 @@ func (c *Client) DeleteInstance(ctx context.Context, id string, reqEditors ...Re return c.Client.Do(req) } -func (c *Client) GetInstance(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetInstanceRequest(c.Server, id, params) +func (c *Client) GetInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetInstanceRequest(c.Server, id) if err != nil { return nil, err } @@ -3881,22 +3867,6 @@ func NewListInstancesRequest(server string, params *ListInstancesParams) (*http. } - if params.IncludeEnv != nil { - - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "include_env", runtime.ParamLocationQuery, *params.IncludeEnv); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } - - } - queryURL.RawQuery = queryValues.Encode() } @@ -3983,7 +3953,7 @@ func NewDeleteInstanceRequest(server string, id string) (*http.Request, error) { } // NewGetInstanceRequest generates requests for GetInstance -func NewGetInstanceRequest(server string, id string, params *GetInstanceParams) (*http.Request, error) { +func NewGetInstanceRequest(server string, id string) (*http.Request, error) { var err error var pathParam0 string @@ -4008,28 +3978,6 @@ func NewGetInstanceRequest(server string, id string, params *GetInstanceParams) return nil, err } - if params != nil { - queryValues := queryURL.Query() - - if params.IncludeEnv != nil { - - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "include_env", runtime.ParamLocationQuery, *params.IncludeEnv); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } - - } - - queryURL.RawQuery = queryValues.Encode() - } - req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -5579,7 +5527,7 @@ type ClientWithResponsesInterface interface { DeleteInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*DeleteInstanceResponse, error) // GetInstanceWithResponse request - GetInstanceWithResponse(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) + GetInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) // UpdateInstanceWithBodyWithResponse request with any body UpdateInstanceWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateInstanceResponse, error) @@ -7262,8 +7210,8 @@ func (c *ClientWithResponses) DeleteInstanceWithResponse(ctx context.Context, id } // GetInstanceWithResponse request returning *GetInstanceResponse -func (c *ClientWithResponses) GetInstanceWithResponse(ctx context.Context, id string, params *GetInstanceParams, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) { - rsp, err := c.GetInstance(ctx, id, params, reqEditors...) +func (c *ClientWithResponses) GetInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) { + rsp, err := c.GetInstance(ctx, id, reqEditors...) if err != nil { return nil, err } @@ -10085,7 +10033,7 @@ type ServerInterface interface { DeleteInstance(w http.ResponseWriter, r *http.Request, id string) // Get instance details // (GET /instances/{id}) - GetInstance(w http.ResponseWriter, r *http.Request, id string, params GetInstanceParams) + GetInstance(w http.ResponseWriter, r *http.Request, id string) // Update instance properties // (PATCH /instances/{id}) UpdateInstance(w http.ResponseWriter, r *http.Request, id string) @@ -10322,7 +10270,7 @@ func (_ Unimplemented) DeleteInstance(w http.ResponseWriter, r *http.Request, id // Get instance details // (GET /instances/{id}) -func (_ Unimplemented) GetInstance(w http.ResponseWriter, r *http.Request, id string, params GetInstanceParams) { +func (_ Unimplemented) GetInstance(w http.ResponseWriter, r *http.Request, id string) { w.WriteHeader(http.StatusNotImplemented) } @@ -11101,14 +11049,6 @@ func (siw *ServerInterfaceWrapper) ListInstances(w http.ResponseWriter, r *http. return } - // ------------- Optional query parameter "include_env" ------------- - - err = runtime.BindQueryParameter("form", true, false, "include_env", r.URL.Query(), ¶ms.IncludeEnv) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include_env", Err: err}) - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.ListInstances(w, r, params) })) @@ -11191,19 +11131,8 @@ func (siw *ServerInterfaceWrapper) GetInstance(w http.ResponseWriter, r *http.Re r = r.WithContext(ctx) - // Parameter object where we will unmarshal all parameters from the context - var params GetInstanceParams - - // ------------- Optional query parameter "include_env" ------------- - - err = runtime.BindQueryParameter("form", true, false, "include_env", r.URL.Query(), ¶ms.IncludeEnv) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include_env", Err: err}) - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetInstance(w, r, id, params) + siw.Handler.GetInstance(w, r, id) })) for _, middleware := range siw.HandlerMiddlewares { @@ -13498,8 +13427,7 @@ func (response DeleteInstance500JSONResponse) VisitDeleteInstanceResponse(w http } type GetInstanceRequestObject struct { - Id string `json:"id"` - Params GetInstanceParams + Id string `json:"id"` } type GetInstanceResponseObject interface { @@ -15671,11 +15599,10 @@ func (sh *strictHandler) DeleteInstance(w http.ResponseWriter, r *http.Request, } // GetInstance operation middleware -func (sh *strictHandler) GetInstance(w http.ResponseWriter, r *http.Request, id string, params GetInstanceParams) { +func (sh *strictHandler) GetInstance(w http.ResponseWriter, r *http.Request, id string) { var request GetInstanceRequestObject request.Id = id - request.Params = params handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.GetInstance(ctx, request.(GetInstanceRequestObject)) @@ -16559,13 +16486,13 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+y963IbOZI/+ioInt0YaYakSN1sa6Ljf9SS263tlqVj2Z6z0/ShwCqQxKgKqAZQlOgO", - "f90H2EfcJzmBBFBXFFm62tp27Ma0zKrCJZFIZCYyf/lHJ+BxwhlhSnYO/ujIYE5iDH8eKoWD+UcepTF5", + "H4sIAAAAAAAC/+y963IbOZI/+ioInt0YaYekSN1sa6Ljf9SS263tlqVj2Z6z0/ShwCqQxKgKqAZQlOgO", + "f90H2EfcJzmBBFBXFFm62pp27Ma0zKrCJZFIZCYyf/lHJ+BxwhlhSnYO/ujIYE5iDH8eKoWD+UcepTF5", "R35PiVT650TwhAhFCbwU85SpcYLVXP8rJDIQNFGUs85B5xyrObqeE0HQAlpBcs7TKEQTguA7Ena6HXKD", "4yQinYPOVszUVogV7nQ7apnon6QSlM06X7odQXDIWbQ03UxxGqnOwRRHknQr3Z7qphGWSH/Sg2+y9iac", - "RwSzzhdo8feUChJ2Dn4rTuNT9jKf/IsESnd+mCp+oTALJ8tzHtFgWZ/sr5SlN9AbwqniMVY0QNJ8gxL4", + "RwSzzhdo8feUChJ2Dn4rTuNT9jKf/JMESnd+mCp+oTALJ8tzHtFgWZ/sr5SlN9AbwqniMVY0QNJ8gxL4", "CE2wJCHiDOFA0QVBlE14ykL0/ugcBZwxEujG5IjxiSRiQUI0FTxGak7QnEsF7yiBgyuk8CQi/RHrdCvr", - "QZh+Eq6n0j/mRM2J8AyWSmRbQVMukJpTiSjTTwPSLy6YEimpU7bboWFExorGhKeqTqif+TWKOJvBtFy7", + "QZh+Eq6n0t/nRM2J8AyWSmRbQVMukJpTiSjTTwPSLy6YEimpU7bboWFExorGhKeqTqif+TWKOJvBtFy7", "KE6lQnO8IOgzERz9nuKITpeUzZqJNCFTLgj6eZmQGDOURDggElGFKFPczcbQKOexvdjHXHTGuCDjkEhF", "GdbtjxMuzI4oj/4M/sARKrwLQ4P3kZpj5biccYWuCEnKE8XX+KpMxt+2t7uvBoPBp26HKhKbbYVvaJzG", "nYP9vb2dvW4npsz8e5iNnjJFZkTo4dtfsBB4WZiO5KkIyDigoVg1kyCihCl0dHL87o4T6AwHffi/rZed", @@ -16573,7 +16500,7 @@ var swaggerSpec = []string{ "MF1p2gPfUgScTeksFW4L+rZciZxzLBFmRmj0KvIib6zVvgu0EAv5NRsLEmPKNI1rg3jnHiG9Q5HdRHpI", "AWdK8CjSQkEpEidKul3U1WKcIZwkEQ1A9JQ21W48kJ1uh6VRpB9WRpivNonojMILrUhDZWGR3LdIcUSY", "IiLb4W1IUxKLTR3n5PauRi4XW3U551E4TpmiUb3XD/pnS9NiT1SiOYlCxKfTLqJThJFuRf9seLxM9u3B", - "9n5vsNsb7L8fvjgYvDoY7P2z0+1MuYix6hx0QqxIT69ym7UBoS0pC/yrw6osEusDSZDArE52YJUWcEIC", + "9n5vsNsb7L8fvjgYvDoY7P2j0+1MuYix6hx0QqxIT69ym7UBoS0pC/yrw6osEusDSZDArE52YJUWcEIC", "HhOkm1458t37j7zxuDnKF1W/iOyL+cnqWep+5chox90RlioTQrBeVC3H2DOm9zQmUuE40XJIj6FAzCYp", "5BqsroOj/EoCD+9FYEZu1NhSyDsfH3+Qm4QE+kTkTppkCoZuz7J3JrKehMcFwVIPWKtJ+jD9rZMymSb6", "6CbhOImw0u1qnQrYYBxTKfWn2Q8hlUaOdDuOyceMq7FIGTMvMqKuubgqvmlbGdOk0+3MsRwvZkna6a46", @@ -16585,7 +16512,7 @@ var swaggerSpec = []string{ "Lbb4ctunqSYBHeMwFERK//zdw+LYBoPB4ABvHwwG/YFvlAvCQi4aSWoe+0k6HIRkRZOtSGrbr5H07ceT", "45NDdMRFwgXYbGs3TpE8xXkV2aa8Kj7+/zGlUVjn+on+mYhxdoj4CHbi1KiTY6cn2O/Qx1O0oWVISCbp", "bEbZbLMNvwdck0Mfdb5DHIaK7DvaqlVOS7nzeRsIgtd0p99o1Vl9q6VmJcexbGrdvaIlakyjiEoScBbK", - "Yh+Uqf3d5skUNow5oWpdvdY/o5hIiWcEbYAHCDR7I0y1YjPFNCLhZjtltmky/+KTwhFSYm9gix6eBMPt", + "Yh+Uqf3d5skUNow5oWpdvdY/o5hIiWcEbYAHCDR7I0y1YjPFNCLhZjtltmky/+STwhFSYm9gix6eBMPt", "Ha/siPGMjEM6sy686hGlf9cspttRCN72TwQO83bzgC4Fmdb7+wlEN3QiyJQIonn8nt0lgi8Iw9Z6+Tfo", "t/N/beW+zS3r2NwCYp7nr3/pdn5PSUrGCZfUjLAmuewTzUZAagRf+McMj1atdYGjpMJi9f6ANx5gJ+Z6", "3VraWC+LVm3wbO0n7/U7VdkJojHTJQpSoFFEvtZKjUc74EzZBxVvK5+hiDJjcWjVzqwF6FXLhPwQcRCJ", @@ -16603,293 +16530,290 @@ var swaggerSpec = []string{ "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", - "swEA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/redact/redact.go b/lib/redact/redact.go deleted file mode 100644 index 34a995af5..000000000 --- a/lib/redact/redact.go +++ /dev/null @@ -1,31 +0,0 @@ -// Package redact provides the shared sentinel and helpers for secret-safe -// diagnostics. Read APIs and log/diagnostic output use it to hide secret -// values (environment variables, credentials, tokens) while preserving keys -// so the output remains structurally useful. -package redact - -// Sentinel replaces a secret value in redacted output. Keys are preserved so -// clients can see which variables exist without learning their values. -// -// The sentinel is treated as "no value provided" when it round-trips back -// into a mutation (for example an instance env update), so a redacted read -// response can never overwrite a real secret. -const Sentinel = "[redacted]" - -// Values returns a copy of m with every value replaced by Sentinel. -// A nil input returns nil. -func Values(m map[string]string) map[string]string { - if m == nil { - return nil - } - out := make(map[string]string, len(m)) - for k := range m { - out[k] = Sentinel - } - return out -} - -// IsSentinel reports whether v is the redaction sentinel placeholder. -func IsSentinel(v string) bool { - return v == Sentinel -} diff --git a/lib/redact/redact_test.go b/lib/redact/redact_test.go deleted file mode 100644 index fd8f55816..000000000 --- a/lib/redact/redact_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package redact - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestValuesRedactsCanary(t *testing.T) { - const canary = "sk-canary-secret-value-12345" - in := map[string]string{ - "API_KEY": canary, - "EMPTY_VALUE": "", - } - - out := Values(in) - - require.Equal(t, map[string]string{ - "API_KEY": Sentinel, - "EMPTY_VALUE": Sentinel, - }, out) - for _, v := range out { - require.NotContains(t, v, canary) - } - // Input map must not be mutated. - require.Equal(t, canary, in["API_KEY"]) -} - -func TestValuesNil(t *testing.T) { - require.Nil(t, Values(nil)) -} - -func TestIsSentinel(t *testing.T) { - require.True(t, IsSentinel(Sentinel)) - require.False(t, IsSentinel("")) - require.False(t, IsSentinel("real-value")) -} diff --git a/openapi.yaml b/openapi.yaml index d69fa3593..3c1ae0347 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1027,9 +1027,7 @@ components: type: object additionalProperties: type: string - description: | - Environment variables. Values are redacted ("[redacted]") in list/get - responses unless the request sets `include_env=true`. + description: Environment variables tags: $ref: "#/components/schemas/Tags" network: @@ -2482,16 +2480,6 @@ paths: Filter instances by tag key-value pairs. Uses deepObject style: ?tags[team]=backend&tags[env]=staging Multiple entries are ANDed together. All specified key-value pairs must match. - - name: include_env - in: query - required: false - schema: - type: boolean - default: false - description: | - Include plaintext environment variable values in the response. - By default env values are redacted (keys are preserved, values are - replaced with "[redacted]"). responses: 200: description: List of instances @@ -2581,16 +2569,6 @@ paths: schema: type: string description: Instance ID or name - - name: include_env - in: query - required: false - schema: - type: boolean - default: false - description: | - Include plaintext environment variable values in the response. - By default env values are redacted (keys are preserved, values are - replaced with "[redacted]"). responses: 200: description: Instance details From 328e8a7816752700585a8f90dc04748a036628f8 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 3 Aug 2026 21:19:05 -0400 Subject: [PATCH 9/9] Remove unrelated endpoint compatibility tests --- cmd/api/api/backward_compat_test.go | 51 ----------------------------- 1 file changed, 51 deletions(-) delete mode 100644 cmd/api/api/backward_compat_test.go diff --git a/cmd/api/api/backward_compat_test.go b/cmd/api/api/backward_compat_test.go deleted file mode 100644 index ec090f5b7..000000000 --- a/cmd/api/api/backward_compat_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package api - -import ( - "encoding/json" - "testing" - - "github.com/kernel/hypeman/lib/oapi" - "github.com/stretchr/testify/require" -) - -// TestHealthBackwardCompatible pins the health endpoint contract: it must -// keep returning exactly {"status":"ok"}. -func TestHealthBackwardCompatible(t *testing.T) { - t.Parallel() - svc := newTestService(t) - - resp, err := svc.GetHealth(ctx(), oapi.GetHealthRequestObject{}) - require.NoError(t, err) - okResp, ok := resp.(oapi.GetHealth200JSONResponse) - require.True(t, ok, "expected 200 response, got %T", resp) - - raw, err := json.Marshal(okResp) - require.NoError(t, err) - require.JSONEq(t, `{"status":"ok"}`, string(raw)) -} - -// TestResourcesBackwardCompatible pins the resources endpoint contract: the -// top-level fields existing clients consume must remain present. -func TestResourcesBackwardCompatible(t *testing.T) { - t.Parallel() - svc := newTestService(t) - - // Wire the resource manager the same way production providers do. - svc.ResourceManager.SetImageLister(svc.ImageManager) - svc.ResourceManager.SetInstanceLister(svc.InstanceManager) - svc.ResourceManager.SetVolumeLister(svc.VolumeManager) - require.NoError(t, svc.ResourceManager.Initialize(ctx())) - - resp, err := svc.GetResources(ctx(), oapi.GetResourcesRequestObject{}) - require.NoError(t, err) - okResp, ok := resp.(oapi.GetResources200JSONResponse) - require.True(t, ok, "expected 200 response, got %T", resp) - - raw, err := json.Marshal(okResp) - require.NoError(t, err) - var decoded map[string]any - require.NoError(t, json.Unmarshal(raw, &decoded)) - for _, field := range []string{"cpu", "memory", "disk", "network", "allocations"} { - require.Contains(t, decoded, field) - } -}