diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index ba5b448a..1c3b4585 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -798,6 +798,83 @@ func (s *ApiService) StatInstancePath(ctx context.Context, request oapi.StatInst return response, nil } +// UpdateInstanceCredentials replaces credential brokering policies for an instance +func (s *ApiService) UpdateInstanceCredentials(ctx context.Context, request oapi.UpdateInstanceCredentialsRequestObject) (oapi.UpdateInstanceCredentialsResponseObject, error) { + inst := mw.GetResolvedInstance[instances.Instance](ctx) + if inst == nil { + return oapi.UpdateInstanceCredentials500JSONResponse{ + Code: "internal_error", + Message: "resource not resolved", + }, nil + } + log := logger.FromContext(ctx) + + if request.Body == nil { + return oapi.UpdateInstanceCredentials400JSONResponse{ + Code: "invalid_request", + Message: "request body is required", + }, nil + } + + // Convert OAPI credentials to domain type + var credentials map[string]instances.CredentialPolicy + if request.Body.Credentials != nil { + credentials = make(map[string]instances.CredentialPolicy, len(*request.Body.Credentials)) + for credentialName, credential := range *request.Body.Credentials { + policy := instances.CredentialPolicy{ + Source: instances.CredentialSource{ + Env: credential.Source.Env, + }, + Inject: make([]instances.CredentialInjectRule, 0, len(credential.Inject)), + } + for _, inject := range credential.Inject { + rule := instances.CredentialInjectRule{ + As: instances.CredentialInjectAs{ + Header: inject.As.Header, + Format: inject.As.Format, + }, + } + if inject.Hosts != nil { + rule.Hosts = append([]string(nil), (*inject.Hosts)...) + } + policy.Inject = append(policy.Inject, rule) + } + credentials[credentialName] = policy + } + } + + env := make(map[string]string) + if request.Body.Env != nil { + env = *request.Body.Env + } + + result, err := s.InstanceManager.UpdateCredentials(ctx, inst.Id, instances.UpdateCredentialsRequest{ + Credentials: credentials, + Env: env, + }) + if err != nil { + switch { + case errors.Is(err, instances.ErrNotFound): + return oapi.UpdateInstanceCredentials404JSONResponse{ + Code: "not_found", + Message: "instance not found", + }, nil + case errors.Is(err, instances.ErrInvalidRequest): + return oapi.UpdateInstanceCredentials400JSONResponse{ + Code: "invalid_request", + Message: err.Error(), + }, nil + default: + log.ErrorContext(ctx, "failed to update credentials", "error", err) + return oapi.UpdateInstanceCredentials500JSONResponse{ + Code: "internal_error", + Message: "failed to update credentials", + }, nil + } + } + return oapi.UpdateInstanceCredentials200JSONResponse(instanceToOAPI(*result)), nil +} + // AttachVolume attaches a volume to an instance (not yet implemented) func (s *ApiService) AttachVolume(ctx context.Context, request oapi.AttachVolumeRequestObject) (oapi.AttachVolumeResponseObject, error) { return oapi.AttachVolume500JSONResponse{ diff --git a/lib/egressproxy/README.md b/lib/egressproxy/README.md index 42d77d42..f2eb03a3 100644 --- a/lib/egressproxy/README.md +++ b/lib/egressproxy/README.md @@ -37,6 +37,10 @@ This keeps real secrets out of the VM while still allowing authenticated egress - Egress enforcement is applied per instance TAP device and removed when the instance stops/standbys/deletes. - Enforcement intentionally targets TCP egress only. DNS/other non-TCP traffic is not rewritten and is not blocked by `all` mode. +## Credential rotation + +Credentials can be updated at runtime via `PATCH /instances/{id}/credentials` without restarting the VM or touching the guest. The endpoint uses merge semantics: only credentials included in the request are added or updated by name; credentials not mentioned are left unchanged. The proxy picks up new values immediately for running instances. For stopped or standby instances, the updated config takes effect on next start/restore. + ## Limits of enforcement - Header injection is applied to HTTP headers only (not request/response bodies). diff --git a/lib/egressproxy/service.go b/lib/egressproxy/service.go index 75cd2e7f..7fcddfa0 100644 --- a/lib/egressproxy/service.go +++ b/lib/egressproxy/service.go @@ -217,6 +217,27 @@ func compileHeaderInjectRules(cfgRules []HeaderInjectRuleConfig) ([]headerInject return out, nil } +// UpdateInstancePolicy atomically replaces the header inject rules for an +// already-registered instance without touching iptables enforcement rules. +// Returns ErrInstanceNotRegistered if the instance has no active policy. +func (s *Service) UpdateInstancePolicy(instanceID string, rules []HeaderInjectRuleConfig) error { + compiled, err := compileHeaderInjectRules(rules) + if err != nil { + return err + } + + s.mu.Lock() + defer s.mu.Unlock() + + sourceIP, ok := s.sourceIPByInstance[instanceID] + if !ok { + return ErrInstanceNotRegistered + } + + s.policiesBySourceIP[sourceIP] = sourcePolicy{headerInjectRules: compiled} + return nil +} + func (s *Service) UnregisterInstance(_ context.Context, instanceID string) { s.mu.Lock() sourceIP, ok := s.sourceIPByInstance[instanceID] diff --git a/lib/egressproxy/types.go b/lib/egressproxy/types.go index 29c2e838..41dd5472 100644 --- a/lib/egressproxy/types.go +++ b/lib/egressproxy/types.go @@ -7,7 +7,8 @@ const ( ) var ( - ErrGatewayMismatch = errors.New("egress proxy already initialized with different gateway") + ErrGatewayMismatch = errors.New("egress proxy already initialized with different gateway") + ErrInstanceNotRegistered = errors.New("instance not registered with egress proxy") ) // InstanceConfig defines per-instance proxy behavior. diff --git a/lib/egressproxy/update_policy_test.go b/lib/egressproxy/update_policy_test.go new file mode 100644 index 00000000..5810f480 --- /dev/null +++ b/lib/egressproxy/update_policy_test.go @@ -0,0 +1,175 @@ +package egressproxy + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUpdateInstancePolicy_ReplacesRules(t *testing.T) { + t.Parallel() + + svc := &Service{ + policiesBySourceIP: map[string]sourcePolicy{ + "10.0.0.2": { + headerInjectRules: []headerInjectRule{ + {headerName: "Authorization", headerValue: "Bearer old-key"}, + }, + }, + }, + sourceIPByInstance: map[string]string{ + "inst-1": "10.0.0.2", + }, + } + + err := svc.UpdateInstancePolicy("inst-1", []HeaderInjectRuleConfig{ + {HeaderName: "Authorization", HeaderValue: "Bearer new-key", AllowedDomains: []string{"api.openai.com"}}, + }) + require.NoError(t, err) + + // Verify the new rules are applied + hdr := http.Header{} + svc.applyHeaderInjections("10.0.0.2", "api.openai.com", hdr, true) + require.Equal(t, "Bearer new-key", hdr.Get("Authorization")) + + // Verify domain scoping works (should not inject for other domains) + hdr2 := http.Header{} + svc.applyHeaderInjections("10.0.0.2", "api.github.com", hdr2, true) + require.Empty(t, hdr2.Get("Authorization")) +} + +func TestUpdateInstancePolicy_ClearsRulesWithEmptySlice(t *testing.T) { + t.Parallel() + + svc := &Service{ + policiesBySourceIP: map[string]sourcePolicy{ + "10.0.0.2": { + headerInjectRules: []headerInjectRule{ + {headerName: "Authorization", headerValue: "Bearer old-key"}, + }, + }, + }, + sourceIPByInstance: map[string]string{ + "inst-1": "10.0.0.2", + }, + } + + err := svc.UpdateInstancePolicy("inst-1", []HeaderInjectRuleConfig{}) + require.NoError(t, err) + + hdr := http.Header{} + svc.applyHeaderInjections("10.0.0.2", "api.openai.com", hdr, true) + require.Empty(t, hdr.Get("Authorization")) +} + +func TestUpdateInstancePolicy_ErrorsForUnregisteredInstance(t *testing.T) { + t.Parallel() + + svc := &Service{ + policiesBySourceIP: map[string]sourcePolicy{}, + sourceIPByInstance: map[string]string{}, + } + + err := svc.UpdateInstancePolicy("nonexistent", []HeaderInjectRuleConfig{ + {HeaderName: "Authorization", HeaderValue: "Bearer key"}, + }) + require.ErrorIs(t, err, ErrInstanceNotRegistered) +} + +func TestUpdateInstancePolicy_IsIdempotent(t *testing.T) { + t.Parallel() + + svc := &Service{ + policiesBySourceIP: map[string]sourcePolicy{ + "10.0.0.2": {}, + }, + sourceIPByInstance: map[string]string{ + "inst-1": "10.0.0.2", + }, + } + + rules := []HeaderInjectRuleConfig{ + {HeaderName: "Authorization", HeaderValue: "Bearer same-key"}, + } + + // Call twice — should produce the same result + require.NoError(t, svc.UpdateInstancePolicy("inst-1", rules)) + require.NoError(t, svc.UpdateInstancePolicy("inst-1", rules)) + + hdr := http.Header{} + svc.applyHeaderInjections("10.0.0.2", "api.example.com", hdr, true) + require.Equal(t, "Bearer same-key", hdr.Get("Authorization")) +} + +func TestUpdateInstancePolicy_DoesNotAffectOtherInstances(t *testing.T) { + t.Parallel() + + matchers1, _ := compileDomainMatchers([]string{"api.openai.com"}) + svc := &Service{ + policiesBySourceIP: map[string]sourcePolicy{ + "10.0.0.2": { + headerInjectRules: []headerInjectRule{ + {headerName: "Authorization", headerValue: "Bearer inst1-key", domainMatchers: matchers1}, + }, + }, + "10.0.0.3": { + headerInjectRules: []headerInjectRule{ + {headerName: "Authorization", headerValue: "Bearer inst2-key"}, + }, + }, + }, + sourceIPByInstance: map[string]string{ + "inst-1": "10.0.0.2", + "inst-2": "10.0.0.3", + }, + } + + // Update inst-1 only + err := svc.UpdateInstancePolicy("inst-1", []HeaderInjectRuleConfig{ + {HeaderName: "Authorization", HeaderValue: "Bearer inst1-new-key"}, + }) + require.NoError(t, err) + + // inst-2 should be unaffected + hdr := http.Header{} + svc.applyHeaderInjections("10.0.0.3", "api.example.com", hdr, true) + require.Equal(t, "Bearer inst2-key", hdr.Get("Authorization")) +} + +func TestUpdateInstancePolicy_RegisterThenUpdate(t *testing.T) { + t.Parallel() + + // Simulate a service with a pre-registered instance (without starting a real listener) + matchers, err := compileDomainMatchers([]string{"api.openai.com"}) + require.NoError(t, err) + + svc := &Service{ + policiesBySourceIP: map[string]sourcePolicy{ + "10.0.0.2": { + headerInjectRules: []headerInjectRule{ + {headerName: "Authorization", headerValue: "Bearer original-key", domainMatchers: matchers}, + }, + }, + }, + sourceIPByInstance: map[string]string{ + "inst-1": "10.0.0.2", + }, + } + + // Verify original key + hdr := http.Header{} + svc.applyHeaderInjections("10.0.0.2", "api.openai.com", hdr, true) + require.Equal(t, "Bearer original-key", hdr.Get("Authorization")) + + // Update the policy with a rotated key + err = svc.UpdateInstancePolicy("inst-1", []HeaderInjectRuleConfig{ + {HeaderName: "Authorization", HeaderValue: "Bearer rotated-key", AllowedDomains: []string{"api.openai.com"}}, + }) + require.NoError(t, err) + + // Verify rotated key is used + hdr2 := http.Header{} + svc.applyHeaderInjections("10.0.0.2", "api.openai.com", hdr2, true) + require.Equal(t, "Bearer rotated-key", hdr2.Get("Authorization")) +} diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 8305a0c5..f5203f46 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -41,6 +41,7 @@ type Manager interface { StartInstance(ctx context.Context, id string, req StartInstanceRequest) (*Instance, error) StreamInstanceLogs(ctx context.Context, id string, tail int, follow bool, source LogSource) (<-chan string, error) RotateLogs(ctx context.Context, maxBytes int64, maxFiles int) error + UpdateCredentials(ctx context.Context, id string, req UpdateCredentialsRequest) (*Instance, error) AttachVolume(ctx context.Context, id string, volumeId string, req AttachVolumeRequest) (*Instance, error) DetachVolume(ctx context.Context, id string, volumeId string) (*Instance, error) // ListInstanceAllocations returns resource allocations for all instances. @@ -441,6 +442,14 @@ func (m *manager) RotateLogs(ctx context.Context, maxBytes int64, maxFiles int) return lastErr } +// UpdateCredentials replaces the credential policies for an instance. +func (m *manager) UpdateCredentials(ctx context.Context, id string, req UpdateCredentialsRequest) (*Instance, error) { + lock := m.getInstanceLock(id) + lock.Lock() + defer lock.Unlock() + return m.updateCredentials(ctx, id, req) +} + // AttachVolume attaches a volume to an instance (not yet implemented) func (m *manager) AttachVolume(ctx context.Context, id string, volumeId string, req AttachVolumeRequest) (*Instance, error) { return nil, fmt.Errorf("attach volume not yet implemented") diff --git a/lib/instances/types.go b/lib/instances/types.go index 8dc48837..e16425d8 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -267,6 +267,13 @@ type ForkSnapshotRequest struct { TargetHypervisor hypervisor.Type // Optional, allowed only for Stopped snapshots } +// UpdateCredentialsRequest is the domain request for replacing instance credentials. +// This is a full replacement — the provided credentials map replaces the existing one entirely. +type UpdateCredentialsRequest struct { + Credentials map[string]CredentialPolicy // New credential policies (replaces existing) + Env map[string]string // Updated env map containing real secret values +} + // AttachVolumeRequest is the domain request for attaching a volume (used for API compatibility) type AttachVolumeRequest struct { MountPath string diff --git a/lib/instances/update_credentials.go b/lib/instances/update_credentials.go new file mode 100644 index 00000000..beab79ec --- /dev/null +++ b/lib/instances/update_credentials.go @@ -0,0 +1,98 @@ +package instances + +import ( + "context" + "fmt" + + "github.com/kernel/hypeman/lib/egressproxy" + "github.com/kernel/hypeman/lib/logger" +) + +// updateCredentials merges credential policies into an instance's existing set. +// Credentials included in the request are added or updated by name; credentials +// not mentioned are left unchanged. If the instance has an active egress proxy +// registration (i.e., it is running), the proxy policy is updated atomically. +// For stopped/standby instances, only the stored metadata is updated — the proxy +// policy will be applied on next start/restore. +func (m *manager) updateCredentials(ctx context.Context, id string, req UpdateCredentialsRequest) (*Instance, error) { + log := logger.FromContext(ctx) + log.InfoContext(ctx, "updating credentials", "instance_id", id) + + // 1. Load instance metadata + meta, err := m.loadMetadata(id) + if err != nil { + return nil, err + } + stored := &meta.StoredMetadata + + // 2. Validate: credentials require egress proxy enabled + if len(req.Credentials) > 0 { + if stored.NetworkEgress == nil || !stored.NetworkEgress.Enabled { + return nil, fmt.Errorf("%w: credentials require network.egress.enabled=true", ErrInvalidRequest) + } + } + + // 3. Normalize and validate the incoming credential policies + normalized, err := normalizeCredentialPolicies(req.Credentials) + if err != nil { + return nil, err + } + + // 4. Merge env: stored env + request env (request values override) + mergedEnv := make(map[string]string, len(stored.Env)+len(req.Env)) + for k, v := range stored.Env { + mergedEnv[k] = v + } + for k, v := range req.Env { + mergedEnv[k] = v + } + + // 5. Merge credentials: start from existing, overlay with incoming + mergedCreds := cloneCredentialPolicies(stored.Credentials) + if mergedCreds == nil { + mergedCreds = make(map[string]CredentialPolicy) + } + for name, policy := range normalized { + mergedCreds[name] = policy + } + + // 6. Validate env bindings for the full merged credential set + if len(mergedCreds) > 0 { + if err := validateCredentialEnvBindings(mergedCreds, mergedEnv); err != nil { + return nil, err + } + } + + // 7. Update stored metadata + stored.Credentials = mergedCreds + stored.Env = mergedEnv + + // 6. Update proxy policy if instance has an active egress proxy registration + m.egressProxyMu.Lock() + svc := m.egressProxy + m.egressProxyMu.Unlock() + + if svc != nil { + rules := buildEgressProxyInjectRules(stored.NetworkEgress, stored.Credentials, stored.Env) + if err := svc.UpdateInstancePolicy(id, rules); err != nil { + // ErrInstanceNotRegistered means instance isn't running with egress proxy — + // that's fine, the updated metadata will be picked up on next start/restore. + if err != egressproxy.ErrInstanceNotRegistered { + return nil, fmt.Errorf("update egress proxy policy: %w", err) + } + log.DebugContext(ctx, "instance not registered with egress proxy, skipping policy update", "instance_id", id) + } else { + log.InfoContext(ctx, "updated egress proxy policy", "instance_id", id) + } + } + + // 7. Persist metadata + if err := m.saveMetadata(meta); err != nil { + return nil, fmt.Errorf("save metadata: %w", err) + } + + // 8. Return updated instance + inst := m.toInstance(ctx, meta) + log.InfoContext(ctx, "credentials updated", "instance_id", id, "credential_count", len(stored.Credentials)) + return &inst, nil +} diff --git a/lib/instances/update_credentials_test.go b/lib/instances/update_credentials_test.go new file mode 100644 index 00000000..a5e82097 --- /dev/null +++ b/lib/instances/update_credentials_test.go @@ -0,0 +1,345 @@ +package instances + +import ( + "os" + "testing" + + "github.com/kernel/hypeman/lib/paths" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// setupCredentialsTestManager creates a minimal manager with temp paths and prepopulated metadata. +func setupCredentialsTestManager(t *testing.T, meta *metadata) *manager { + t.Helper() + tmpDir := t.TempDir() + p := paths.New(tmpDir) + + m := &manager{paths: p} + + // Create instance directory so saveMetadata can write + require.NoError(t, os.MkdirAll(p.InstanceDir(meta.Id), 0755)) + require.NoError(t, m.saveMetadata(meta)) + + return m +} + +func TestUpdateCredentials_ValidatesEgressRequired(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: nil, + Env: map[string]string{}, + }, + }) + + _, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: map[string]CredentialPolicy{ + "MY_KEY": { + Source: CredentialSource{Env: "MY_KEY"}, + Inject: []CredentialInjectRule{ + {As: CredentialInjectAs{Header: "Authorization", Format: "Bearer ${value}"}}, + }, + }, + }, + Env: map[string]string{"MY_KEY": "real-secret"}, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidRequest) + assert.Contains(t, err.Error(), "credentials require network.egress.enabled=true") +} + +func TestUpdateCredentials_ValidatesEnvBinding(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: &NetworkEgressPolicy{Enabled: true}, + Env: map[string]string{}, + }, + }) + + _, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: map[string]CredentialPolicy{ + "MY_KEY": { + Source: CredentialSource{Env: "MY_KEY"}, + Inject: []CredentialInjectRule{ + {As: CredentialInjectAs{Header: "Authorization", Format: "Bearer ${value}"}}, + }, + }, + }, + Env: map[string]string{}, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidRequest) + assert.Contains(t, err.Error(), "must be present in env") +} + +func TestUpdateCredentials_PreservesExistingWhenNilRequest(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: &NetworkEgressPolicy{Enabled: true}, + Env: map[string]string{"MY_KEY": "old-secret"}, + Credentials: map[string]CredentialPolicy{ + "MY_KEY": { + Source: CredentialSource{Env: "MY_KEY"}, + Inject: []CredentialInjectRule{ + {As: CredentialInjectAs{Header: "Authorization", Format: "Bearer ${value}"}}, + }, + }, + }, + }, + }) + + // PATCH with nil credentials should preserve existing credentials + result, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: nil, + Env: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, result) + + reloaded, err := m.loadMetadata("test-inst") + require.NoError(t, err) + require.Len(t, reloaded.StoredMetadata.Credentials, 1) + _, ok := reloaded.StoredMetadata.Credentials["MY_KEY"] + assert.True(t, ok, "existing credential should be preserved") +} + +func TestUpdateCredentials_MergesNewCredentialWithExisting(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: &NetworkEgressPolicy{Enabled: true}, + Env: map[string]string{ + "OPENAI_KEY": "openai-secret", + "ANTHROPIC_KEY": "anthropic-secret", + }, + Credentials: map[string]CredentialPolicy{ + "OPENAI_KEY": { + Source: CredentialSource{Env: "OPENAI_KEY"}, + Inject: []CredentialInjectRule{ + { + Hosts: []string{"api.openai.com"}, + As: CredentialInjectAs{Header: "Authorization", Format: "Bearer ${value}"}, + }, + }, + }, + }, + }, + }) + + // PATCH: add a new credential without touching the existing one + result, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: map[string]CredentialPolicy{ + "ANTHROPIC_KEY": { + Source: CredentialSource{Env: "ANTHROPIC_KEY"}, + Inject: []CredentialInjectRule{ + { + Hosts: []string{"api.anthropic.com"}, + As: CredentialInjectAs{Header: "x-api-key", Format: "${value}"}, + }, + }, + }, + }, + Env: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, result) + + reloaded, err := m.loadMetadata("test-inst") + require.NoError(t, err) + require.Len(t, reloaded.StoredMetadata.Credentials, 2) + + // Existing credential preserved + openai, ok := reloaded.StoredMetadata.Credentials["OPENAI_KEY"] + require.True(t, ok, "existing OPENAI_KEY credential should be preserved") + assert.Equal(t, "OPENAI_KEY", openai.Source.Env) + assert.Equal(t, []string{"api.openai.com"}, openai.Inject[0].Hosts) + + // New credential added + anthropic, ok := reloaded.StoredMetadata.Credentials["ANTHROPIC_KEY"] + require.True(t, ok, "new ANTHROPIC_KEY credential should be added") + assert.Equal(t, "ANTHROPIC_KEY", anthropic.Source.Env) + assert.Equal(t, []string{"api.anthropic.com"}, anthropic.Inject[0].Hosts) +} + +func TestUpdateCredentials_OverridesExistingByName(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: &NetworkEgressPolicy{Enabled: true}, + Env: map[string]string{"MY_KEY": "old-secret"}, + Credentials: map[string]CredentialPolicy{ + "MY_KEY": { + Source: CredentialSource{Env: "MY_KEY"}, + Inject: []CredentialInjectRule{ + { + Hosts: []string{"api.old.com"}, + As: CredentialInjectAs{Header: "Authorization", Format: "Bearer ${value}"}, + }, + }, + }, + }, + }, + }) + + // PATCH: update existing credential with new hosts + result, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: map[string]CredentialPolicy{ + "MY_KEY": { + Source: CredentialSource{Env: "MY_KEY"}, + Inject: []CredentialInjectRule{ + { + Hosts: []string{"api.new.com"}, + As: CredentialInjectAs{Header: "Authorization", Format: "Bearer ${value}"}, + }, + }, + }, + }, + Env: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, result) + + reloaded, err := m.loadMetadata("test-inst") + require.NoError(t, err) + require.Len(t, reloaded.StoredMetadata.Credentials, 1) + policy := reloaded.StoredMetadata.Credentials["MY_KEY"] + assert.Equal(t, []string{"api.new.com"}, policy.Inject[0].Hosts, "credential should be updated with new hosts") +} + +func TestUpdateCredentials_MergesEnv(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: &NetworkEgressPolicy{Enabled: true}, + Env: map[string]string{ + "MY_KEY": "old-secret", + "EXISTING_VAR": "keep-me", + }, + }, + }) + + result, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: map[string]CredentialPolicy{ + "MY_KEY": { + Source: CredentialSource{Env: "MY_KEY"}, + Inject: []CredentialInjectRule{ + {As: CredentialInjectAs{Header: "Authorization", Format: "Bearer ${value}"}}, + }, + }, + }, + Env: map[string]string{"MY_KEY": "new-rotated-secret"}, + }) + require.NoError(t, err) + require.NotNil(t, result) + + reloaded, err := m.loadMetadata("test-inst") + require.NoError(t, err) + assert.Equal(t, "new-rotated-secret", reloaded.StoredMetadata.Env["MY_KEY"]) + assert.Equal(t, "keep-me", reloaded.StoredMetadata.Env["EXISTING_VAR"]) +} + +func TestUpdateCredentials_NormalizesCredentials(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: &NetworkEgressPolicy{Enabled: true}, + Env: map[string]string{"MY_KEY": "secret"}, + }, + }) + + result, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: map[string]CredentialPolicy{ + " MY_KEY ": { + Source: CredentialSource{Env: " MY_KEY "}, + Inject: []CredentialInjectRule{ + { + Hosts: []string{" API.OpenAI.com "}, + As: CredentialInjectAs{Header: " Authorization ", Format: " Bearer ${value} "}, + }, + }, + }, + }, + Env: map[string]string{}, + }) + require.NoError(t, err) + require.NotNil(t, result) + + reloaded, err := m.loadMetadata("test-inst") + require.NoError(t, err) + policy, ok := reloaded.StoredMetadata.Credentials["MY_KEY"] + require.True(t, ok) + assert.Equal(t, "MY_KEY", policy.Source.Env) + assert.Equal(t, "Authorization", policy.Inject[0].As.Header) + assert.Equal(t, "Bearer ${value}", policy.Inject[0].As.Format) + assert.Equal(t, []string{"api.openai.com"}, policy.Inject[0].Hosts) +} + +func TestUpdateCredentials_RejectsInvalidFormat(t *testing.T) { + t.Parallel() + + m := setupCredentialsTestManager(t, &metadata{ + StoredMetadata: StoredMetadata{ + Id: "test-inst", + Name: "test", + NetworkEnabled: true, + NetworkEgress: &NetworkEgressPolicy{Enabled: true}, + Env: map[string]string{"MY_KEY": "secret"}, + }, + }) + + _, err := m.updateCredentials(t.Context(), "test-inst", UpdateCredentialsRequest{ + Credentials: map[string]CredentialPolicy{ + "MY_KEY": { + Source: CredentialSource{Env: "MY_KEY"}, + Inject: []CredentialInjectRule{ + {As: CredentialInjectAs{Header: "Authorization", Format: "Bearer no-template"}}, + }, + }, + }, + Env: map[string]string{}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "must include ${value}") +} + +func TestUpdateCredentials_NotFoundForMissingInstance(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + m := &manager{paths: paths.New(tmpDir)} + + _, err := m.updateCredentials(t.Context(), "nonexistent", UpdateCredentialsRequest{}) + require.Error(t, err) + assert.ErrorIs(t, err, ErrNotFound) +} diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 161067ad..578467d7 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -1039,6 +1039,19 @@ type SnapshotTargetState string // Tags User-defined key-value tags. type Tags map[string]string +// UpdateCredentialsRequest defines model for UpdateCredentialsRequest. +type UpdateCredentialsRequest struct { + // Credentials Credential brokering policies to add or update, keyed by guest-visible env var name. + // Credentials included here are merged into the existing set: matching names are + // overwritten, new names are added, and credentials not mentioned are left unchanged. + Credentials *map[string]CreateInstanceRequestCredential `json:"credentials,omitempty"` + + // Env Environment variable updates. Values here are merged with the existing env map + // (new keys are added, existing keys are overwritten). Use this to supply or rotate + // the real secret values referenced by credential policies. + Env *map[string]string `json:"env,omitempty"` +} + // Volume defines model for Volume. type Volume struct { // Attachments List of current attachments (empty if not attached) @@ -1264,6 +1277,9 @@ type CreateIngressJSONRequestBody = CreateIngressRequest // CreateInstanceJSONRequestBody defines body for CreateInstance for application/json ContentType. type CreateInstanceJSONRequestBody = CreateInstanceRequest +// UpdateInstanceCredentialsJSONRequestBody defines body for UpdateInstanceCredentials for application/json ContentType. +type UpdateInstanceCredentialsJSONRequestBody = UpdateCredentialsRequest + // ForkInstanceJSONRequestBody defines body for ForkInstance for application/json ContentType. type ForkInstanceJSONRequestBody = ForkInstanceRequest @@ -1435,6 +1451,11 @@ type ClientInterface interface { // GetInstance request GetInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + // UpdateInstanceCredentialsWithBody request with any body + UpdateInstanceCredentialsWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateInstanceCredentials(ctx context.Context, id string, body UpdateInstanceCredentialsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ForkInstanceWithBody request with any body ForkInstanceWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1840,6 +1861,30 @@ func (c *Client) GetInstance(ctx context.Context, id string, reqEditors ...Reque return c.Client.Do(req) } +func (c *Client) UpdateInstanceCredentialsWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateInstanceCredentialsRequestWithBody(c.Server, id, contentType, body) + 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) UpdateInstanceCredentials(ctx context.Context, id string, body UpdateInstanceCredentialsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateInstanceCredentialsRequest(c.Server, id, body) + 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) ForkInstanceWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewForkInstanceRequestWithBody(c.Server, id, contentType, body) if err != nil { @@ -3088,6 +3133,53 @@ func NewGetInstanceRequest(server string, id string) (*http.Request, error) { return req, nil } +// NewUpdateInstanceCredentialsRequest calls the generic UpdateInstanceCredentials builder with application/json body +func NewUpdateInstanceCredentialsRequest(server string, id string, body UpdateInstanceCredentialsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpdateInstanceCredentialsRequestWithBody(server, id, "application/json", bodyReader) +} + +// NewUpdateInstanceCredentialsRequestWithBody generates requests for UpdateInstanceCredentials with any type of body +func NewUpdateInstanceCredentialsRequestWithBody(server string, id string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/instances/%s/credentials", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewForkInstanceRequest calls the generic ForkInstance builder with application/json body func NewForkInstanceRequest(server string, id string, body ForkInstanceJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -4277,6 +4369,11 @@ type ClientWithResponsesInterface interface { // GetInstanceWithResponse request GetInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetInstanceResponse, error) + // UpdateInstanceCredentialsWithBodyWithResponse request with any body + UpdateInstanceCredentialsWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateInstanceCredentialsResponse, error) + + UpdateInstanceCredentialsWithResponse(ctx context.Context, id string, body UpdateInstanceCredentialsJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateInstanceCredentialsResponse, error) + // ForkInstanceWithBodyWithResponse request with any body ForkInstanceWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ForkInstanceResponse, error) @@ -4916,6 +5013,31 @@ func (r GetInstanceResponse) StatusCode() int { return 0 } +type UpdateInstanceCredentialsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Instance + JSON400 *Error + JSON404 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r UpdateInstanceCredentialsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateInstanceCredentialsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ForkInstanceResponse struct { Body []byte HTTPResponse *http.Response @@ -5701,6 +5823,23 @@ func (c *ClientWithResponses) GetInstanceWithResponse(ctx context.Context, id st return ParseGetInstanceResponse(rsp) } +// UpdateInstanceCredentialsWithBodyWithResponse request with arbitrary body returning *UpdateInstanceCredentialsResponse +func (c *ClientWithResponses) UpdateInstanceCredentialsWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateInstanceCredentialsResponse, error) { + rsp, err := c.UpdateInstanceCredentialsWithBody(ctx, id, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateInstanceCredentialsResponse(rsp) +} + +func (c *ClientWithResponses) UpdateInstanceCredentialsWithResponse(ctx context.Context, id string, body UpdateInstanceCredentialsJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateInstanceCredentialsResponse, error) { + rsp, err := c.UpdateInstanceCredentials(ctx, id, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateInstanceCredentialsResponse(rsp) +} + // ForkInstanceWithBodyWithResponse request with arbitrary body returning *ForkInstanceResponse func (c *ClientWithResponses) ForkInstanceWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ForkInstanceResponse, error) { rsp, err := c.ForkInstanceWithBody(ctx, id, contentType, body, reqEditors...) @@ -6917,6 +7056,53 @@ func ParseGetInstanceResponse(rsp *http.Response) (*GetInstanceResponse, error) return response, nil } +// ParseUpdateInstanceCredentialsResponse parses an HTTP response from a UpdateInstanceCredentialsWithResponse call +func ParseUpdateInstanceCredentialsResponse(rsp *http.Response) (*UpdateInstanceCredentialsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpdateInstanceCredentialsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Instance + 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 == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &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 +} + // ParseForkInstanceResponse parses an HTTP response from a ForkInstanceWithResponse call func ParseForkInstanceResponse(rsp *http.Response) (*ForkInstanceResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -7994,6 +8180,9 @@ type ServerInterface interface { // Get instance details // (GET /instances/{id}) GetInstance(w http.ResponseWriter, r *http.Request, id string) + // Update instance credentials + // (PATCH /instances/{id}/credentials) + UpdateInstanceCredentials(w http.ResponseWriter, r *http.Request, id string) // Fork an instance from stopped, standby, or running (with from_running=true) // (POST /instances/{id}/fork) ForkInstance(w http.ResponseWriter, r *http.Request, id string) @@ -8204,6 +8393,12 @@ func (_ Unimplemented) GetInstance(w http.ResponseWriter, r *http.Request, id st w.WriteHeader(http.StatusNotImplemented) } +// Update instance credentials +// (PATCH /instances/{id}/credentials) +func (_ Unimplemented) UpdateInstanceCredentials(w http.ResponseWriter, r *http.Request, id string) { + w.WriteHeader(http.StatusNotImplemented) +} + // Fork an instance from stopped, standby, or running (with from_running=true) // (POST /instances/{id}/fork) func (_ Unimplemented) ForkInstance(w http.ResponseWriter, r *http.Request, id string) { @@ -9004,6 +9199,37 @@ func (siw *ServerInterfaceWrapper) GetInstance(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } +// UpdateInstanceCredentials operation middleware +func (siw *ServerInterfaceWrapper) UpdateInstanceCredentials(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + 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.UpdateInstanceCredentials(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ForkInstance operation middleware func (siw *ServerInterfaceWrapper) ForkInstance(w http.ResponseWriter, r *http.Request) { @@ -9994,6 +10220,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/instances/{id}", wrapper.GetInstance) }) + r.Group(func(r chi.Router) { + r.Patch(options.BaseURL+"/instances/{id}/credentials", wrapper.UpdateInstanceCredentials) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/instances/{id}/fork", wrapper.ForkInstance) }) @@ -10981,6 +11210,51 @@ func (response GetInstance500JSONResponse) VisitGetInstanceResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type UpdateInstanceCredentialsRequestObject struct { + Id string `json:"id"` + Body *UpdateInstanceCredentialsJSONRequestBody +} + +type UpdateInstanceCredentialsResponseObject interface { + VisitUpdateInstanceCredentialsResponse(w http.ResponseWriter) error +} + +type UpdateInstanceCredentials200JSONResponse Instance + +func (response UpdateInstanceCredentials200JSONResponse) VisitUpdateInstanceCredentialsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateInstanceCredentials400JSONResponse Error + +func (response UpdateInstanceCredentials400JSONResponse) VisitUpdateInstanceCredentialsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateInstanceCredentials404JSONResponse Error + +func (response UpdateInstanceCredentials404JSONResponse) VisitUpdateInstanceCredentialsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateInstanceCredentials500JSONResponse Error + +func (response UpdateInstanceCredentials500JSONResponse) VisitUpdateInstanceCredentialsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type ForkInstanceRequestObject struct { Id string `json:"id"` Body *ForkInstanceJSONRequestBody @@ -12030,6 +12304,9 @@ type StrictServerInterface interface { // Get instance details // (GET /instances/{id}) GetInstance(ctx context.Context, request GetInstanceRequestObject) (GetInstanceResponseObject, error) + // Update instance credentials + // (PATCH /instances/{id}/credentials) + UpdateInstanceCredentials(ctx context.Context, request UpdateInstanceCredentialsRequestObject) (UpdateInstanceCredentialsResponseObject, error) // Fork an instance from stopped, standby, or running (with from_running=true) // (POST /instances/{id}/fork) ForkInstance(ctx context.Context, request ForkInstanceRequestObject) (ForkInstanceResponseObject, error) @@ -12747,6 +13024,39 @@ func (sh *strictHandler) GetInstance(w http.ResponseWriter, r *http.Request, id } } +// UpdateInstanceCredentials operation middleware +func (sh *strictHandler) UpdateInstanceCredentials(w http.ResponseWriter, r *http.Request, id string) { + var request UpdateInstanceCredentialsRequestObject + + request.Id = id + + var body UpdateInstanceCredentialsJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UpdateInstanceCredentials(ctx, request.(UpdateInstanceCredentialsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateInstanceCredentials") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateInstanceCredentialsResponseObject); ok { + if err := validResponse.VisitUpdateInstanceCredentialsResponse(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)) + } +} + // ForkInstance operation middleware func (sh *strictHandler) ForkInstance(w http.ResponseWriter, r *http.Request, id string) { var request ForkInstanceRequestObject @@ -13374,217 +13684,225 @@ 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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", + "/w8AAP//eOC5FdwzAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/scopes/scopes.go b/lib/scopes/scopes.go index 1ebbc49a..dba90766 100644 --- a/lib/scopes/scopes.go +++ b/lib/scopes/scopes.go @@ -239,6 +239,7 @@ var RouteScopes = map[string]Scope{ "GET /instances/{id}/stat": InstanceRead, "GET /instances/{id}/stats": InstanceRead, "POST /instances/{id}/stop": InstanceWrite, + "PATCH /instances/{id}/credentials": InstanceWrite, "DELETE /instances/{id}/volumes/{volumeId}": VolumeWrite, "POST /instances/{id}/volumes/{volumeId}": VolumeWrite, diff --git a/openapi.yaml b/openapi.yaml index 1f0b5e1a..ca36b210 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -203,6 +203,35 @@ components: $ref: "#/components/schemas/CreateInstanceRequestCredentialInject" minItems: 1 + UpdateCredentialsRequest: + type: object + properties: + credentials: + type: object + description: | + Credential brokering policies to add or update, keyed by guest-visible env var name. + Credentials included here are merged into the existing set: matching names are + overwritten, new names are added, and credentials not mentioned are left unchanged. + additionalProperties: + $ref: "#/components/schemas/CreateInstanceRequestCredential" + example: + OUTBOUND_OPENAI_KEY: + source: + env: OUTBOUND_OPENAI_KEY + inject: + - hosts: [api.openai.com, "*.openai.com"] + as: + header: Authorization + format: "Bearer ${value}" + env: + type: object + additionalProperties: + type: string + description: | + Environment variable updates. Values here are merged with the existing env map + (new keys are added, existing keys are overwritten). Use this to supply or rotate + the real secret values referenced by credential policies. + CreateInstanceRequest: type: object required: [name, image] @@ -2397,6 +2426,60 @@ paths: schema: $ref: "#/components/schemas/Error" + /instances/{id}/credentials: + patch: + summary: Update instance credentials + description: | + Merges credential brokering policies for an instance. Credentials included in the + request are added or updated by name; credentials not mentioned are left unchanged. + Real secrets stay on the host; the proxy rewrites headers at request time, so + updating credentials does NOT require any VM-side changes. + + If the instance is running with an active egress proxy, the proxy policy is updated + atomically. For stopped or standby instances, the updated config is persisted and + takes effect on next start/restore. + operationId: updateInstanceCredentials + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Instance ID or name + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateCredentialsRequest" + responses: + 200: + description: Credentials updated + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + 400: + description: Bad request - invalid credentials + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 404: + description: Instance not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /instances/{id}/volumes/{volumeId}: post: summary: Attach volume to instance