diff --git a/.ko.yaml b/.ko.yaml index 774665cec..a001cee43 100644 --- a/.ko.yaml +++ b/.ko.yaml @@ -21,3 +21,6 @@ defaultPlatforms: baseImageOverrides: github.com/agent-substrate/substrate/demos/sandbox: alpine github.com/agent-substrate/substrate/demos/agent-secret: alpine + +x-agentgatewayEgressBaseImageOverrides: + github.com/agent-substrate/substrate/cmd/ateom-gvisor: cr.agentgateway.dev/agentgateway:latest-dev diff --git a/charts/substrate-crds/templates/ate.dev_actortemplates.yaml b/charts/substrate-crds/templates/ate.dev_actortemplates.yaml index cdb07e788..e0b9a98f4 100644 --- a/charts/substrate-crds/templates/ate.dev_actortemplates.yaml +++ b/charts/substrate-crds/templates/ate.dev_actortemplates.yaml @@ -152,6 +152,329 @@ spec: type: object maxItems: 10 type: array + egressPolicy: + description: |- + EgressPolicy defines the default outbound network policy for actors + created from this template. + properties: + allow: + description: Allow contains destination rules actors created from + this template may reach. + items: + properties: + credentials: + description: |- + Credentials configures explicit egress gateway credential injection for + matching outbound requests. + properties: + inject: + description: |- + Inject configures credentials that the egress gateway injects into + matching outbound requests. Values are referenced from Kubernetes Secrets; + the policy does not contain credential material. + items: + properties: + header: + description: Header is the outbound HTTP header + name to set. + type: string + valueFrom: + description: ValueFrom selects the source of the + injected credential value. + properties: + secretKeyRef: + description: SecretKeyRef selects a key in + a Kubernetes Secret. + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - header + - valueFrom + type: object + type: array + type: object + name: + description: Name is an optional human-readable identifier + for this rule. + type: string + ports: + description: |- + Ports is the list of destination ports matched by this rule. + If empty, the rule applies to all destination ports. + items: + properties: + port: + description: Port is the destination port number. + format: int32 + type: integer + protocol: + description: Protocol is the transport protocol for + this port. + type: string + required: + - port + type: object + type: array + tls: + description: TLS defines transport security requirements + for this destination. + properties: + intercept: + description: Intercept configures explicit TLS interception + for matching egress traffic. + properties: + issuerSecretRef: + description: |- + IssuerSecretRef references the CA material used by the egress gateway to + issue certificates for intercepted TLS traffic. + properties: + name: + description: name is unique within a namespace + to reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + validateUpstream: + description: |- + ValidateUpstream controls whether the egress gateway validates the + upstream service certificate before proxying intercepted traffic. + type: boolean + type: object + mode: + description: Mode controls how TLS is handled for matching + egress traffic. + enum: + - Require + - Originate + - Intercept + - Disable + type: string + required: + description: Required controls whether matching egress + traffic must use TLS. + type: boolean + type: object + to: + description: To lists the destinations matched by this rule. + items: + properties: + host: + description: Host is the DNS name to match for egress + traffic. + type: string + ipBlock: + description: IPBlock is the IP range to match for + egress traffic. + properties: + cidr: + description: CIDR is an IP address range in CIDR + notation. + type: string + required: + - cidr + type: object + type: object + type: array + type: object + type: array + audit: + description: Audit configures egress logging and tracing for actors + created from this template. + properties: + logs: + description: Logs enables egress access logs for actors created + from this template. + type: boolean + redactHeaders: + description: RedactHeaders is the list of headers that must + be redacted from audit output. + items: + type: string + type: array + traces: + description: Traces enables egress tracing for actors created + from this template. + type: boolean + type: object + defaultAction: + description: DefaultAction is applied when no allow rule matches. + enum: + - Allow + - Deny + type: string + deny: + description: Deny contains destination rules actors created from + this template may not reach. + items: + properties: + credentials: + description: |- + Credentials configures explicit egress gateway credential injection for + matching outbound requests. + properties: + inject: + description: |- + Inject configures credentials that the egress gateway injects into + matching outbound requests. Values are referenced from Kubernetes Secrets; + the policy does not contain credential material. + items: + properties: + header: + description: Header is the outbound HTTP header + name to set. + type: string + valueFrom: + description: ValueFrom selects the source of the + injected credential value. + properties: + secretKeyRef: + description: SecretKeyRef selects a key in + a Kubernetes Secret. + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - header + - valueFrom + type: object + type: array + type: object + name: + description: Name is an optional human-readable identifier + for this rule. + type: string + ports: + description: |- + Ports is the list of destination ports matched by this rule. + If empty, the rule applies to all destination ports. + items: + properties: + port: + description: Port is the destination port number. + format: int32 + type: integer + protocol: + description: Protocol is the transport protocol for + this port. + type: string + required: + - port + type: object + type: array + tls: + description: TLS defines transport security requirements + for this destination. + properties: + intercept: + description: Intercept configures explicit TLS interception + for matching egress traffic. + properties: + issuerSecretRef: + description: |- + IssuerSecretRef references the CA material used by the egress gateway to + issue certificates for intercepted TLS traffic. + properties: + name: + description: name is unique within a namespace + to reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + validateUpstream: + description: |- + ValidateUpstream controls whether the egress gateway validates the + upstream service certificate before proxying intercepted traffic. + type: boolean + type: object + mode: + description: Mode controls how TLS is handled for matching + egress traffic. + enum: + - Require + - Originate + - Intercept + - Disable + type: string + required: + description: Required controls whether matching egress + traffic must use TLS. + type: boolean + type: object + to: + description: To lists the destinations matched by this rule. + items: + properties: + host: + description: Host is the DNS name to match for egress + traffic. + type: string + ipBlock: + description: IPBlock is the IP range to match for + egress traffic. + properties: + cidr: + description: CIDR is an IP address range in CIDR + notation. + type: string + required: + - cidr + type: object + type: object + type: array + type: object + type: array + type: object pauseImage: description: |- PauseImage is the container to use as the root sandbox container. diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index d8210574b..6ce1ebfbf 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -141,29 +141,8 @@ func (s *CallAteletSuspendStep) Execute(ctx context.Context, input *SuspendInput ActorTemplateName: state.Actor.GetActorTemplateName(), ActorId: state.Actor.GetActorId(), Runsc: runscCfg, - Spec: &ateletpb.WorkloadSpec{ - PauseImage: state.ActorTemplate.Spec.PauseImage, - }, - SnapshotUriPrefix: state.Actor.GetInProgressSnapshot(), - } - for _, ctr := range state.ActorTemplate.Spec.Containers { - ateletCtr := &ateletpb.Container{ - Name: ctr.Name, - Image: ctr.Image, - Command: ctr.Command, - } - for _, env := range ctr.Env { - var val string - if env.Value != nil { - val = *env.Value - } - ateletEnv := &ateletpb.EnvEntry{ - Name: env.Name, - Value: val, - } - ateletCtr.Env = append(ateletCtr.Env, ateletEnv) - } - req.Spec.Containers = append(req.Spec.Containers, ateletCtr) + Spec: checkpointWorkloadSpecFromActorTemplate(state.ActorTemplate), + SnapshotUriPrefix: state.Actor.GetInProgressSnapshot(), } _, err = client.Checkpoint(ctx, req) if err != nil { diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index 018fb444f..969243715 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -34,7 +34,8 @@ const envSecretCacheTTL = 30 * time.Second func workloadSpecFromActorTemplate(ctx context.Context, kubeClient kubernetes.Interface, secretCache *envSecretCache, actorTemplate *atev1alpha1.ActorTemplate) (*ateletpb.WorkloadSpec, error) { workloadSpec := &ateletpb.WorkloadSpec{ - PauseImage: actorTemplate.Spec.PauseImage, + PauseImage: actorTemplate.Spec.PauseImage, + EgressPolicy: buildAteletEgressPolicy(actorTemplate.Spec.EgressPolicy), } resolver := envResolver{ kubeClient: kubeClient, @@ -63,6 +64,123 @@ func workloadSpecFromActorTemplate(ctx context.Context, kubeClient kubernetes.In return workloadSpec, nil } +func checkpointWorkloadSpecFromActorTemplate(actorTemplate *atev1alpha1.ActorTemplate) *ateletpb.WorkloadSpec { + workloadSpec := &ateletpb.WorkloadSpec{ + PauseImage: actorTemplate.Spec.PauseImage, + EgressPolicy: buildAteletEgressPolicy(actorTemplate.Spec.EgressPolicy), + } + for _, ctr := range actorTemplate.Spec.Containers { + ateletCtr := &ateletpb.Container{ + Name: ctr.Name, + Image: ctr.Image, + Command: ctr.Command, + } + for _, env := range ctr.Env { + var val string + if env.Value != nil { + val = *env.Value + } + ateletCtr.Env = append(ateletCtr.Env, &ateletpb.EnvEntry{ + Name: env.Name, + Value: val, + }) + } + workloadSpec.Containers = append(workloadSpec.Containers, ateletCtr) + } + return workloadSpec +} + +func buildAteletEgressPolicy(policy *atev1alpha1.EgressPolicy) *ateletpb.EgressPolicy { + if policy == nil { + return nil + } + return &ateletpb.EgressPolicy{ + DefaultAction: string(policy.DefaultAction), + Allow: buildAteletEgressPolicyRules(policy.Allow), + Deny: buildAteletEgressPolicyRules(policy.Deny), + Audit: buildAteletEgressAuditPolicy(policy.Audit), + } +} + +func buildAteletEgressAuditPolicy(policy *atev1alpha1.EgressAuditPolicy) *ateletpb.EgressAuditPolicy { + if policy == nil { + return nil + } + return &ateletpb.EgressAuditPolicy{ + Logs: policy.Logs, + Traces: policy.Traces, + RedactHeaders: append([]string(nil), policy.RedactHeaders...), + } +} + +func buildAteletEgressPolicyRules(rules []atev1alpha1.EgressPolicyRule) []*ateletpb.EgressPolicyRule { + out := make([]*ateletpb.EgressPolicyRule, 0, len(rules)) + for _, rule := range rules { + outRule := &ateletpb.EgressPolicyRule{} + for _, dest := range rule.To { + outDest := &ateletpb.EgressPolicyDestination{Host: dest.Host} + if dest.IPBlock != nil { + outDest.Cidr = dest.IPBlock.CIDR + } + outRule.To = append(outRule.To, outDest) + } + for _, port := range rule.Ports { + outRule.Ports = append(outRule.Ports, &ateletpb.EgressPort{ + Port: uint32(port.Port), + Protocol: string(port.Protocol), + }) + } + outRule.Tls = buildAteletEgressTLSPolicy(rule.TLS) + outRule.Credentials = buildAteletEgressCredentialPolicy(rule.Credentials) + out = append(out, outRule) + } + return out +} + +func buildAteletEgressTLSPolicy(policy *atev1alpha1.EgressTLSPolicy) *ateletpb.EgressTLSPolicy { + if policy == nil { + return nil + } + out := &ateletpb.EgressTLSPolicy{ + Mode: string(policy.Mode), + Required: policy.Required, + } + if policy.Intercept != nil { + out.Intercept = &ateletpb.EgressTLSInterceptPolicy{ + ValidateUpstream: policy.Intercept.ValidateUpstream, + } + if policy.Intercept.IssuerSecretRef != nil { + out.Intercept.IssuerSecretRef = &ateletpb.SecretReference{ + Name: policy.Intercept.IssuerSecretRef.Name, + Namespace: policy.Intercept.IssuerSecretRef.Namespace, + } + } + } + return out +} + +func buildAteletEgressCredentialPolicy(policy *atev1alpha1.EgressCredentialPolicy) *ateletpb.EgressCredentialPolicy { + if policy == nil { + return nil + } + out := &ateletpb.EgressCredentialPolicy{} + for _, injection := range policy.Inject { + outInjection := &ateletpb.EgressCredentialInjection{ + Header: injection.Header, + } + if injection.ValueFrom.SecretKeyRef != nil { + outInjection.ValueFrom = &ateletpb.EgressCredentialValueFrom{ + SecretKeyRef: &ateletpb.SecretKeySelector{ + Name: injection.ValueFrom.SecretKeyRef.Name, + Key: injection.ValueFrom.SecretKeyRef.Key, + }, + } + } + out.Inject = append(out.Inject, outInjection) + } + return out +} + type envResolver struct { kubeClient kubernetes.Interface namespace string diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index c4425bade..cfd867162 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -587,13 +587,106 @@ func (s *AteomHerder) dialAteom(ctx context.Context, targetAteomUid string) (ate // buildAteomWorkloadSpec projects the atelet-facing workload spec onto // the ateom-facing one — currently just the container names. func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec) *ateompb.WorkloadSpec { - out := &ateompb.WorkloadSpec{} + out := &ateompb.WorkloadSpec{ + EgressPolicy: buildAteomEgressPolicy(spec.GetEgressPolicy()), + } for _, ctr := range spec.GetContainers() { out.Containers = append(out.Containers, &ateompb.Container{Name: ctr.GetName()}) } return out } +func buildAteomEgressPolicy(policy *ateletpb.EgressPolicy) *ateompb.EgressPolicy { + if policy == nil { + return nil + } + return &ateompb.EgressPolicy{ + DefaultAction: policy.GetDefaultAction(), + Allow: buildAteomEgressPolicyRules(policy.GetAllow()), + Deny: buildAteomEgressPolicyRules(policy.GetDeny()), + Audit: buildAteomEgressAuditPolicy(policy.GetAudit()), + } +} + +func buildAteomEgressAuditPolicy(policy *ateletpb.EgressAuditPolicy) *ateompb.EgressAuditPolicy { + if policy == nil { + return nil + } + return &ateompb.EgressAuditPolicy{ + Logs: policy.GetLogs(), + Traces: policy.GetTraces(), + RedactHeaders: append([]string(nil), policy.GetRedactHeaders()...), + } +} + +func buildAteomEgressPolicyRules(rules []*ateletpb.EgressPolicyRule) []*ateompb.EgressPolicyRule { + out := make([]*ateompb.EgressPolicyRule, 0, len(rules)) + for _, rule := range rules { + outRule := &ateompb.EgressPolicyRule{ + Tls: buildAteomEgressTLSPolicy(rule.GetTls()), + Credentials: buildAteomEgressCredentialPolicy(rule.GetCredentials()), + } + for _, dest := range rule.GetTo() { + outRule.To = append(outRule.To, &ateompb.EgressPolicyDestination{ + Host: dest.GetHost(), + Cidr: dest.GetCidr(), + }) + } + for _, port := range rule.GetPorts() { + outRule.Ports = append(outRule.Ports, &ateompb.EgressPort{ + Port: port.GetPort(), + Protocol: port.GetProtocol(), + }) + } + out = append(out, outRule) + } + return out +} + +func buildAteomEgressTLSPolicy(policy *ateletpb.EgressTLSPolicy) *ateompb.EgressTLSPolicy { + if policy == nil { + return nil + } + out := &ateompb.EgressTLSPolicy{ + Mode: policy.GetMode(), + Required: policy.GetRequired(), + } + if policy.GetIntercept() != nil { + out.Intercept = &ateompb.EgressTLSInterceptPolicy{ + ValidateUpstream: policy.GetIntercept().GetValidateUpstream(), + } + if policy.GetIntercept().GetIssuerSecretRef() != nil { + out.Intercept.IssuerSecretRef = &ateompb.SecretReference{ + Name: policy.GetIntercept().GetIssuerSecretRef().GetName(), + Namespace: policy.GetIntercept().GetIssuerSecretRef().GetNamespace(), + } + } + } + return out +} + +func buildAteomEgressCredentialPolicy(policy *ateletpb.EgressCredentialPolicy) *ateompb.EgressCredentialPolicy { + if policy == nil { + return nil + } + out := &ateompb.EgressCredentialPolicy{} + for _, injection := range policy.GetInject() { + outInjection := &ateompb.EgressCredentialInjection{ + Header: injection.GetHeader(), + } + if injection.GetValueFrom().GetSecretKeyRef() != nil { + outInjection.ValueFrom = &ateompb.EgressCredentialValueFrom{ + SecretKeyRef: &ateompb.SecretKeySelector{ + Name: injection.GetValueFrom().GetSecretKeyRef().GetName(), + Key: injection.GetValueFrom().GetSecretKeyRef().GetKey(), + }, + } + } + out.Inject = append(out.Inject, outInjection) + } + return out +} + // uploadIfExists uploads a local file to GCS (zstd-compressed) only if // the file is present. Missing files are silently skipped — used for // optional checkpoint side-files (pages.img, pages_meta.img). diff --git a/cmd/ateom-gvisor/egress/agentgateway/agentgateway.go b/cmd/ateom-gvisor/egress/agentgateway/agentgateway.go new file mode 100644 index 000000000..6be971a59 --- /dev/null +++ b/cmd/ateom-gvisor/egress/agentgateway/agentgateway.go @@ -0,0 +1,652 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agentgateway + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/agent-substrate/substrate/cmd/ateom-gvisor/egress" + "github.com/agent-substrate/substrate/internal/ateompath" + "github.com/agent-substrate/substrate/internal/proto/ateompb" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "sigs.k8s.io/yaml" +) + +const ( + HTTPPort = 15001 + HTTPSPort = 15002 +) + +type Provider struct { + BinaryPath string + PodUID string + ReapLock *sync.RWMutex +} + +var _ egress.Provider = Provider{} + +func (p Provider) Name() string { + return "agentgateway" +} + +func (p Provider) CapturePorts() egress.CapturePorts { + return egress.CapturePorts{ + HTTP: HTTPPort, + HTTPS: HTTPSPort, + } +} + +func (p Provider) NeedsGateway(policy *ateompb.EgressPolicy) bool { + return PolicyNeedsAgentgateway(policy) +} + +func (p Provider) NewGateway(ctx context.Context) (egress.Gateway, error) { + return NewManager(ctx, p.BinaryPath, p.PodUID, p.ReapLock) +} + +type Manager struct { + lock sync.Mutex + reapLock *sync.RWMutex + binaryPath string + podUID string + configPath string + tlsDir string + secret secretResolver + cmd *exec.Cmd +} + +var _ egress.Gateway = (*Manager)(nil) + +type secretResolver interface { + SecretValue(ctx context.Context, namespace, name, key string) (string, error) + TLSSecret(ctx context.Context, namespace, name string) ([]byte, []byte, error) +} + +func NewManager(ctx context.Context, binaryPath, podUID string, reapLock *sync.RWMutex) (*Manager, error) { + manager := &Manager{ + binaryPath: binaryPath, + podUID: podUID, + reapLock: reapLock, + configPath: ateompath.EgressAgentgatewayConfigPath(podUID), + tlsDir: ateompath.EgressAgentgatewayTLSDir(podUID), + } + secret, err := newKubernetesEgressSecretResolver() + if err != nil { + slog.InfoContext(ctx, "Kubernetes secret resolver unavailable for egress agentgateway", slog.Any("err", err)) + } else { + manager.secret = secret + } + if err := manager.writeConfig(ctx, "", nil); err != nil { + return nil, err + } + return manager, nil +} + +func (m *Manager) ApplyPolicy(ctx context.Context, defaultNamespace string, policy *ateompb.EgressPolicy) error { + if m == nil { + return nil + } + m.lock.Lock() + defer m.lock.Unlock() + + if err := m.writeConfig(ctx, defaultNamespace, policy); err != nil { + return err + } + if !PolicyNeedsAgentgateway(policy) { + return m.stopLocked(ctx) + } + if m.cmd != nil && m.cmd.Process != nil { + return nil + } + + if m.reapLock != nil { + m.reapLock.RLock() + } + cmd := exec.Command(m.binaryPath, "-f", m.configPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + if m.reapLock != nil { + m.reapLock.RUnlock() + } + return fmt.Errorf("while starting egress agentgateway %q: %w", m.binaryPath, err) + } + if m.reapLock != nil { + m.reapLock.RUnlock() + } + + m.cmd = cmd + go func() { + err := cmd.Wait() + m.lock.Lock() + if m.cmd == cmd { + m.cmd = nil + } + m.lock.Unlock() + if err != nil { + slog.WarnContext(ctx, "Egress agentgateway exited", slog.Any("err", err)) + } + }() + slog.InfoContext(ctx, "Started egress agentgateway", slog.String("config", m.configPath)) + return nil +} + +func (m *Manager) Stop(ctx context.Context) { + m.lock.Lock() + defer m.lock.Unlock() + if err := m.stopLocked(ctx); err != nil { + slog.WarnContext(ctx, "Failed to stop egress agentgateway", slog.Any("err", err)) + } +} + +func (m *Manager) stopLocked(ctx context.Context) error { + if m.cmd == nil || m.cmd.Process == nil { + return nil + } + slog.InfoContext(ctx, "Stopping egress agentgateway") + if err := m.cmd.Process.Kill(); err != nil { + return fmt.Errorf("while killing egress agentgateway: %w", err) + } + m.cmd = nil + return nil +} + +func (m *Manager) writeConfig(ctx context.Context, defaultNamespace string, policy *ateompb.EgressPolicy) error { + if err := os.MkdirAll(filepath.Dir(m.configPath), 0o700); err != nil { + return fmt.Errorf("while creating egress agentgateway config directory: %w", err) + } + if err := os.MkdirAll(m.tlsDir, 0o700); err != nil { + return fmt.Errorf("while creating egress agentgateway tls directory: %w", err) + } + cfg, err := renderEgressAgentgatewayConfig(ctx, m.secret, defaultNamespace, m.tlsDir, policy) + if err != nil { + return err + } + if err := os.WriteFile(m.configPath, cfg, 0o600); err != nil { + return fmt.Errorf("while writing egress agentgateway config: %w", err) + } + return nil +} + +func PolicyNeedsAgentgateway(policy *ateompb.EgressPolicy) bool { + if policy == nil { + return false + } + for _, rule := range policy.GetAllow() { + if egressRuleHasTransparentHTTPRoute(rule) { + return true + } + if egressRuleHasTransparentTLSRoute(rule) { + return true + } + if len(rule.GetCredentials().GetInject()) > 0 { + return true + } + switch rule.GetTls().GetMode() { + case "Require", "Originate", "Intercept": + return true + } + if rule.GetTls().GetRequired() { + return true + } + } + return false +} + +func egressRuleHasTransparentTLSRoute(rule *ateompb.EgressPolicyRule) bool { + hasHost := false + for _, dest := range rule.GetTo() { + if dest.GetHost() != "" { + hasHost = true + break + } + } + if !hasHost || rule.GetTls().GetMode() == "Intercept" { + return false + } + for _, port := range effectiveEgressPorts(rule) { + if egressPortUsesTLSPassthrough(rule, port) { + return true + } + } + return false +} + +func egressRuleHasTransparentHTTPRoute(rule *ateompb.EgressPolicyRule) bool { + hasHost := false + for _, dest := range rule.GetTo() { + if dest.GetHost() != "" { + hasHost = true + break + } + } + if !hasHost || egressRuleRequiresBackendTLS(rule) { + return false + } + for _, port := range effectiveEgressPorts(rule) { + if isTCPEgressPort(port) && effectiveEgressPort(port) == 80 && !egressPortUsesTLSPassthrough(rule, port) { + return true + } + } + return false +} + +type localAgentgatewayConfig struct { + Schema string `json:"# yaml-language-server,omitempty"` + Config localAgentgatewaySettings `json:"config"` + Binds []localAgentgatewayBind `json:"binds"` +} + +type localAgentgatewaySettings struct { + AdminAddr string `json:"adminAddr"` + ReadinessAddr string `json:"readinessAddr"` + StatsAddr string `json:"statsAddr"` +} + +type localAgentgatewayBind struct { + Port int `json:"port"` + Listeners []localAgentgatewayListener `json:"listeners"` +} + +type localAgentgatewayListener struct { + Name string `json:"name"` + Protocol string `json:"protocol"` + TLS *localAgentgatewayFrontendTLS `json:"tls,omitempty"` + Routes []localAgentgatewayRoute `json:"routes,omitempty"` + TCPRoutes []localAgentgatewayTCPRoute `json:"tcpRoutes,omitempty"` +} + +type localAgentgatewayFrontendTLS struct { + Mode string `json:"mode,omitempty"` + Cert string `json:"cert"` + Key string `json:"key"` +} + +type localAgentgatewayRoute struct { + Name string `json:"name"` + Matches []localAgentgatewayMatch `json:"matches,omitempty"` + Policies *localAgentgatewayRoutePolicy `json:"policies,omitempty"` + Backends []localAgentgatewayRouteBackend `json:"backends,omitempty"` +} + +type localAgentgatewayMatch struct { + Headers []localAgentgatewayHeaderMatch `json:"headers,omitempty"` +} + +type localAgentgatewayHeaderMatch struct { + Name string `json:"name"` + Value localAgentgatewayStringMatchValue `json:"value"` +} + +type localAgentgatewayStringMatchValue struct { + Exact string `json:"exact"` +} + +type localAgentgatewayRoutePolicy struct { + RequestHeaderModifier *localAgentgatewayHeaderModifier `json:"requestHeaderModifier,omitempty"` + DirectResponse *localAgentgatewayDirectResponse `json:"directResponse,omitempty"` +} + +type localAgentgatewayHeaderModifier struct { + Set map[string]string `json:"set,omitempty"` +} + +type localAgentgatewayDirectResponse struct { + Status int `json:"status"` + Body string `json:"body"` +} + +type localAgentgatewayRouteBackend struct { + Host string `json:"host"` + Policies *localAgentgatewayBackendPolicies `json:"policies,omitempty"` +} + +type localAgentgatewayTCPRoute struct { + Name string `json:"name"` + Hostnames []string `json:"hostnames,omitempty"` + Backends []localAgentgatewayRouteBackend `json:"backends,omitempty"` +} + +type localAgentgatewayBackendPolicies struct { + BackendTLS map[string]any `json:"backendTLS,omitempty"` +} + +func renderEgressAgentgatewayConfig(ctx context.Context, secrets secretResolver, defaultNamespace, tlsDir string, policy *ateompb.EgressPolicy) ([]byte, error) { + cfg := localAgentgatewayConfig{ + Config: localAgentgatewaySettings{ + AdminAddr: "127.0.0.1:15000", + ReadinessAddr: "127.0.0.1:15021", + StatsAddr: "127.0.0.1:15020", + }, + Binds: []localAgentgatewayBind{ + { + Port: HTTPPort, + Listeners: []localAgentgatewayListener{{ + Name: "egress-http", + Protocol: "HTTP", + Routes: []localAgentgatewayRoute{denyEgressRoute()}, + }}, + }, + }, + } + + httpsListener := localAgentgatewayListener{ + Name: "egress-https", + Protocol: "TLS", + } + for _, rule := range policy.GetAllow() { + routes, err := routesForEgressRule(ctx, secrets, defaultNamespace, tlsDir, rule) + if err != nil { + return nil, err + } + for _, route := range routes { + if rule.GetTls().GetMode() == "Intercept" { + httpsListener.Protocol = "HTTPS" + if httpsListener.TLS == nil { + tls, err := frontendTLSForInterceptRule(ctx, secrets, defaultNamespace, tlsDir, rule) + if err != nil { + return nil, err + } + httpsListener.TLS = tls + } + httpsListener.Routes = append([]localAgentgatewayRoute{route}, httpsListener.Routes...) + continue + } + if egressRuleRequiresBackendTLS(rule) { + continue + } + cfg.Binds[0].Listeners[0].Routes = append([]localAgentgatewayRoute{route}, cfg.Binds[0].Listeners[0].Routes...) + } + if httpsListener.Protocol == "TLS" && egressRuleHasTransparentTLSRoute(rule) { + httpsListener.TCPRoutes = append(httpsListener.TCPRoutes, tcpRoutesForEgressRule(rule)...) + } + } + if httpsListener.TLS != nil || len(httpsListener.TCPRoutes) > 0 { + if httpsListener.TLS != nil && len(httpsListener.Routes) == 0 { + httpsListener.Routes = []localAgentgatewayRoute{denyEgressRoute()} + } + cfg.Binds = append(cfg.Binds, localAgentgatewayBind{ + Port: HTTPSPort, + Listeners: []localAgentgatewayListener{httpsListener}, + }) + } + + out, err := yaml.Marshal(cfg) + if err != nil { + return nil, fmt.Errorf("while rendering egress agentgateway config: %w", err) + } + return append([]byte("# yaml-language-server: $schema=https://agentgateway.dev/schema/config\n"), out...), nil +} + +func routesForEgressRule(ctx context.Context, secrets secretResolver, defaultNamespace, _ string, rule *ateompb.EgressPolicyRule) ([]localAgentgatewayRoute, error) { + var routes []localAgentgatewayRoute + for _, dest := range rule.GetTo() { + host := dest.GetHost() + if host == "" { + continue + } + for _, port := range effectiveEgressPorts(rule) { + if !isTCPEgressPort(port) || egressPortUsesTLSPassthrough(rule, port) { + continue + } + backend := localAgentgatewayRouteBackend{Host: fmt.Sprintf("%s:%d", host, effectiveEgressPort(port))} + if egressRuleRequiresBackendTLS(rule) { + backend.Policies = &localAgentgatewayBackendPolicies{BackendTLS: backendTLSForEgressRule(rule, host)} + } + // TODO: Extend route rendering here if the egress API grows L7 + // policy fields such as path matches, rate limits, or header + // handling. The current policy intentionally stays at + // host/port/default-action enforcement. + route := localAgentgatewayRoute{ + Name: localRouteName(host, port), + Matches: egressAuthorityMatches(host, effectiveEgressPort(port)), + Backends: []localAgentgatewayRouteBackend{backend}, + } + headers, err := egressInjectedHeaders(ctx, secrets, defaultNamespace, rule.GetCredentials()) + if err != nil { + return nil, err + } + if len(headers) > 0 { + route.Policies = &localAgentgatewayRoutePolicy{ + RequestHeaderModifier: &localAgentgatewayHeaderModifier{Set: headers}, + } + } + routes = append(routes, route) + } + } + return routes, nil +} + +func tcpRoutesForEgressRule(rule *ateompb.EgressPolicyRule) []localAgentgatewayTCPRoute { + var routes []localAgentgatewayTCPRoute + for _, dest := range rule.GetTo() { + host := dest.GetHost() + if host == "" { + continue + } + for _, port := range effectiveEgressPorts(rule) { + if !egressPortUsesTLSPassthrough(rule, port) { + continue + } + routes = append(routes, localAgentgatewayTCPRoute{ + Name: localRouteName(host, port), + Hostnames: []string{host}, + Backends: []localAgentgatewayRouteBackend{{ + Host: fmt.Sprintf("%s:%d", host, effectiveEgressPort(port)), + }}, + }) + } + } + return routes +} + +func egressAuthorityMatches(host string, port uint32) []localAgentgatewayMatch { + matches := []localAgentgatewayMatch{ + {Headers: []localAgentgatewayHeaderMatch{{ + Name: ":authority", + Value: localAgentgatewayStringMatchValue{Exact: host}, + }}}, + } + hostWithPort := fmt.Sprintf("%s:%d", host, port) + if hostWithPort != host { + matches = append(matches, localAgentgatewayMatch{Headers: []localAgentgatewayHeaderMatch{{ + Name: ":authority", + Value: localAgentgatewayStringMatchValue{Exact: hostWithPort}, + }}}) + } + return matches +} + +func egressInjectedHeaders(ctx context.Context, secrets secretResolver, defaultNamespace string, policy *ateompb.EgressCredentialPolicy) (map[string]string, error) { + if policy == nil || len(policy.GetInject()) == 0 { + return nil, nil + } + if secrets == nil { + return nil, fmt.Errorf("egress credential injection requires Kubernetes secret access") + } + headers := map[string]string{} + for _, injection := range policy.GetInject() { + header := strings.TrimSpace(injection.GetHeader()) + if header == "" { + return nil, fmt.Errorf("egress credential injection header is required") + } + ref := injection.GetValueFrom().GetSecretKeyRef() + if ref == nil { + return nil, fmt.Errorf("egress credential injection for header %q requires valueFrom.secretKeyRef", header) + } + value, err := secrets.SecretValue(ctx, defaultNamespace, ref.GetName(), ref.GetKey()) + if err != nil { + return nil, err + } + headers[header] = value + } + return headers, nil +} + +func backendTLSForEgressRule(rule *ateompb.EgressPolicyRule, host string) map[string]any { + backendTLS := map[string]any{ + "hostname": host, + } + if rule.GetTls().GetMode() == "Intercept" && !rule.GetTls().GetIntercept().GetValidateUpstream() { + backendTLS["insecure"] = true + } + return backendTLS +} + +func frontendTLSForInterceptRule(ctx context.Context, secrets secretResolver, defaultNamespace, tlsDir string, rule *ateompb.EgressPolicyRule) (*localAgentgatewayFrontendTLS, error) { + if secrets == nil { + return nil, fmt.Errorf("egress TLS intercept requires Kubernetes secret access") + } + ref := rule.GetTls().GetIntercept().GetIssuerSecretRef() + if ref == nil { + return nil, fmt.Errorf("egress TLS intercept requires tls.intercept.issuerSecretRef") + } + namespace := ref.GetNamespace() + if namespace == "" { + namespace = defaultNamespace + } + cert, key, err := secrets.TLSSecret(ctx, namespace, ref.GetName()) + if err != nil { + return nil, err + } + certPath := filepath.Join(tlsDir, "intercept-ca.crt") + keyPath := filepath.Join(tlsDir, "intercept-ca.key") + if err := os.WriteFile(certPath, cert, 0o600); err != nil { + return nil, fmt.Errorf("while writing egress intercept CA cert: %w", err) + } + if err := os.WriteFile(keyPath, key, 0o600); err != nil { + return nil, fmt.Errorf("while writing egress intercept CA key: %w", err) + } + return &localAgentgatewayFrontendTLS{Mode: "dynamicCa", Cert: certPath, Key: keyPath}, nil +} + +func denyEgressRoute() localAgentgatewayRoute { + return localAgentgatewayRoute{ + Name: "default-deny", + Policies: &localAgentgatewayRoutePolicy{ + DirectResponse: &localAgentgatewayDirectResponse{ + Status: 403, + Body: "egress denied", + }, + }, + } +} + +func egressRuleRequiresBackendTLS(rule *ateompb.EgressPolicyRule) bool { + tls := rule.GetTls() + return tls.GetRequired() || tls.GetMode() == "Require" || tls.GetMode() == "Originate" || tls.GetMode() == "Intercept" +} + +func egressPortUsesTLSPassthrough(rule *ateompb.EgressPolicyRule, port *ateompb.EgressPort) bool { + if !isTCPEgressPort(port) || effectiveEgressPort(port) != 443 { + return false + } + return rule.GetTls().GetMode() != "Intercept" +} + +func effectiveEgressPorts(rule *ateompb.EgressPolicyRule) []*ateompb.EgressPort { + ports := rule.GetPorts() + if len(ports) == 0 { + return []*ateompb.EgressPort{{Port: 443, Protocol: "TCP"}} + } + return ports +} + +func isTCPEgressPort(port *ateompb.EgressPort) bool { + return port.GetProtocol() == "" || strings.EqualFold(port.GetProtocol(), "TCP") +} + +func effectiveEgressPort(port *ateompb.EgressPort) uint32 { + if port.GetPort() == 0 { + return 443 + } + return port.GetPort() +} + +func localRouteName(host string, port *ateompb.EgressPort) string { + base := host + base = strings.NewReplacer(".", "-", "_", "-", ":", "-").Replace(base) + return fmt.Sprintf("allow-%s-%d", base, effectiveEgressPort(port)) +} + +type kubernetesEgressSecretResolver struct { + client kubernetes.Interface +} + +func newKubernetesEgressSecretResolver() (*kubernetesEgressSecretResolver, error) { + cfg, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + return &kubernetesEgressSecretResolver{client: client}, nil +} + +func (r *kubernetesEgressSecretResolver) SecretValue(ctx context.Context, defaultNamespace, name, key string) (string, error) { + if name == "" || key == "" { + return "", fmt.Errorf("egress credential secret name and key are required") + } + secret, err := r.client.CoreV1().Secrets(defaultNamespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return "", secretGetError(defaultNamespace, name, err) + } + value, ok := secret.Data[key] + if !ok { + return "", fmt.Errorf("secret %s/%s does not contain key %q", defaultNamespace, name, key) + } + return string(value), nil +} + +func (r *kubernetesEgressSecretResolver) TLSSecret(ctx context.Context, namespace, name string) ([]byte, []byte, error) { + if namespace == "" { + return nil, nil, fmt.Errorf("egress TLS intercept secret namespace is required") + } + if name == "" { + return nil, nil, fmt.Errorf("egress TLS intercept secret name is required") + } + secret, err := r.client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, nil, secretGetError(namespace, name, err) + } + cert := secret.Data[corev1.TLSCertKey] + key := secret.Data[corev1.TLSPrivateKeyKey] + if len(cert) == 0 || len(key) == 0 { + return nil, nil, fmt.Errorf("secret %s/%s must contain %q and %q for egress TLS intercept", namespace, name, corev1.TLSCertKey, corev1.TLSPrivateKeyKey) + } + return cert, key, nil +} + +func secretGetError(namespace, name string, err error) error { + if apierrors.IsForbidden(err) { + return fmt.Errorf("ateom service account cannot read secret %s/%s for egress agentgateway config: %w", namespace, name, err) + } + return fmt.Errorf("while reading secret %s/%s for egress agentgateway config: %w", namespace, name, err) +} diff --git a/cmd/ateom-gvisor/egress/agentgateway/agentgateway_test.go b/cmd/ateom-gvisor/egress/agentgateway/agentgateway_test.go new file mode 100644 index 000000000..4dcf68b57 --- /dev/null +++ b/cmd/ateom-gvisor/egress/agentgateway/agentgateway_test.go @@ -0,0 +1,259 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agentgateway + +import ( + "context" + "strings" + "testing" + + "github.com/agent-substrate/substrate/internal/proto/ateompb" +) + +func TestRenderEgressAgentgatewayConfigAddsHTTPRouteAndHeaders(t *testing.T) { + secrets := fakeEgressSecretResolver{ + values: map[string]string{ + "dev-agents/openai-token/token": "Bearer test-token", + }, + } + policy := egressPolicyForTest() + policy.Allow[0].Ports[0].Port = 80 + policy.Allow[0].Tls = nil + cfg, err := renderEgressAgentgatewayConfig(context.Background(), secrets, "dev-agents", t.TempDir(), policy) + if err != nil { + t.Fatalf("renderEgressAgentgatewayConfig() error = %v", err) + } + got := string(cfg) + for _, want := range []string{ + "port: 15001", + "name: allow-api-openai-com-80", + "name: ':authority'", + "exact: api.openai.com", + "exact: api.openai.com:80", + "host: api.openai.com:80", + "Authorization: Bearer test-token", + "directResponse:", + "status: 403", + } { + if !strings.Contains(got, want) { + t.Fatalf("rendered config missing %q:\n%s", want, got) + } + } +} + +func TestRenderEgressAgentgatewayConfigDoesNotAddTLSRulesToHTTPListener(t *testing.T) { + policy := egressPolicyForTest() + policy.Allow[0].Credentials = nil + cfg, err := renderEgressAgentgatewayConfig(context.Background(), nil, "dev-agents", t.TempDir(), policy) + if err != nil { + t.Fatalf("renderEgressAgentgatewayConfig() error = %v", err) + } + got := string(cfg) + for _, notWant := range []string{ + "backendTLS: {}", + } { + if strings.Contains(got, notWant) { + t.Fatalf("rendered HTTP listener config should not contain TLS route %q:\n%s", notWant, got) + } + } +} + +func TestRenderEgressAgentgatewayConfigAddsTLSPassthroughTCPRoute(t *testing.T) { + policy := egressPolicyForTest() + policy.Allow[0].Credentials = nil + cfg, err := renderEgressAgentgatewayConfig(context.Background(), nil, "dev-agents", t.TempDir(), policy) + if err != nil { + t.Fatalf("renderEgressAgentgatewayConfig() error = %v", err) + } + got := string(cfg) + for _, want := range []string{ + "port: 15002", + "protocol: TLS", + "tcpRoutes:", + "name: allow-api-openai-com-443", + "hostnames:", + "- api.openai.com", + "host: api.openai.com:443", + } { + if !strings.Contains(got, want) { + t.Fatalf("rendered config missing %q:\n%s", want, got) + } + } +} + +func TestRenderEgressAgentgatewayConfigDefaultsPort443ToTLSPassthrough(t *testing.T) { + policy := &ateompb.EgressPolicy{ + DefaultAction: "Deny", + Allow: []*ateompb.EgressPolicyRule{{ + To: []*ateompb.EgressPolicyDestination{ + {Host: "example.com"}, + }, + Ports: []*ateompb.EgressPort{ + {Port: 80, Protocol: "TCP"}, + {Port: 443, Protocol: "TCP"}, + }, + }}, + } + cfg, err := renderEgressAgentgatewayConfig(context.Background(), nil, "dev-agents", t.TempDir(), policy) + if err != nil { + t.Fatalf("renderEgressAgentgatewayConfig() error = %v", err) + } + got := string(cfg) + for _, want := range []string{ + "port: 15001", + "name: allow-example-com-80", + "host: example.com:80", + "port: 15002", + "protocol: TLS", + "name: allow-example-com-443", + "hostnames:", + "- example.com", + "host: example.com:443", + } { + if !strings.Contains(got, want) { + t.Fatalf("rendered config missing %q:\n%s", want, got) + } + } +} + +func TestRenderEgressAgentgatewayConfigAddsInterceptHTTPSBind(t *testing.T) { + secrets := fakeEgressSecretResolver{ + certs: map[string]fakeTLSSecret{ + "dev-agents/egress-ca": { + cert: []byte("cert"), + key: []byte("key"), + }, + }, + } + policy := egressPolicyForTest() + policy.Allow[0].Tls.Mode = "Intercept" + policy.Allow[0].Tls.Intercept = &ateompb.EgressTLSInterceptPolicy{ + IssuerSecretRef: &ateompb.SecretReference{ + Name: "egress-ca", + Namespace: "dev-agents", + }, + ValidateUpstream: true, + } + cfg, err := renderEgressAgentgatewayConfig(context.Background(), secrets, "dev-agents", t.TempDir(), policy) + if err != nil { + t.Fatalf("renderEgressAgentgatewayConfig() error = %v", err) + } + got := string(cfg) + for _, want := range []string{ + "port: 15002", + "protocol: HTTPS", + "tls:", + "mode: dynamicCa", + "cert:", + "intercept-ca.crt", + "key:", + "intercept-ca.key", + "host: api.openai.com:443", + "backendTLS:", + "hostname: api.openai.com", + } { + if !strings.Contains(got, want) { + t.Fatalf("rendered config missing %q:\n%s", want, got) + } + } +} + +func TestRenderEgressAgentgatewayConfigCanDisableInterceptUpstreamValidation(t *testing.T) { + secrets := fakeEgressSecretResolver{ + certs: map[string]fakeTLSSecret{ + "dev-agents/egress-ca": { + cert: []byte("cert"), + key: []byte("key"), + }, + }, + } + policy := egressPolicyForTest() + policy.Allow[0].Tls.Mode = "Intercept" + policy.Allow[0].Tls.Intercept = &ateompb.EgressTLSInterceptPolicy{ + IssuerSecretRef: &ateompb.SecretReference{ + Name: "egress-ca", + Namespace: "dev-agents", + }, + ValidateUpstream: false, + } + cfg, err := renderEgressAgentgatewayConfig(context.Background(), secrets, "dev-agents", t.TempDir(), policy) + if err != nil { + t.Fatalf("renderEgressAgentgatewayConfig() error = %v", err) + } + got := string(cfg) + for _, want := range []string{ + "mode: dynamicCa", + "backendTLS:", + "insecure: true", + } { + if !strings.Contains(got, want) { + t.Fatalf("rendered config missing %q:\n%s", want, got) + } + } +} + +func egressPolicyForTest() *ateompb.EgressPolicy { + return &ateompb.EgressPolicy{ + DefaultAction: "Deny", + Allow: []*ateompb.EgressPolicyRule{ + { + To: []*ateompb.EgressPolicyDestination{ + {Host: "api.openai.com"}, + }, + Ports: []*ateompb.EgressPort{ + {Port: 443, Protocol: "TCP"}, + }, + Tls: &ateompb.EgressTLSPolicy{ + Mode: "Require", + Required: true, + }, + Credentials: &ateompb.EgressCredentialPolicy{ + Inject: []*ateompb.EgressCredentialInjection{ + { + Header: "Authorization", + ValueFrom: &ateompb.EgressCredentialValueFrom{ + SecretKeyRef: &ateompb.SecretKeySelector{ + Name: "openai-token", + Key: "token", + }, + }, + }, + }, + }, + }, + }, + } +} + +type fakeEgressSecretResolver struct { + values map[string]string + certs map[string]fakeTLSSecret +} + +type fakeTLSSecret struct { + cert []byte + key []byte +} + +func (r fakeEgressSecretResolver) SecretValue(_ context.Context, namespace, name, key string) (string, error) { + return r.values[namespace+"/"+name+"/"+key], nil +} + +func (r fakeEgressSecretResolver) TLSSecret(_ context.Context, namespace, name string) ([]byte, []byte, error) { + secret := r.certs[namespace+"/"+name] + return secret.cert, secret.key, nil +} diff --git a/cmd/ateom-gvisor/egress/egress.go b/cmd/ateom-gvisor/egress/egress.go new file mode 100644 index 000000000..d66e2e120 --- /dev/null +++ b/cmd/ateom-gvisor/egress/egress.go @@ -0,0 +1,48 @@ +//go:build linux + +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package egress + +import ( + "context" + + "github.com/agent-substrate/substrate/internal/proto/ateompb" +) + +// CapturePorts identifies the local listener ports used for transparent actor +// egress capture in the worker pod network namespace. +type CapturePorts struct { + HTTP uint16 + HTTPS uint16 +} + +// Gateway is a local egress gateway process that enforces an ActorTemplate +// egress policy for the currently active actor. +type Gateway interface { + ApplyPolicy(ctx context.Context, defaultNamespace string, policy *ateompb.EgressPolicy) error + Stop(ctx context.Context) +} + +// Provider owns proxy-specific policy support decisions and gateway creation. +// Implementations can render different local proxy configs, such as +// AgentGateway or Envoy, without leaking those details into ateom service +// lifecycle and network setup code. +type Provider interface { + Name() string + CapturePorts() CapturePorts + NeedsGateway(policy *ateompb.EgressPolicy) bool + NewGateway(ctx context.Context) (Gateway, error) +} diff --git a/cmd/ateom-gvisor/main.go b/cmd/ateom-gvisor/main.go index 365447fca..9a6815186 100644 --- a/cmd/ateom-gvisor/main.go +++ b/cmd/ateom-gvisor/main.go @@ -27,6 +27,8 @@ import ( "sync" "cloud.google.com/go/compute/metadata" + "github.com/agent-substrate/substrate/cmd/ateom-gvisor/egress" + agw "github.com/agent-substrate/substrate/cmd/ateom-gvisor/egress/agentgateway" "github.com/agent-substrate/substrate/cmd/ateom-gvisor/internal/ateom" "github.com/agent-substrate/substrate/internal/ateinterceptors" "github.com/agent-substrate/substrate/internal/ateompath" @@ -49,7 +51,8 @@ import ( ) var ( - podUID = pflag.String("pod-uid", "", "The UID of the current pod") + podUID = pflag.String("pod-uid", "", "The UID of the current pod") + egressAgentgatewayBinary = pflag.String("egress-agentgateway-binary", "/app/agentgateway", "Path to the agentgateway binary used for local egress policy enforcement.") showVersion = pflag.Bool("version", false, "Print version and exit.") @@ -135,7 +138,12 @@ func do(ctx context.Context) error { } actorLogger := ateom.NewActorLogger(syncedWriter, metadata.OnGCE()) - ateomService := NewService(interiorNetNS, actorLogger) + ateomService := NewService(interiorNetNS, actorLogger, agw.Provider{ + BinaryPath: *egressAgentgatewayBinary, + PodUID: *podUID, + ReapLock: &reapLock, + }) + defer ateomService.Stop(ctx) svr := grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler()), @@ -162,19 +170,29 @@ type AteomService struct { interiorNetNS netns.NsHandle actorLogger *ateom.ActorLogger + + egressProvider egress.Provider + egressGateway egress.Gateway } var _ ateompb.AteomServer = (*AteomService)(nil) // NewService creates a new AteomService. -func NewService(interiorNetNS netns.NsHandle, actorLogger *ateom.ActorLogger) *AteomService { +func NewService(interiorNetNS netns.NsHandle, actorLogger *ateom.ActorLogger, egressProvider egress.Provider) *AteomService { svc := &AteomService{ - interiorNetNS: interiorNetNS, - actorLogger: actorLogger, + interiorNetNS: interiorNetNS, + actorLogger: actorLogger, + egressProvider: egressProvider, } return svc } +func (s *AteomService) Stop(ctx context.Context) { + if s.egressGateway != nil { + s.egressGateway.Stop(ctx) + } +} + func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkloadRequest) (resp *ateompb.RunWorkloadResponse, retErr error) { s.lock.Lock() defer s.lock.Unlock() @@ -186,7 +204,11 @@ func (s *AteomService) RunWorkload(ctx context.Context, req *ateompb.RunWorkload // * Correct runsc version is downloaded and placed on disk. // * All OCI bundles are set up, including for "pause" container. - if err := s.setupActorNetwork(ctx); err != nil { + policy := req.GetSpec().GetEgressPolicy() + if err := s.applyEgressPolicy(ctx, req.GetActorTemplateNamespace(), policy); err != nil { + return nil, err + } + if err := s.setupActorNetwork(ctx, policy); err != nil { return nil, fmt.Errorf("while setting up actor network: %w", err) } defer func() { @@ -272,6 +294,9 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec } s.cleanupActorNetworkOrExit(ctx, "Failed to clean up actor network after checkpoint") + if err := s.applyEgressPolicy(ctx, req.GetActorTemplateNamespace(), nil); err != nil { + return nil, err + } s.actorLogger.EmitLifecycleLog("Actor checkpointed", req.GetActorId(), req.GetActorTemplateName(), req.GetActorTemplateNamespace()) @@ -316,7 +341,11 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore // * All OCI bundles are set up, including for "pause" container. // * Checkpoint downloaded and placed on disk - if err := s.setupActorNetwork(ctx); err != nil { + policy := req.GetSpec().GetEgressPolicy() + if err := s.applyEgressPolicy(ctx, req.GetActorTemplateNamespace(), policy); err != nil { + return nil, err + } + if err := s.setupActorNetwork(ctx, policy); err != nil { return nil, fmt.Errorf("while setting up actor network: %w", err) } defer func() { @@ -363,7 +392,27 @@ func (s *AteomService) RestoreWorkload(ctx context.Context, req *ateompb.Restore return &ateompb.RestoreWorkloadResponse{}, nil } -func (s *AteomService) setupActorNetwork(ctx context.Context) (retErr error) { +func (s *AteomService) applyEgressPolicy(ctx context.Context, defaultNamespace string, policy *ateompb.EgressPolicy) error { + if s.egressProvider == nil { + return nil + } + if s.egressProvider.NeedsGateway(policy) && s.egressGateway == nil { + egressGateway, err := s.egressProvider.NewGateway(ctx) + if err != nil { + return fmt.Errorf("while initializing %s egress gateway: %w", s.egressProvider.Name(), err) + } + s.egressGateway = egressGateway + } + if s.egressGateway == nil { + return nil + } + if err := s.egressGateway.ApplyPolicy(ctx, defaultNamespace, policy); err != nil { + return fmt.Errorf("while configuring %s egress gateway: %w", s.egressProvider.Name(), err) + } + return nil +} + +func (s *AteomService) setupActorNetwork(ctx context.Context, egressPolicy *ateompb.EgressPolicy) (retErr error) { // Build a fresh point-to-point network between the worker pod netns and the // gVisor interior netns. The worker side keeps the pod's real eth0, creates // ateom0 as the gateway, and moves only the veth peer into the actor netns. @@ -431,7 +480,8 @@ func (s *AteomService) setupActorNetwork(ctx context.Context) (retErr error) { if err := enableIPv4Forwarding(); err != nil { return err } - if err := installActorNftablesRules(podIP); err != nil { + routeEgressToGateway := s.egressProvider != nil && s.egressProvider.NeedsGateway(egressPolicy) + if err := installActorNftablesRules(podIP, routeEgressToGateway, s.egressCapturePorts()); err != nil { return err } @@ -447,6 +497,13 @@ func (s *AteomService) setupActorNetwork(ctx context.Context) (retErr error) { return nil } +func (s *AteomService) egressCapturePorts() egress.CapturePorts { + if s.egressProvider == nil { + return egress.CapturePorts{} + } + return s.egressProvider.CapturePorts() +} + func configureActorVeth(ctx context.Context) error { // Run inside the gVisor interior netns after setupActorNetwork moves the // veth peer there. gVisor reads link names, addresses, and routes from this @@ -596,7 +653,7 @@ func enableIPv4Forwarding() error { return nil } -func installActorNftablesRules(podIP net.IP) error { +func installActorNftablesRules(podIP net.IP, routeEgressToGateway bool, capturePorts egress.CapturePorts) error { // Install a dedicated nftables table for the active actor. Keeping all // rules in an ateom-owned table makes cleanup simple and avoids mutating // Kubernetes or CNI-managed chains directly. @@ -605,7 +662,8 @@ func installActorNftablesRules(podIP net.IP) error { // networking supports dual-stack pods. The current compatibility path is // IPv4-only. // - // The temporary compatibility rules do three things: + // The temporary compatibility rules do three things when local egress + // gateway enforcement is not enabled: // // * postrouting: masquerade actor egress from 169.254.17.2 behind the worker // pod IP so replies route back to the pod. @@ -613,9 +671,16 @@ func installActorNftablesRules(podIP net.IP) error { // actor veth IP on TCP/80, preserving existing inbound behavior. // * forward: accept forwarded packets between the actor veth and pod eth0. // - // This is not the final egress policy path. The later AgentGateway phase - // should replace the broad masquerade path with transparent TCP capture and - // default-deny rules. + // When local egress gateway enforcement is enabled, actor HTTP/HTTPS egress + // is transparently redirected to the gateway's local listener ports in this + // worker netns. In that mode, the gateway opens upstream connections from the + // worker pod netns using the pod IP, so ateom does not masquerade actor + // egress. The forward chain only permits inbound actor traffic and replies. + // + // TODO: If AgentGateway exposes SO_MARK configuration for its upstream + // sockets, use that mark to make the AGW-owned OUTPUT traffic explicit in + // nftables policy instead of relying solely on local redirect plus FORWARD + // isolation. if err := removeActorNftablesRules(); err != nil { return err } @@ -634,6 +699,12 @@ func installActorNftablesRules(podIP net.IP) error { Hooknum: nftables.ChainHookPrerouting, Priority: nftables.ChainPriorityNATDest, }) + if routeEgressToGateway { + if capturePorts.HTTP == 0 || capturePorts.HTTPS == 0 { + return fmt.Errorf("local egress gateway capture ports must be set") + } + addTransparentHTTPEgressRedirect(c, table, prerouting, capturePorts) + } // TODO: Support inbound UDP DNAT for actors that expose UDP protocols such // as QUIC. // TODO: Replace the hard-coded HTTP port with the actor's configured @@ -661,35 +732,44 @@ func installActorNftablesRules(podIP net.IP) error { Exprs: preroutingExprs, }) - postrouting := c.AddChain(&nftables.Chain{ - Name: "postrouting", - Table: table, - Type: nftables.ChainTypeNAT, - Hooknum: nftables.ChainHookPostrouting, - Priority: nftables.ChainPriorityNATSource, - }) - c.AddRule(&nftables.Rule{ - Table: table, - Chain: postrouting, - Exprs: append(ipSourceEqual(actorVethIP), &expr.Masq{}), - }) - acceptPolicy := nftables.ChainPolicyAccept + forwardPolicy := &acceptPolicy + if routeEgressToGateway { + dropPolicy := nftables.ChainPolicyDrop + forwardPolicy = &dropPolicy + } else { + postrouting := c.AddChain(&nftables.Chain{ + Name: "postrouting", + Table: table, + Type: nftables.ChainTypeNAT, + Hooknum: nftables.ChainHookPostrouting, + Priority: nftables.ChainPriorityNATSource, + }) + c.AddRule(&nftables.Rule{ + Table: table, + Chain: postrouting, + Exprs: append(ipSourceEqual(actorVethIP), &expr.Masq{}), + }) + } forward := c.AddChain(&nftables.Chain{ Name: "forward", Table: table, Type: nftables.ChainTypeFilter, Hooknum: nftables.ChainHookForward, Priority: nftables.ChainPriorityFilter, - Policy: &acceptPolicy, - }) - c.AddRule(&nftables.Rule{ - Table: table, - Chain: forward, - Exprs: []expr.Any{ - &expr.Verdict{Kind: expr.VerdictAccept}, - }, + Policy: forwardPolicy, }) + if routeEgressToGateway { + addEgressGatewayForwardRules(c, table, forward) + } else { + c.AddRule(&nftables.Rule{ + Table: table, + Chain: forward, + Exprs: []expr.Any{ + &expr.Verdict{Kind: expr.VerdictAccept}, + }, + }) + } if err := c.Flush(); err != nil { return fmt.Errorf("while installing actor nftables rules: %w", err) @@ -719,6 +799,40 @@ func removeActorNftablesRules() error { return nil } +func addEgressGatewayForwardRules(c *nftables.Conn, table *nftables.Table, forward *nftables.Chain) { + c.AddRule(&nftables.Rule{ + Table: table, + Chain: forward, + Exprs: append(establishedOrRelated(), &expr.Verdict{Kind: expr.VerdictAccept}), + }) + + inboundExprs := append(ipDestinationEqual(actorVethIP), tcpDestinationPortEqual(80)...) + inboundExprs = append(inboundExprs, &expr.Verdict{Kind: expr.VerdictAccept}) + c.AddRule(&nftables.Rule{ + Table: table, + Chain: forward, + Exprs: inboundExprs, + }) +} + +func establishedOrRelated() []expr.Any { + return []expr.Any{ + &expr.Ct{Register: 1, Key: expr.CtKeySTATE}, + &expr.Bitwise{ + SourceRegister: 1, + DestRegister: 1, + Len: 4, + Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitESTABLISHED | expr.CtStateBitRELATED), + Xor: binaryutil.NativeEndian.PutUint32(0), + }, + &expr.Cmp{ + Op: expr.CmpOpNeq, + Register: 1, + Data: binaryutil.NativeEndian.PutUint32(0), + }, + } +} + func ipSourceEqual(ip string) []expr.Any { return ipPayloadEqual(12, ip) } @@ -744,12 +858,43 @@ func ipPayloadEqual(offset uint32, ip string) []expr.Any { } func tcpDestinationPortEqual(port uint16) []expr.Any { + return destinationPortEqual("TCP", port) +} + +func addTransparentHTTPEgressRedirect(c *nftables.Conn, table *nftables.Table, prerouting *nftables.Chain, capturePorts egress.CapturePorts) { + addTransparentEgressRedirect(c, table, prerouting, 80, capturePorts.HTTP) + addTransparentEgressRedirect(c, table, prerouting, 443, capturePorts.HTTPS) +} + +func addTransparentEgressRedirect(c *nftables.Conn, table *nftables.Table, prerouting *nftables.Chain, originalPort uint16, proxyPort uint16) { + exprs := append(ipSourceEqual(actorVethIP), tcpDestinationPortEqual(originalPort)...) + exprs = append(exprs, + &expr.Immediate{ + Register: 2, + Data: binaryutil.BigEndian.PutUint16(proxyPort), + }, + &expr.Redir{ + RegisterProtoMin: 2, + }, + ) + c.AddRule(&nftables.Rule{ + Table: table, + Chain: prerouting, + Exprs: exprs, + }) +} + +func destinationPortEqual(protocol string, port uint16) []expr.Any { + proto := byte(unix.IPPROTO_TCP) + if protocol == "UDP" { + proto = unix.IPPROTO_UDP + } return []expr.Any{ &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, - Data: []byte{unix.IPPROTO_TCP}, + Data: []byte{proto}, }, &expr.Payload{ DestRegister: 1, diff --git a/demos/counter/counter.go b/demos/counter/counter.go index 6b99d5db2..740d824f0 100644 --- a/demos/counter/counter.go +++ b/demos/counter/counter.go @@ -20,6 +20,8 @@ import ( "context" "crypto/rand" "crypto/sha256" + "crypto/tls" + "crypto/x509" "encoding/base64" "fmt" "io" @@ -42,6 +44,7 @@ func main() { slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) defaultMux := http.NewServeMux() + defaultMux.HandleFunc("/egress", handleEgress) defaultMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() count := atomic.AddUint64(&requestCount, 1) @@ -94,6 +97,59 @@ func writeRandomFile() error { return nil } +func handleEgress(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + targetURL := r.URL.Query().Get("url") + if targetURL == "" { + targetURL = "http://example.com/" + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.Proxy = nil + if caPEM := os.Getenv("EGRESS_CA_CERT_PEM"); caPEM != "" { + roots, err := x509.SystemCertPool() + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + if roots == nil { + roots = x509.NewCertPool() + } + if !roots.AppendCertsFromPEM([]byte(caPEM)) { + http.Error(w, "EGRESS_CA_CERT_PEM does not contain a valid PEM certificate", http.StatusBadGateway) + return + } + transport.TLSClientConfig = &tls.Config{ + RootCAs: roots, + } + } + client := &http.Client{Transport: transport} + + resp, err := client.Do(req) + if err != nil { + slog.ErrorContext(ctx, "Egress request failed", slog.String("url", targetURL), slog.Any("err", err)) + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 512)) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + + slog.InfoContext(ctx, "Egress request completed", slog.String("url", targetURL), slog.Int("status", resp.StatusCode)) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "egress url: %s\nstatus: %d\nbody_prefix:\n%s\n", targetURL, resp.StatusCode, string(body)) +} + func hashRandomFile() string { rfBytes, err := os.ReadFile("/random-content-file") if err != nil { diff --git a/docs/dev/agentgateway-egress.md b/docs/dev/agentgateway-egress.md new file mode 100644 index 000000000..bc19d1ccf --- /dev/null +++ b/docs/dev/agentgateway-egress.md @@ -0,0 +1,290 @@ +# agentgateway Egress + +This guide covers the local development path for running Substrate with +agentgateway bundled into the `ateom-gvisor` image for actor egress. + +## Setup + +1. Install Substrate with agentgateway egress bundling enabled: + + ```sh + export KO_DOCKER_REPO=localhost:5001 + + ./hack/install-ate-kind.sh --bundle-agentgateway-egress=enable --deploy-ate-system --router=agentgateway + ./hack/install-ate-kind.sh --bundle-agentgateway-egress=enable --deploy-demo-counter + ``` + +2. Install the `kubectl ate` plugin: + + ```sh + go install ./cmd/kubectl-ate + ``` + + If `kubectl ate` is still unavailable, make sure your Go bin directory is in + `PATH`, for example `export PATH="$(go env GOPATH)/bin:$PATH"`. + +The `--bundle-agentgateway-egress=enable` flag configures `ko` for this setup +run so `github.com/agent-substrate/substrate/cmd/ateom-gvisor` uses +`cr.agentgateway.dev/agentgateway:latest-dev` as its base image. Normal +installs without this flag continue to use the default `ko` image settings. + +Bundling selects agentgateway as the local egress gateway implementation by +making the `agentgateway` binary available in the `ateom-gvisor` image. +Per-actor enforcement is driven by `ActorTemplate.spec.egressPolicy`: when a +workload has an egress policy that requires local gateway handling, +`ateom-gvisor` starts and configures the bundled egress gateway for that +workload. + +The egress policy should describe traffic policy, not the proxy implementation. +For Helm-based installs, the default egress gateway implementation should be a +chart/install setting rather than an `ActorTemplate` field. + +To test a different agentgateway image while keeping the same setup path, pass +an explicit image: + +```sh +./hack/install-ate-kind.sh \ + --bundle-agentgateway-egress=enable \ + --agentgateway-egress-image=cr.agentgateway.dev/agentgateway: \ + --deploy-ate-system +``` + +## Smoke Test + +If you changed the demo counter locally, rerun +`./hack/install-ate-kind.sh --bundle-agentgateway-egress=enable +--deploy-demo-counter` before creating the actor so the WorkerPool image still +contains `agentgateway` and the actor snapshot uses the updated counter image. +Use a fresh actor name after changing the template, because existing actors keep +using their previous snapshot. + +Reapply the egress policy after every `--deploy-demo-counter` run. The demo +install reapplies the `ActorTemplate`, which clears local patches. + +Configure the demo `ActorTemplate` with an egress policy that allows +`example.com`, then denies other HTTP/HTTPS egress. DNS is allowed by default +for name resolution. + +```sh +export KO_DOCKER_REPO=localhost:5001 + +./hack/run-tool.sh ko apply -f - </tmp/example-mitm-openssl.cnf <<'EOF' +[req] +distinguished_name = dn +prompt = no +x509_extensions = v3_ca + +[dn] +CN = example-egress-ca + +[v3_ca] +basicConstraints = critical,CA:TRUE +keyUsage = critical,keyCertSign,cRLSign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +EOF + +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout /tmp/example-mitm.key \ + -out /tmp/example-mitm.crt \ + -days 7 \ + -config /tmp/example-mitm-openssl.cnf + +kubectl -n ate-demo-counter create secret tls example-mitm \ + --cert=/tmp/example-mitm.crt \ + --key=/tmp/example-mitm.key \ + --dry-run=client -o yaml | kubectl apply -f - + +kubectl -n ate-demo-counter create role ateom-egress-secret-reader \ + --verb=get \ + --resource=secrets \ + --resource-name=example-mitm \ + --dry-run=client -o yaml | kubectl apply -f - + +kubectl -n ate-demo-counter create rolebinding ateom-egress-secret-reader \ + --role=ateom-egress-secret-reader \ + --serviceaccount=ate-demo-counter:default \ + --dry-run=client -o yaml | kubectl apply -f - + +kubectl -n ate-demo-counter create rolebinding ate-api-egress-secret-reader \ + --role=ateom-egress-secret-reader \ + --serviceaccount=ate-system:ate-api-server \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +The worker pod service account needs this secret for `issuerSecretRef`. +`ate-api-server` also needs it in this example because it resolves the counter +container's `EGRESS_CA_CERT_PEM` `secretKeyRef` before resuming the actor. + +Apply the `ActorTemplate` with an intercept rule and pass the CA certificate +into the counter actor: + +```sh +export KO_DOCKER_REPO=localhost:5001 + +./hack/run-tool.sh ko apply -f - <&2 + exit 1 + ;; + esac +} + +configure_ko_for_agentgateway_egress() { + if [[ "${ATE_INSTALL_BUNDLE_AGENTGATEWAY_EGRESS}" != "true" ]]; then + return + fi + + local ko_config_path="${KO_CONFIG_PATH:-${ROOT}/.ko.yaml}" + local temp_config="" + ATE_INSTALL_KO_CONFIG_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ko-agentgateway-egress.XXXXXX") + temp_config="${ATE_INSTALL_KO_CONFIG_DIR}/.ko.yaml" + ATE_INSTALL_KO_CONFIG_PATH="${temp_config}" + + awk \ + -v key="${ATEOM_GVISOR_IMPORTPATH}" \ + -v value="${ATE_INSTALL_AGENTGATEWAY_EGRESS_IMAGE}" ' + BEGIN { + inserted = 0 + in_overrides = 0 + } + in_overrides && $0 !~ /^ / && $0 !~ /^$/ { + print " " key ": " value + inserted = 1 + in_overrides = 0 + } + { + if (in_overrides && $1 == key ":") { + print " " key ": " value + inserted = 1 + next + } + print + if ($0 == "baseImageOverrides:") { + in_overrides = 1 + } + } + END { + if (in_overrides && !inserted) { + print " " key ": " value + } else if (!in_overrides && !inserted) { + print "" + print "baseImageOverrides:" + print " " key ": " value + } + } + ' "${ko_config_path}" >"${temp_config}" + + export KO_CONFIG_PATH="${temp_config}" +} + atenet_router_manifest() { case "${ATE_INSTALL_ATENET_ROUTER}" in agentgateway) @@ -403,7 +477,9 @@ if [ "$#" -eq 0 ]; then fi # If -h or --help appears anywhere in the command line, print the usage and exit. -for arg in "$@"; do +preparse_args=("$@") +while [[ "${#preparse_args[@]}" -gt 0 ]]; do + arg="${preparse_args[0]}" case "$arg" in -h|--help) usage @@ -412,9 +488,26 @@ for arg in "$@"; do --router=*) set_atenet_router "${arg#--router=}" ;; + --bundle-agentgateway-egress) + if [[ "${#preparse_args[@]}" -lt 2 ]]; then + echo "Error: --bundle-agentgateway-egress requires a value (enable)" >&2 + exit 1 + fi + set_bundle_agentgateway_egress "${preparse_args[1]}" + preparse_args=("${preparse_args[@]:1}") + ;; + --bundle-agentgateway-egress=*) + set_bundle_agentgateway_egress "${arg#--bundle-agentgateway-egress=}" + ;; + --agentgateway-egress-image=*) + ATE_INSTALL_AGENTGATEWAY_EGRESS_IMAGE="${arg#--agentgateway-egress-image=}" + ;; esac + preparse_args=("${preparse_args[@]:1}") done +configure_ko_for_agentgateway_egress + while [[ "$#" -gt 0 ]]; do # Run ${demo}_cmdline if it exists. If it returns 0, then we successfully # handled this argument and can continue. Otherwise, fallthrough to check @@ -431,6 +524,16 @@ while [[ "$#" -gt 0 ]]; do case $1 in --deploy-ate-system) deploy_ate_system ;; --router=*) ;; + --bundle-agentgateway-egress) + if [[ "$#" -lt 2 ]]; then + echo "Error: --bundle-agentgateway-egress requires a value (enable)" >&2 + exit 1 + fi + set_bundle_agentgateway_egress "$2" + shift + ;; + --bundle-agentgateway-egress=*) ;; + --agentgateway-egress-image=*) ;; --delete-ate-system) delete_ate_system ;; --delete-all) delete_all ;; diff --git a/internal/ateompath/ateompath.go b/internal/ateompath/ateompath.go index 304d53f5f..00ed0c710 100644 --- a/internal/ateompath/ateompath.go +++ b/internal/ateompath/ateompath.go @@ -42,6 +42,21 @@ func AteomPath(podUID string) string { ) } +func EgressAgentgatewayDir(podUID string) string { + return filepath.Join( + AteomPath(podUID), + "egress-agentgateway", + ) +} + +func EgressAgentgatewayConfigPath(podUID string) string { + return filepath.Join(EgressAgentgatewayDir(podUID), "config.yaml") +} + +func EgressAgentgatewayTLSDir(podUID string) string { + return filepath.Join(EgressAgentgatewayDir(podUID), "tls") +} + func AteomSocketPath(podUID string) string { return filepath.Join( AteomPath(podUID), diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index e3a55dd50..6da81c5dd 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -328,6 +328,7 @@ type WorkloadSpec struct { state protoimpl.MessageState `protogen:"open.v1"` Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` PauseImage string `protobuf:"bytes,2,opt,name=pause_image,json=pauseImage,proto3" json:"pause_image,omitempty"` + EgressPolicy *EgressPolicy `protobuf:"bytes,3,opt,name=egress_policy,json=egressPolicy,proto3" json:"egress_policy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -376,6 +377,13 @@ func (x *WorkloadSpec) GetPauseImage() string { return "" } +func (x *WorkloadSpec) GetEgressPolicy() *EgressPolicy { + if x != nil { + return x.EgressPolicy + } + return nil +} + type Container struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -496,6 +504,662 @@ func (x *EnvEntry) GetValue() string { return "" } +type EgressPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + DefaultAction string `protobuf:"bytes,1,opt,name=default_action,json=defaultAction,proto3" json:"default_action,omitempty"` + Allow []*EgressPolicyRule `protobuf:"bytes,2,rep,name=allow,proto3" json:"allow,omitempty"` + Deny []*EgressPolicyRule `protobuf:"bytes,3,rep,name=deny,proto3" json:"deny,omitempty"` + Audit *EgressAuditPolicy `protobuf:"bytes,4,opt,name=audit,proto3" json:"audit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPolicy) Reset() { + *x = EgressPolicy{} + mi := &file_atelet_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPolicy) ProtoMessage() {} + +func (x *EgressPolicy) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPolicy.ProtoReflect.Descriptor instead. +func (*EgressPolicy) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{8} +} + +func (x *EgressPolicy) GetDefaultAction() string { + if x != nil { + return x.DefaultAction + } + return "" +} + +func (x *EgressPolicy) GetAllow() []*EgressPolicyRule { + if x != nil { + return x.Allow + } + return nil +} + +func (x *EgressPolicy) GetDeny() []*EgressPolicyRule { + if x != nil { + return x.Deny + } + return nil +} + +func (x *EgressPolicy) GetAudit() *EgressAuditPolicy { + if x != nil { + return x.Audit + } + return nil +} + +type EgressPolicyRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + To []*EgressPolicyDestination `protobuf:"bytes,1,rep,name=to,proto3" json:"to,omitempty"` + Ports []*EgressPort `protobuf:"bytes,2,rep,name=ports,proto3" json:"ports,omitempty"` + Tls *EgressTLSPolicy `protobuf:"bytes,3,opt,name=tls,proto3" json:"tls,omitempty"` + Credentials *EgressCredentialPolicy `protobuf:"bytes,4,opt,name=credentials,proto3" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPolicyRule) Reset() { + *x = EgressPolicyRule{} + mi := &file_atelet_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPolicyRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPolicyRule) ProtoMessage() {} + +func (x *EgressPolicyRule) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPolicyRule.ProtoReflect.Descriptor instead. +func (*EgressPolicyRule) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{9} +} + +func (x *EgressPolicyRule) GetTo() []*EgressPolicyDestination { + if x != nil { + return x.To + } + return nil +} + +func (x *EgressPolicyRule) GetPorts() []*EgressPort { + if x != nil { + return x.Ports + } + return nil +} + +func (x *EgressPolicyRule) GetTls() *EgressTLSPolicy { + if x != nil { + return x.Tls + } + return nil +} + +func (x *EgressPolicyRule) GetCredentials() *EgressCredentialPolicy { + if x != nil { + return x.Credentials + } + return nil +} + +type EgressPolicyDestination struct { + state protoimpl.MessageState `protogen:"open.v1"` + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Cidr string `protobuf:"bytes,2,opt,name=cidr,proto3" json:"cidr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPolicyDestination) Reset() { + *x = EgressPolicyDestination{} + mi := &file_atelet_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPolicyDestination) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPolicyDestination) ProtoMessage() {} + +func (x *EgressPolicyDestination) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPolicyDestination.ProtoReflect.Descriptor instead. +func (*EgressPolicyDestination) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{10} +} + +func (x *EgressPolicyDestination) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *EgressPolicyDestination) GetCidr() string { + if x != nil { + return x.Cidr + } + return "" +} + +type EgressPort struct { + state protoimpl.MessageState `protogen:"open.v1"` + Port uint32 `protobuf:"varint,1,opt,name=port,proto3" json:"port,omitempty"` + Protocol string `protobuf:"bytes,2,opt,name=protocol,proto3" json:"protocol,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPort) Reset() { + *x = EgressPort{} + mi := &file_atelet_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPort) ProtoMessage() {} + +func (x *EgressPort) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPort.ProtoReflect.Descriptor instead. +func (*EgressPort) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{11} +} + +func (x *EgressPort) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *EgressPort) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +type EgressTLSPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mode string `protobuf:"bytes,1,opt,name=mode,proto3" json:"mode,omitempty"` + Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty"` + Intercept *EgressTLSInterceptPolicy `protobuf:"bytes,3,opt,name=intercept,proto3" json:"intercept,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressTLSPolicy) Reset() { + *x = EgressTLSPolicy{} + mi := &file_atelet_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressTLSPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressTLSPolicy) ProtoMessage() {} + +func (x *EgressTLSPolicy) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressTLSPolicy.ProtoReflect.Descriptor instead. +func (*EgressTLSPolicy) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{12} +} + +func (x *EgressTLSPolicy) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *EgressTLSPolicy) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *EgressTLSPolicy) GetIntercept() *EgressTLSInterceptPolicy { + if x != nil { + return x.Intercept + } + return nil +} + +type EgressTLSInterceptPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + IssuerSecretRef *SecretReference `protobuf:"bytes,1,opt,name=issuer_secret_ref,json=issuerSecretRef,proto3" json:"issuer_secret_ref,omitempty"` + ValidateUpstream bool `protobuf:"varint,2,opt,name=validate_upstream,json=validateUpstream,proto3" json:"validate_upstream,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressTLSInterceptPolicy) Reset() { + *x = EgressTLSInterceptPolicy{} + mi := &file_atelet_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressTLSInterceptPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressTLSInterceptPolicy) ProtoMessage() {} + +func (x *EgressTLSInterceptPolicy) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressTLSInterceptPolicy.ProtoReflect.Descriptor instead. +func (*EgressTLSInterceptPolicy) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{13} +} + +func (x *EgressTLSInterceptPolicy) GetIssuerSecretRef() *SecretReference { + if x != nil { + return x.IssuerSecretRef + } + return nil +} + +func (x *EgressTLSInterceptPolicy) GetValidateUpstream() bool { + if x != nil { + return x.ValidateUpstream + } + return false +} + +type EgressCredentialPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Inject []*EgressCredentialInjection `protobuf:"bytes,1,rep,name=inject,proto3" json:"inject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressCredentialPolicy) Reset() { + *x = EgressCredentialPolicy{} + mi := &file_atelet_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressCredentialPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressCredentialPolicy) ProtoMessage() {} + +func (x *EgressCredentialPolicy) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressCredentialPolicy.ProtoReflect.Descriptor instead. +func (*EgressCredentialPolicy) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{14} +} + +func (x *EgressCredentialPolicy) GetInject() []*EgressCredentialInjection { + if x != nil { + return x.Inject + } + return nil +} + +type EgressCredentialInjection struct { + state protoimpl.MessageState `protogen:"open.v1"` + Header string `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` + ValueFrom *EgressCredentialValueFrom `protobuf:"bytes,2,opt,name=value_from,json=valueFrom,proto3" json:"value_from,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressCredentialInjection) Reset() { + *x = EgressCredentialInjection{} + mi := &file_atelet_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressCredentialInjection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressCredentialInjection) ProtoMessage() {} + +func (x *EgressCredentialInjection) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressCredentialInjection.ProtoReflect.Descriptor instead. +func (*EgressCredentialInjection) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{15} +} + +func (x *EgressCredentialInjection) GetHeader() string { + if x != nil { + return x.Header + } + return "" +} + +func (x *EgressCredentialInjection) GetValueFrom() *EgressCredentialValueFrom { + if x != nil { + return x.ValueFrom + } + return nil +} + +type EgressCredentialValueFrom struct { + state protoimpl.MessageState `protogen:"open.v1"` + SecretKeyRef *SecretKeySelector `protobuf:"bytes,1,opt,name=secret_key_ref,json=secretKeyRef,proto3" json:"secret_key_ref,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressCredentialValueFrom) Reset() { + *x = EgressCredentialValueFrom{} + mi := &file_atelet_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressCredentialValueFrom) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressCredentialValueFrom) ProtoMessage() {} + +func (x *EgressCredentialValueFrom) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressCredentialValueFrom.ProtoReflect.Descriptor instead. +func (*EgressCredentialValueFrom) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{16} +} + +func (x *EgressCredentialValueFrom) GetSecretKeyRef() *SecretKeySelector { + if x != nil { + return x.SecretKeyRef + } + return nil +} + +type EgressAuditPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Logs bool `protobuf:"varint,1,opt,name=logs,proto3" json:"logs,omitempty"` + Traces bool `protobuf:"varint,2,opt,name=traces,proto3" json:"traces,omitempty"` + RedactHeaders []string `protobuf:"bytes,3,rep,name=redact_headers,json=redactHeaders,proto3" json:"redact_headers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressAuditPolicy) Reset() { + *x = EgressAuditPolicy{} + mi := &file_atelet_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressAuditPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressAuditPolicy) ProtoMessage() {} + +func (x *EgressAuditPolicy) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressAuditPolicy.ProtoReflect.Descriptor instead. +func (*EgressAuditPolicy) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{17} +} + +func (x *EgressAuditPolicy) GetLogs() bool { + if x != nil { + return x.Logs + } + return false +} + +func (x *EgressAuditPolicy) GetTraces() bool { + if x != nil { + return x.Traces + } + return false +} + +func (x *EgressAuditPolicy) GetRedactHeaders() []string { + if x != nil { + return x.RedactHeaders + } + return nil +} + +type SecretReference struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecretReference) Reset() { + *x = SecretReference{} + mi := &file_atelet_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecretReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretReference) ProtoMessage() {} + +func (x *SecretReference) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretReference.ProtoReflect.Descriptor instead. +func (*SecretReference) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{18} +} + +func (x *SecretReference) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SecretReference) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +type SecretKeySelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecretKeySelector) Reset() { + *x = SecretKeySelector{} + mi := &file_atelet_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecretKeySelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretKeySelector) ProtoMessage() {} + +func (x *SecretKeySelector) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretKeySelector.ProtoReflect.Descriptor instead. +func (*SecretKeySelector) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{19} +} + +func (x *SecretKeySelector) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SecretKeySelector) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + type RunResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -504,7 +1168,7 @@ type RunResponse struct { func (x *RunResponse) Reset() { *x = RunResponse{} - mi := &file_atelet_proto_msgTypes[8] + mi := &file_atelet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -516,7 +1180,7 @@ func (x *RunResponse) String() string { func (*RunResponse) ProtoMessage() {} func (x *RunResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[8] + mi := &file_atelet_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -529,7 +1193,7 @@ func (x *RunResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunResponse.ProtoReflect.Descriptor instead. func (*RunResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{8} + return file_atelet_proto_rawDescGZIP(), []int{20} } type CheckpointRequest struct { @@ -557,7 +1221,7 @@ type CheckpointRequest struct { func (x *CheckpointRequest) Reset() { *x = CheckpointRequest{} - mi := &file_atelet_proto_msgTypes[9] + mi := &file_atelet_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -569,7 +1233,7 @@ func (x *CheckpointRequest) String() string { func (*CheckpointRequest) ProtoMessage() {} func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[9] + mi := &file_atelet_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -582,7 +1246,7 @@ func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{9} + return file_atelet_proto_rawDescGZIP(), []int{21} } func (x *CheckpointRequest) GetTargetAteomUid() string { @@ -642,7 +1306,7 @@ type CheckpointResponse struct { func (x *CheckpointResponse) Reset() { *x = CheckpointResponse{} - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -654,7 +1318,7 @@ func (x *CheckpointResponse) String() string { func (*CheckpointResponse) ProtoMessage() {} func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[10] + mi := &file_atelet_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -667,7 +1331,7 @@ func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{10} + return file_atelet_proto_rawDescGZIP(), []int{22} } type RestoreRequest struct { @@ -686,7 +1350,7 @@ type RestoreRequest struct { func (x *RestoreRequest) Reset() { *x = RestoreRequest{} - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -698,7 +1362,7 @@ func (x *RestoreRequest) String() string { func (*RestoreRequest) ProtoMessage() {} func (x *RestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[11] + mi := &file_atelet_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -711,7 +1375,7 @@ func (x *RestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreRequest.ProtoReflect.Descriptor instead. func (*RestoreRequest) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{11} + return file_atelet_proto_rawDescGZIP(), []int{23} } func (x *RestoreRequest) GetTargetAteomUid() string { @@ -771,7 +1435,7 @@ type RestoreResponse struct { func (x *RestoreResponse) Reset() { *x = RestoreResponse{} - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -783,7 +1447,7 @@ func (x *RestoreResponse) String() string { func (*RestoreResponse) ProtoMessage() {} func (x *RestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_atelet_proto_msgTypes[12] + mi := &file_atelet_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -796,7 +1460,7 @@ func (x *RestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreResponse.ProtoReflect.Descriptor instead. func (*RestoreResponse) Descriptor() ([]byte, []int) { - return file_atelet_proto_rawDescGZIP(), []int{12} + return file_atelet_proto_rawDescGZIP(), []int{24} } var File_atelet_proto protoreflect.FileDescriptor @@ -823,13 +1487,14 @@ const file_atelet_proto_rawDesc = "" + "\vRunscConfig\x121\n" + "\x05amd64\x18\x01 \x01(\v2\x1b.atelet.RunscPlatformConfigR\x05amd64\x121\n" + "\x05arm64\x18\x02 \x01(\v2\x1b.atelet.RunscPlatformConfigR\x05arm64\x12D\n" + - "\x0eauthentication\x18\x03 \x01(\v2\x1c.atelet.AuthenticationConfigR\x0eauthentication\"b\n" + + "\x0eauthentication\x18\x03 \x01(\v2\x1c.atelet.AuthenticationConfigR\x0eauthentication\"\x9d\x01\n" + "\fWorkloadSpec\x121\n" + "\n" + "containers\x18\x01 \x03(\v2\x11.atelet.ContainerR\n" + "containers\x12\x1f\n" + "\vpause_image\x18\x02 \x01(\tR\n" + - "pauseImage\"s\n" + + "pauseImage\x129\n" + + "\regress_policy\x18\x03 \x01(\v2\x14.atelet.EgressPolicyR\fegressPolicy\"s\n" + "\tContainer\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05image\x18\x02 \x01(\tR\x05image\x12\x18\n" + @@ -837,7 +1502,49 @@ const file_atelet_proto_rawDesc = "" + "\x03env\x18\x04 \x03(\v2\x10.atelet.EnvEntryR\x03env\"4\n" + "\bEnvEntry\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value\"\r\n" + + "\x05value\x18\x02 \x01(\tR\x05value\"\xc4\x01\n" + + "\fEgressPolicy\x12%\n" + + "\x0edefault_action\x18\x01 \x01(\tR\rdefaultAction\x12.\n" + + "\x05allow\x18\x02 \x03(\v2\x18.atelet.EgressPolicyRuleR\x05allow\x12,\n" + + "\x04deny\x18\x03 \x03(\v2\x18.atelet.EgressPolicyRuleR\x04deny\x12/\n" + + "\x05audit\x18\x04 \x01(\v2\x19.atelet.EgressAuditPolicyR\x05audit\"\xda\x01\n" + + "\x10EgressPolicyRule\x12/\n" + + "\x02to\x18\x01 \x03(\v2\x1f.atelet.EgressPolicyDestinationR\x02to\x12(\n" + + "\x05ports\x18\x02 \x03(\v2\x12.atelet.EgressPortR\x05ports\x12)\n" + + "\x03tls\x18\x03 \x01(\v2\x17.atelet.EgressTLSPolicyR\x03tls\x12@\n" + + "\vcredentials\x18\x04 \x01(\v2\x1e.atelet.EgressCredentialPolicyR\vcredentials\"A\n" + + "\x17EgressPolicyDestination\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04cidr\x18\x02 \x01(\tR\x04cidr\"<\n" + + "\n" + + "EgressPort\x12\x12\n" + + "\x04port\x18\x01 \x01(\rR\x04port\x12\x1a\n" + + "\bprotocol\x18\x02 \x01(\tR\bprotocol\"\x81\x01\n" + + "\x0fEgressTLSPolicy\x12\x12\n" + + "\x04mode\x18\x01 \x01(\tR\x04mode\x12\x1a\n" + + "\brequired\x18\x02 \x01(\bR\brequired\x12>\n" + + "\tintercept\x18\x03 \x01(\v2 .atelet.EgressTLSInterceptPolicyR\tintercept\"\x8c\x01\n" + + "\x18EgressTLSInterceptPolicy\x12C\n" + + "\x11issuer_secret_ref\x18\x01 \x01(\v2\x17.atelet.SecretReferenceR\x0fissuerSecretRef\x12+\n" + + "\x11validate_upstream\x18\x02 \x01(\bR\x10validateUpstream\"S\n" + + "\x16EgressCredentialPolicy\x129\n" + + "\x06inject\x18\x01 \x03(\v2!.atelet.EgressCredentialInjectionR\x06inject\"u\n" + + "\x19EgressCredentialInjection\x12\x16\n" + + "\x06header\x18\x01 \x01(\tR\x06header\x12@\n" + + "\n" + + "value_from\x18\x02 \x01(\v2!.atelet.EgressCredentialValueFromR\tvalueFrom\"\\\n" + + "\x19EgressCredentialValueFrom\x12?\n" + + "\x0esecret_key_ref\x18\x01 \x01(\v2\x19.atelet.SecretKeySelectorR\fsecretKeyRef\"f\n" + + "\x11EgressAuditPolicy\x12\x12\n" + + "\x04logs\x18\x01 \x01(\bR\x04logs\x12\x16\n" + + "\x06traces\x18\x02 \x01(\bR\x06traces\x12%\n" + + "\x0eredact_headers\x18\x03 \x03(\tR\rredactHeaders\"C\n" + + "\x0fSecretReference\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + + "\tnamespace\x18\x02 \x01(\tR\tnamespace\"9\n" + + "\x11SecretKeySelector\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\"\r\n" + "\vRunResponse\"\xc7\x02\n" + "\x11CheckpointRequest\x12(\n" + "\x10target_ateom_uid\x18\x01 \x01(\tR\x0etargetAteomUid\x128\n" + @@ -875,21 +1582,33 @@ func file_atelet_proto_rawDescGZIP() []byte { return file_atelet_proto_rawDescData } -var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_atelet_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_atelet_proto_goTypes = []any{ - (*RunRequest)(nil), // 0: atelet.RunRequest - (*GCPAuthenticationConfig)(nil), // 1: atelet.GCPAuthenticationConfig - (*AuthenticationConfig)(nil), // 2: atelet.AuthenticationConfig - (*RunscPlatformConfig)(nil), // 3: atelet.RunscPlatformConfig - (*RunscConfig)(nil), // 4: atelet.RunscConfig - (*WorkloadSpec)(nil), // 5: atelet.WorkloadSpec - (*Container)(nil), // 6: atelet.Container - (*EnvEntry)(nil), // 7: atelet.EnvEntry - (*RunResponse)(nil), // 8: atelet.RunResponse - (*CheckpointRequest)(nil), // 9: atelet.CheckpointRequest - (*CheckpointResponse)(nil), // 10: atelet.CheckpointResponse - (*RestoreRequest)(nil), // 11: atelet.RestoreRequest - (*RestoreResponse)(nil), // 12: atelet.RestoreResponse + (*RunRequest)(nil), // 0: atelet.RunRequest + (*GCPAuthenticationConfig)(nil), // 1: atelet.GCPAuthenticationConfig + (*AuthenticationConfig)(nil), // 2: atelet.AuthenticationConfig + (*RunscPlatformConfig)(nil), // 3: atelet.RunscPlatformConfig + (*RunscConfig)(nil), // 4: atelet.RunscConfig + (*WorkloadSpec)(nil), // 5: atelet.WorkloadSpec + (*Container)(nil), // 6: atelet.Container + (*EnvEntry)(nil), // 7: atelet.EnvEntry + (*EgressPolicy)(nil), // 8: atelet.EgressPolicy + (*EgressPolicyRule)(nil), // 9: atelet.EgressPolicyRule + (*EgressPolicyDestination)(nil), // 10: atelet.EgressPolicyDestination + (*EgressPort)(nil), // 11: atelet.EgressPort + (*EgressTLSPolicy)(nil), // 12: atelet.EgressTLSPolicy + (*EgressTLSInterceptPolicy)(nil), // 13: atelet.EgressTLSInterceptPolicy + (*EgressCredentialPolicy)(nil), // 14: atelet.EgressCredentialPolicy + (*EgressCredentialInjection)(nil), // 15: atelet.EgressCredentialInjection + (*EgressCredentialValueFrom)(nil), // 16: atelet.EgressCredentialValueFrom + (*EgressAuditPolicy)(nil), // 17: atelet.EgressAuditPolicy + (*SecretReference)(nil), // 18: atelet.SecretReference + (*SecretKeySelector)(nil), // 19: atelet.SecretKeySelector + (*RunResponse)(nil), // 20: atelet.RunResponse + (*CheckpointRequest)(nil), // 21: atelet.CheckpointRequest + (*CheckpointResponse)(nil), // 22: atelet.CheckpointResponse + (*RestoreRequest)(nil), // 23: atelet.RestoreRequest + (*RestoreResponse)(nil), // 24: atelet.RestoreResponse } var file_atelet_proto_depIdxs = []int32{ 4, // 0: atelet.RunRequest.runsc:type_name -> atelet.RunscConfig @@ -899,22 +1618,35 @@ var file_atelet_proto_depIdxs = []int32{ 3, // 4: atelet.RunscConfig.arm64:type_name -> atelet.RunscPlatformConfig 2, // 5: atelet.RunscConfig.authentication:type_name -> atelet.AuthenticationConfig 6, // 6: atelet.WorkloadSpec.containers:type_name -> atelet.Container - 7, // 7: atelet.Container.env:type_name -> atelet.EnvEntry - 4, // 8: atelet.CheckpointRequest.runsc:type_name -> atelet.RunscConfig - 5, // 9: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec - 4, // 10: atelet.RestoreRequest.runsc:type_name -> atelet.RunscConfig - 5, // 11: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec - 0, // 12: atelet.AteomHerder.Run:input_type -> atelet.RunRequest - 9, // 13: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest - 11, // 14: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest - 8, // 15: atelet.AteomHerder.Run:output_type -> atelet.RunResponse - 10, // 16: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse - 12, // 17: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse - 15, // [15:18] is the sub-list for method output_type - 12, // [12:15] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 8, // 7: atelet.WorkloadSpec.egress_policy:type_name -> atelet.EgressPolicy + 7, // 8: atelet.Container.env:type_name -> atelet.EnvEntry + 9, // 9: atelet.EgressPolicy.allow:type_name -> atelet.EgressPolicyRule + 9, // 10: atelet.EgressPolicy.deny:type_name -> atelet.EgressPolicyRule + 17, // 11: atelet.EgressPolicy.audit:type_name -> atelet.EgressAuditPolicy + 10, // 12: atelet.EgressPolicyRule.to:type_name -> atelet.EgressPolicyDestination + 11, // 13: atelet.EgressPolicyRule.ports:type_name -> atelet.EgressPort + 12, // 14: atelet.EgressPolicyRule.tls:type_name -> atelet.EgressTLSPolicy + 14, // 15: atelet.EgressPolicyRule.credentials:type_name -> atelet.EgressCredentialPolicy + 13, // 16: atelet.EgressTLSPolicy.intercept:type_name -> atelet.EgressTLSInterceptPolicy + 18, // 17: atelet.EgressTLSInterceptPolicy.issuer_secret_ref:type_name -> atelet.SecretReference + 15, // 18: atelet.EgressCredentialPolicy.inject:type_name -> atelet.EgressCredentialInjection + 16, // 19: atelet.EgressCredentialInjection.value_from:type_name -> atelet.EgressCredentialValueFrom + 19, // 20: atelet.EgressCredentialValueFrom.secret_key_ref:type_name -> atelet.SecretKeySelector + 4, // 21: atelet.CheckpointRequest.runsc:type_name -> atelet.RunscConfig + 5, // 22: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec + 4, // 23: atelet.RestoreRequest.runsc:type_name -> atelet.RunscConfig + 5, // 24: atelet.RestoreRequest.spec:type_name -> atelet.WorkloadSpec + 0, // 25: atelet.AteomHerder.Run:input_type -> atelet.RunRequest + 21, // 26: atelet.AteomHerder.Checkpoint:input_type -> atelet.CheckpointRequest + 23, // 27: atelet.AteomHerder.Restore:input_type -> atelet.RestoreRequest + 20, // 28: atelet.AteomHerder.Run:output_type -> atelet.RunResponse + 22, // 29: atelet.AteomHerder.Checkpoint:output_type -> atelet.CheckpointResponse + 24, // 30: atelet.AteomHerder.Restore:output_type -> atelet.RestoreResponse + 28, // [28:31] is the sub-list for method output_type + 25, // [25:28] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_atelet_proto_init() } @@ -928,7 +1660,7 @@ func file_atelet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_atelet_proto_rawDesc), len(file_atelet_proto_rawDesc)), NumEnums: 0, - NumMessages: 13, + NumMessages: 25, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 792cffc98..8d21f96d6 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -71,8 +71,9 @@ message RunscConfig { // WorkloadSpec parallels Pod, but with far fewer configurable fields. message WorkloadSpec { - repeated Container containers = 1; - string pause_image = 2; + repeated Container containers = 1; + string pause_image = 2; + EgressPolicy egress_policy = 3; } message Container { @@ -87,6 +88,70 @@ message EnvEntry { string value = 2; } +message EgressPolicy { + string default_action = 1; + repeated EgressPolicyRule allow = 2; + repeated EgressPolicyRule deny = 3; + EgressAuditPolicy audit = 4; +} + +message EgressPolicyRule { + repeated EgressPolicyDestination to = 1; + repeated EgressPort ports = 2; + EgressTLSPolicy tls = 3; + EgressCredentialPolicy credentials = 4; +} + +message EgressPolicyDestination { + string host = 1; + string cidr = 2; +} + +message EgressPort { + uint32 port = 1; + string protocol = 2; +} + +message EgressTLSPolicy { + string mode = 1; + bool required = 2; + EgressTLSInterceptPolicy intercept = 3; +} + +message EgressTLSInterceptPolicy { + SecretReference issuer_secret_ref = 1; + bool validate_upstream = 2; +} + +message EgressCredentialPolicy { + repeated EgressCredentialInjection inject = 1; +} + +message EgressCredentialInjection { + string header = 1; + EgressCredentialValueFrom value_from = 2; +} + +message EgressCredentialValueFrom { + SecretKeySelector secret_key_ref = 1; +} + +message EgressAuditPolicy { + bool logs = 1; + bool traces = 2; + repeated string redact_headers = 3; +} + +message SecretReference { + string name = 1; + string namespace = 2; +} + +message SecretKeySelector { + string name = 1; + string key = 2; +} + message RunResponse { } diff --git a/internal/proto/ateompb/ateom.pb.go b/internal/proto/ateompb/ateom.pb.go index 9114a309b..884a5342b 100644 --- a/internal/proto/ateompb/ateom.pb.go +++ b/internal/proto/ateompb/ateom.pb.go @@ -115,6 +115,7 @@ func (x *RunWorkloadRequest) GetSpec() *WorkloadSpec { type WorkloadSpec struct { state protoimpl.MessageState `protogen:"open.v1"` Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` + EgressPolicy *EgressPolicy `protobuf:"bytes,2,opt,name=egress_policy,json=egressPolicy,proto3" json:"egress_policy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -156,6 +157,13 @@ func (x *WorkloadSpec) GetContainers() []*Container { return nil } +func (x *WorkloadSpec) GetEgressPolicy() *EgressPolicy { + if x != nil { + return x.EgressPolicy + } + return nil +} + type Container struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -200,6 +208,662 @@ func (x *Container) GetName() string { return "" } +type EgressPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + DefaultAction string `protobuf:"bytes,1,opt,name=default_action,json=defaultAction,proto3" json:"default_action,omitempty"` + Allow []*EgressPolicyRule `protobuf:"bytes,2,rep,name=allow,proto3" json:"allow,omitempty"` + Deny []*EgressPolicyRule `protobuf:"bytes,3,rep,name=deny,proto3" json:"deny,omitempty"` + Audit *EgressAuditPolicy `protobuf:"bytes,4,opt,name=audit,proto3" json:"audit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPolicy) Reset() { + *x = EgressPolicy{} + mi := &file_ateom_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPolicy) ProtoMessage() {} + +func (x *EgressPolicy) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPolicy.ProtoReflect.Descriptor instead. +func (*EgressPolicy) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{3} +} + +func (x *EgressPolicy) GetDefaultAction() string { + if x != nil { + return x.DefaultAction + } + return "" +} + +func (x *EgressPolicy) GetAllow() []*EgressPolicyRule { + if x != nil { + return x.Allow + } + return nil +} + +func (x *EgressPolicy) GetDeny() []*EgressPolicyRule { + if x != nil { + return x.Deny + } + return nil +} + +func (x *EgressPolicy) GetAudit() *EgressAuditPolicy { + if x != nil { + return x.Audit + } + return nil +} + +type EgressPolicyRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + To []*EgressPolicyDestination `protobuf:"bytes,1,rep,name=to,proto3" json:"to,omitempty"` + Ports []*EgressPort `protobuf:"bytes,2,rep,name=ports,proto3" json:"ports,omitempty"` + Tls *EgressTLSPolicy `protobuf:"bytes,3,opt,name=tls,proto3" json:"tls,omitempty"` + Credentials *EgressCredentialPolicy `protobuf:"bytes,4,opt,name=credentials,proto3" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPolicyRule) Reset() { + *x = EgressPolicyRule{} + mi := &file_ateom_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPolicyRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPolicyRule) ProtoMessage() {} + +func (x *EgressPolicyRule) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPolicyRule.ProtoReflect.Descriptor instead. +func (*EgressPolicyRule) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{4} +} + +func (x *EgressPolicyRule) GetTo() []*EgressPolicyDestination { + if x != nil { + return x.To + } + return nil +} + +func (x *EgressPolicyRule) GetPorts() []*EgressPort { + if x != nil { + return x.Ports + } + return nil +} + +func (x *EgressPolicyRule) GetTls() *EgressTLSPolicy { + if x != nil { + return x.Tls + } + return nil +} + +func (x *EgressPolicyRule) GetCredentials() *EgressCredentialPolicy { + if x != nil { + return x.Credentials + } + return nil +} + +type EgressPolicyDestination struct { + state protoimpl.MessageState `protogen:"open.v1"` + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + Cidr string `protobuf:"bytes,2,opt,name=cidr,proto3" json:"cidr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPolicyDestination) Reset() { + *x = EgressPolicyDestination{} + mi := &file_ateom_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPolicyDestination) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPolicyDestination) ProtoMessage() {} + +func (x *EgressPolicyDestination) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPolicyDestination.ProtoReflect.Descriptor instead. +func (*EgressPolicyDestination) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{5} +} + +func (x *EgressPolicyDestination) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *EgressPolicyDestination) GetCidr() string { + if x != nil { + return x.Cidr + } + return "" +} + +type EgressPort struct { + state protoimpl.MessageState `protogen:"open.v1"` + Port uint32 `protobuf:"varint,1,opt,name=port,proto3" json:"port,omitempty"` + Protocol string `protobuf:"bytes,2,opt,name=protocol,proto3" json:"protocol,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressPort) Reset() { + *x = EgressPort{} + mi := &file_ateom_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressPort) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressPort) ProtoMessage() {} + +func (x *EgressPort) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressPort.ProtoReflect.Descriptor instead. +func (*EgressPort) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{6} +} + +func (x *EgressPort) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *EgressPort) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +type EgressTLSPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Mode string `protobuf:"bytes,1,opt,name=mode,proto3" json:"mode,omitempty"` + Required bool `protobuf:"varint,2,opt,name=required,proto3" json:"required,omitempty"` + Intercept *EgressTLSInterceptPolicy `protobuf:"bytes,3,opt,name=intercept,proto3" json:"intercept,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressTLSPolicy) Reset() { + *x = EgressTLSPolicy{} + mi := &file_ateom_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressTLSPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressTLSPolicy) ProtoMessage() {} + +func (x *EgressTLSPolicy) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressTLSPolicy.ProtoReflect.Descriptor instead. +func (*EgressTLSPolicy) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{7} +} + +func (x *EgressTLSPolicy) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *EgressTLSPolicy) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *EgressTLSPolicy) GetIntercept() *EgressTLSInterceptPolicy { + if x != nil { + return x.Intercept + } + return nil +} + +type EgressTLSInterceptPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + IssuerSecretRef *SecretReference `protobuf:"bytes,1,opt,name=issuer_secret_ref,json=issuerSecretRef,proto3" json:"issuer_secret_ref,omitempty"` + ValidateUpstream bool `protobuf:"varint,2,opt,name=validate_upstream,json=validateUpstream,proto3" json:"validate_upstream,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressTLSInterceptPolicy) Reset() { + *x = EgressTLSInterceptPolicy{} + mi := &file_ateom_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressTLSInterceptPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressTLSInterceptPolicy) ProtoMessage() {} + +func (x *EgressTLSInterceptPolicy) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressTLSInterceptPolicy.ProtoReflect.Descriptor instead. +func (*EgressTLSInterceptPolicy) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{8} +} + +func (x *EgressTLSInterceptPolicy) GetIssuerSecretRef() *SecretReference { + if x != nil { + return x.IssuerSecretRef + } + return nil +} + +func (x *EgressTLSInterceptPolicy) GetValidateUpstream() bool { + if x != nil { + return x.ValidateUpstream + } + return false +} + +type EgressCredentialPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Inject []*EgressCredentialInjection `protobuf:"bytes,1,rep,name=inject,proto3" json:"inject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressCredentialPolicy) Reset() { + *x = EgressCredentialPolicy{} + mi := &file_ateom_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressCredentialPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressCredentialPolicy) ProtoMessage() {} + +func (x *EgressCredentialPolicy) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressCredentialPolicy.ProtoReflect.Descriptor instead. +func (*EgressCredentialPolicy) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{9} +} + +func (x *EgressCredentialPolicy) GetInject() []*EgressCredentialInjection { + if x != nil { + return x.Inject + } + return nil +} + +type EgressCredentialInjection struct { + state protoimpl.MessageState `protogen:"open.v1"` + Header string `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` + ValueFrom *EgressCredentialValueFrom `protobuf:"bytes,2,opt,name=value_from,json=valueFrom,proto3" json:"value_from,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressCredentialInjection) Reset() { + *x = EgressCredentialInjection{} + mi := &file_ateom_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressCredentialInjection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressCredentialInjection) ProtoMessage() {} + +func (x *EgressCredentialInjection) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressCredentialInjection.ProtoReflect.Descriptor instead. +func (*EgressCredentialInjection) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{10} +} + +func (x *EgressCredentialInjection) GetHeader() string { + if x != nil { + return x.Header + } + return "" +} + +func (x *EgressCredentialInjection) GetValueFrom() *EgressCredentialValueFrom { + if x != nil { + return x.ValueFrom + } + return nil +} + +type EgressCredentialValueFrom struct { + state protoimpl.MessageState `protogen:"open.v1"` + SecretKeyRef *SecretKeySelector `protobuf:"bytes,1,opt,name=secret_key_ref,json=secretKeyRef,proto3" json:"secret_key_ref,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressCredentialValueFrom) Reset() { + *x = EgressCredentialValueFrom{} + mi := &file_ateom_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressCredentialValueFrom) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressCredentialValueFrom) ProtoMessage() {} + +func (x *EgressCredentialValueFrom) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressCredentialValueFrom.ProtoReflect.Descriptor instead. +func (*EgressCredentialValueFrom) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{11} +} + +func (x *EgressCredentialValueFrom) GetSecretKeyRef() *SecretKeySelector { + if x != nil { + return x.SecretKeyRef + } + return nil +} + +type EgressAuditPolicy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Logs bool `protobuf:"varint,1,opt,name=logs,proto3" json:"logs,omitempty"` + Traces bool `protobuf:"varint,2,opt,name=traces,proto3" json:"traces,omitempty"` + RedactHeaders []string `protobuf:"bytes,3,rep,name=redact_headers,json=redactHeaders,proto3" json:"redact_headers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EgressAuditPolicy) Reset() { + *x = EgressAuditPolicy{} + mi := &file_ateom_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EgressAuditPolicy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EgressAuditPolicy) ProtoMessage() {} + +func (x *EgressAuditPolicy) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EgressAuditPolicy.ProtoReflect.Descriptor instead. +func (*EgressAuditPolicy) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{12} +} + +func (x *EgressAuditPolicy) GetLogs() bool { + if x != nil { + return x.Logs + } + return false +} + +func (x *EgressAuditPolicy) GetTraces() bool { + if x != nil { + return x.Traces + } + return false +} + +func (x *EgressAuditPolicy) GetRedactHeaders() []string { + if x != nil { + return x.RedactHeaders + } + return nil +} + +type SecretReference struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecretReference) Reset() { + *x = SecretReference{} + mi := &file_ateom_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecretReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretReference) ProtoMessage() {} + +func (x *SecretReference) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretReference.ProtoReflect.Descriptor instead. +func (*SecretReference) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{13} +} + +func (x *SecretReference) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SecretReference) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +type SecretKeySelector struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecretKeySelector) Reset() { + *x = SecretKeySelector{} + mi := &file_ateom_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecretKeySelector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretKeySelector) ProtoMessage() {} + +func (x *SecretKeySelector) ProtoReflect() protoreflect.Message { + mi := &file_ateom_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretKeySelector.ProtoReflect.Descriptor instead. +func (*SecretKeySelector) Descriptor() ([]byte, []int) { + return file_ateom_proto_rawDescGZIP(), []int{14} +} + +func (x *SecretKeySelector) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SecretKeySelector) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + type RunWorkloadResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -208,7 +872,7 @@ type RunWorkloadResponse struct { func (x *RunWorkloadResponse) Reset() { *x = RunWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[3] + mi := &file_ateom_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -220,7 +884,7 @@ func (x *RunWorkloadResponse) String() string { func (*RunWorkloadResponse) ProtoMessage() {} func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[3] + mi := &file_ateom_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -233,7 +897,7 @@ func (x *RunWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RunWorkloadResponse.ProtoReflect.Descriptor instead. func (*RunWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{3} + return file_ateom_proto_rawDescGZIP(), []int{15} } type CheckpointWorkloadRequest struct { @@ -258,7 +922,7 @@ type CheckpointWorkloadRequest struct { func (x *CheckpointWorkloadRequest) Reset() { *x = CheckpointWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[4] + mi := &file_ateom_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -270,7 +934,7 @@ func (x *CheckpointWorkloadRequest) String() string { func (*CheckpointWorkloadRequest) ProtoMessage() {} func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[4] + mi := &file_ateom_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -283,7 +947,7 @@ func (x *CheckpointWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadRequest.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{4} + return file_ateom_proto_rawDescGZIP(), []int{16} } func (x *CheckpointWorkloadRequest) GetActorTemplateNamespace() string { @@ -336,7 +1000,7 @@ type CheckpointWorkloadResponse struct { func (x *CheckpointWorkloadResponse) Reset() { *x = CheckpointWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[5] + mi := &file_ateom_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -348,7 +1012,7 @@ func (x *CheckpointWorkloadResponse) String() string { func (*CheckpointWorkloadResponse) ProtoMessage() {} func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[5] + mi := &file_ateom_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -361,7 +1025,7 @@ func (x *CheckpointWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointWorkloadResponse.ProtoReflect.Descriptor instead. func (*CheckpointWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{5} + return file_ateom_proto_rawDescGZIP(), []int{17} } type RestoreWorkloadRequest struct { @@ -379,7 +1043,7 @@ type RestoreWorkloadRequest struct { func (x *RestoreWorkloadRequest) Reset() { *x = RestoreWorkloadRequest{} - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -391,7 +1055,7 @@ func (x *RestoreWorkloadRequest) String() string { func (*RestoreWorkloadRequest) ProtoMessage() {} func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[6] + mi := &file_ateom_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -404,7 +1068,7 @@ func (x *RestoreWorkloadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadRequest.ProtoReflect.Descriptor instead. func (*RestoreWorkloadRequest) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{6} + return file_ateom_proto_rawDescGZIP(), []int{18} } func (x *RestoreWorkloadRequest) GetActorTemplateNamespace() string { @@ -457,7 +1121,7 @@ type RestoreWorkloadResponse struct { func (x *RestoreWorkloadResponse) Reset() { *x = RestoreWorkloadResponse{} - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -469,7 +1133,7 @@ func (x *RestoreWorkloadResponse) String() string { func (*RestoreWorkloadResponse) ProtoMessage() {} func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { - mi := &file_ateom_proto_msgTypes[7] + mi := &file_ateom_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -482,7 +1146,7 @@ func (x *RestoreWorkloadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RestoreWorkloadResponse.ProtoReflect.Descriptor instead. func (*RestoreWorkloadResponse) Descriptor() ([]byte, []int) { - return file_ateom_proto_rawDescGZIP(), []int{7} + return file_ateom_proto_rawDescGZIP(), []int{19} } var File_ateom_proto protoreflect.FileDescriptor @@ -496,13 +1160,56 @@ const file_ateom_proto_rawDesc = "" + "\bactor_id\x18\x03 \x01(\tR\aactorId\x12\x1d\n" + "\n" + "runsc_path\x18\x04 \x01(\tR\trunscPath\x12'\n" + - "\x04spec\x18\x05 \x01(\v2\x13.ateom.WorkloadSpecR\x04spec\"@\n" + + "\x04spec\x18\x05 \x01(\v2\x13.ateom.WorkloadSpecR\x04spec\"z\n" + "\fWorkloadSpec\x120\n" + "\n" + "containers\x18\x01 \x03(\v2\x10.ateom.ContainerR\n" + - "containers\"\x1f\n" + + "containers\x128\n" + + "\regress_policy\x18\x02 \x01(\v2\x13.ateom.EgressPolicyR\fegressPolicy\"\x1f\n" + "\tContainer\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\"\x15\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\xc1\x01\n" + + "\fEgressPolicy\x12%\n" + + "\x0edefault_action\x18\x01 \x01(\tR\rdefaultAction\x12-\n" + + "\x05allow\x18\x02 \x03(\v2\x17.ateom.EgressPolicyRuleR\x05allow\x12+\n" + + "\x04deny\x18\x03 \x03(\v2\x17.ateom.EgressPolicyRuleR\x04deny\x12.\n" + + "\x05audit\x18\x04 \x01(\v2\x18.ateom.EgressAuditPolicyR\x05audit\"\xd6\x01\n" + + "\x10EgressPolicyRule\x12.\n" + + "\x02to\x18\x01 \x03(\v2\x1e.ateom.EgressPolicyDestinationR\x02to\x12'\n" + + "\x05ports\x18\x02 \x03(\v2\x11.ateom.EgressPortR\x05ports\x12(\n" + + "\x03tls\x18\x03 \x01(\v2\x16.ateom.EgressTLSPolicyR\x03tls\x12?\n" + + "\vcredentials\x18\x04 \x01(\v2\x1d.ateom.EgressCredentialPolicyR\vcredentials\"A\n" + + "\x17EgressPolicyDestination\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04cidr\x18\x02 \x01(\tR\x04cidr\"<\n" + + "\n" + + "EgressPort\x12\x12\n" + + "\x04port\x18\x01 \x01(\rR\x04port\x12\x1a\n" + + "\bprotocol\x18\x02 \x01(\tR\bprotocol\"\x80\x01\n" + + "\x0fEgressTLSPolicy\x12\x12\n" + + "\x04mode\x18\x01 \x01(\tR\x04mode\x12\x1a\n" + + "\brequired\x18\x02 \x01(\bR\brequired\x12=\n" + + "\tintercept\x18\x03 \x01(\v2\x1f.ateom.EgressTLSInterceptPolicyR\tintercept\"\x8b\x01\n" + + "\x18EgressTLSInterceptPolicy\x12B\n" + + "\x11issuer_secret_ref\x18\x01 \x01(\v2\x16.ateom.SecretReferenceR\x0fissuerSecretRef\x12+\n" + + "\x11validate_upstream\x18\x02 \x01(\bR\x10validateUpstream\"R\n" + + "\x16EgressCredentialPolicy\x128\n" + + "\x06inject\x18\x01 \x03(\v2 .ateom.EgressCredentialInjectionR\x06inject\"t\n" + + "\x19EgressCredentialInjection\x12\x16\n" + + "\x06header\x18\x01 \x01(\tR\x06header\x12?\n" + + "\n" + + "value_from\x18\x02 \x01(\v2 .ateom.EgressCredentialValueFromR\tvalueFrom\"[\n" + + "\x19EgressCredentialValueFrom\x12>\n" + + "\x0esecret_key_ref\x18\x01 \x01(\v2\x18.ateom.SecretKeySelectorR\fsecretKeyRef\"f\n" + + "\x11EgressAuditPolicy\x12\x12\n" + + "\x04logs\x18\x01 \x01(\bR\x04logs\x12\x16\n" + + "\x06traces\x18\x02 \x01(\bR\x06traces\x12%\n" + + "\x0eredact_headers\x18\x03 \x03(\tR\rredactHeaders\"C\n" + + "\x0fSecretReference\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + + "\tnamespace\x18\x02 \x01(\tR\tnamespace\"9\n" + + "\x11SecretKeySelector\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\"\x15\n" + "\x13RunWorkloadResponse\"\x98\x02\n" + "\x19CheckpointWorkloadRequest\x128\n" + "\x18actor_template_namespace\x18\x01 \x01(\tR\x16actorTemplateNamespace\x12.\n" + @@ -539,33 +1246,58 @@ func file_ateom_proto_rawDescGZIP() []byte { return file_ateom_proto_rawDescData } -var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_ateom_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_ateom_proto_goTypes = []any{ (*RunWorkloadRequest)(nil), // 0: ateom.RunWorkloadRequest (*WorkloadSpec)(nil), // 1: ateom.WorkloadSpec (*Container)(nil), // 2: ateom.Container - (*RunWorkloadResponse)(nil), // 3: ateom.RunWorkloadResponse - (*CheckpointWorkloadRequest)(nil), // 4: ateom.CheckpointWorkloadRequest - (*CheckpointWorkloadResponse)(nil), // 5: ateom.CheckpointWorkloadResponse - (*RestoreWorkloadRequest)(nil), // 6: ateom.RestoreWorkloadRequest - (*RestoreWorkloadResponse)(nil), // 7: ateom.RestoreWorkloadResponse + (*EgressPolicy)(nil), // 3: ateom.EgressPolicy + (*EgressPolicyRule)(nil), // 4: ateom.EgressPolicyRule + (*EgressPolicyDestination)(nil), // 5: ateom.EgressPolicyDestination + (*EgressPort)(nil), // 6: ateom.EgressPort + (*EgressTLSPolicy)(nil), // 7: ateom.EgressTLSPolicy + (*EgressTLSInterceptPolicy)(nil), // 8: ateom.EgressTLSInterceptPolicy + (*EgressCredentialPolicy)(nil), // 9: ateom.EgressCredentialPolicy + (*EgressCredentialInjection)(nil), // 10: ateom.EgressCredentialInjection + (*EgressCredentialValueFrom)(nil), // 11: ateom.EgressCredentialValueFrom + (*EgressAuditPolicy)(nil), // 12: ateom.EgressAuditPolicy + (*SecretReference)(nil), // 13: ateom.SecretReference + (*SecretKeySelector)(nil), // 14: ateom.SecretKeySelector + (*RunWorkloadResponse)(nil), // 15: ateom.RunWorkloadResponse + (*CheckpointWorkloadRequest)(nil), // 16: ateom.CheckpointWorkloadRequest + (*CheckpointWorkloadResponse)(nil), // 17: ateom.CheckpointWorkloadResponse + (*RestoreWorkloadRequest)(nil), // 18: ateom.RestoreWorkloadRequest + (*RestoreWorkloadResponse)(nil), // 19: ateom.RestoreWorkloadResponse } var file_ateom_proto_depIdxs = []int32{ - 1, // 0: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 2, // 1: ateom.WorkloadSpec.containers:type_name -> ateom.Container - 1, // 2: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 1, // 3: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec - 0, // 4: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest - 4, // 5: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest - 6, // 6: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest - 3, // 7: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse - 5, // 8: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse - 7, // 9: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 1, // 0: ateom.RunWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 2, // 1: ateom.WorkloadSpec.containers:type_name -> ateom.Container + 3, // 2: ateom.WorkloadSpec.egress_policy:type_name -> ateom.EgressPolicy + 4, // 3: ateom.EgressPolicy.allow:type_name -> ateom.EgressPolicyRule + 4, // 4: ateom.EgressPolicy.deny:type_name -> ateom.EgressPolicyRule + 12, // 5: ateom.EgressPolicy.audit:type_name -> ateom.EgressAuditPolicy + 5, // 6: ateom.EgressPolicyRule.to:type_name -> ateom.EgressPolicyDestination + 6, // 7: ateom.EgressPolicyRule.ports:type_name -> ateom.EgressPort + 7, // 8: ateom.EgressPolicyRule.tls:type_name -> ateom.EgressTLSPolicy + 9, // 9: ateom.EgressPolicyRule.credentials:type_name -> ateom.EgressCredentialPolicy + 8, // 10: ateom.EgressTLSPolicy.intercept:type_name -> ateom.EgressTLSInterceptPolicy + 13, // 11: ateom.EgressTLSInterceptPolicy.issuer_secret_ref:type_name -> ateom.SecretReference + 10, // 12: ateom.EgressCredentialPolicy.inject:type_name -> ateom.EgressCredentialInjection + 11, // 13: ateom.EgressCredentialInjection.value_from:type_name -> ateom.EgressCredentialValueFrom + 14, // 14: ateom.EgressCredentialValueFrom.secret_key_ref:type_name -> ateom.SecretKeySelector + 1, // 15: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 1, // 16: ateom.RestoreWorkloadRequest.spec:type_name -> ateom.WorkloadSpec + 0, // 17: ateom.Ateom.RunWorkload:input_type -> ateom.RunWorkloadRequest + 16, // 18: ateom.Ateom.CheckpointWorkload:input_type -> ateom.CheckpointWorkloadRequest + 18, // 19: ateom.Ateom.RestoreWorkload:input_type -> ateom.RestoreWorkloadRequest + 15, // 20: ateom.Ateom.RunWorkload:output_type -> ateom.RunWorkloadResponse + 17, // 21: ateom.Ateom.CheckpointWorkload:output_type -> ateom.CheckpointWorkloadResponse + 19, // 22: ateom.Ateom.RestoreWorkload:output_type -> ateom.RestoreWorkloadResponse + 20, // [20:23] is the sub-list for method output_type + 17, // [17:20] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_ateom_proto_init() } @@ -579,7 +1311,7 @@ func file_ateom_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ateom_proto_rawDesc), len(file_ateom_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/proto/ateompb/ateom.proto b/internal/proto/ateompb/ateom.proto index 1da19e5a3..c58bd828a 100644 --- a/internal/proto/ateompb/ateom.proto +++ b/internal/proto/ateompb/ateom.proto @@ -59,13 +59,78 @@ message RunWorkloadRequest { // WorkloadSpec parallels Pod, but with far fewer configurable fields. message WorkloadSpec { - repeated Container containers = 1; + repeated Container containers = 1; + EgressPolicy egress_policy = 2; } message Container { string name = 1; } +message EgressPolicy { + string default_action = 1; + repeated EgressPolicyRule allow = 2; + repeated EgressPolicyRule deny = 3; + EgressAuditPolicy audit = 4; +} + +message EgressPolicyRule { + repeated EgressPolicyDestination to = 1; + repeated EgressPort ports = 2; + EgressTLSPolicy tls = 3; + EgressCredentialPolicy credentials = 4; +} + +message EgressPolicyDestination { + string host = 1; + string cidr = 2; +} + +message EgressPort { + uint32 port = 1; + string protocol = 2; +} + +message EgressTLSPolicy { + string mode = 1; + bool required = 2; + EgressTLSInterceptPolicy intercept = 3; +} + +message EgressTLSInterceptPolicy { + SecretReference issuer_secret_ref = 1; + bool validate_upstream = 2; +} + +message EgressCredentialPolicy { + repeated EgressCredentialInjection inject = 1; +} + +message EgressCredentialInjection { + string header = 1; + EgressCredentialValueFrom value_from = 2; +} + +message EgressCredentialValueFrom { + SecretKeySelector secret_key_ref = 1; +} + +message EgressAuditPolicy { + bool logs = 1; + bool traces = 2; + repeated string redact_headers = 3; +} + +message SecretReference { + string name = 1; + string namespace = 2; +} + +message SecretKeySelector { + string name = 1; + string key = 2; +} + message RunWorkloadResponse { } diff --git a/manifests/ate-install/generated/ate.dev_actortemplates.yaml b/manifests/ate-install/generated/ate.dev_actortemplates.yaml index 5f8bd893d..81fc9ad5d 100644 --- a/manifests/ate-install/generated/ate.dev_actortemplates.yaml +++ b/manifests/ate-install/generated/ate.dev_actortemplates.yaml @@ -153,6 +153,329 @@ spec: type: object maxItems: 10 type: array + egressPolicy: + description: |- + EgressPolicy defines the default outbound network policy for actors + created from this template. + properties: + allow: + description: Allow contains destination rules actors created from + this template may reach. + items: + properties: + credentials: + description: |- + Credentials configures explicit egress gateway credential injection for + matching outbound requests. + properties: + inject: + description: |- + Inject configures credentials that the egress gateway injects into + matching outbound requests. Values are referenced from Kubernetes Secrets; + the policy does not contain credential material. + items: + properties: + header: + description: Header is the outbound HTTP header + name to set. + type: string + valueFrom: + description: ValueFrom selects the source of the + injected credential value. + properties: + secretKeyRef: + description: SecretKeyRef selects a key in + a Kubernetes Secret. + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - header + - valueFrom + type: object + type: array + type: object + name: + description: Name is an optional human-readable identifier + for this rule. + type: string + ports: + description: |- + Ports is the list of destination ports matched by this rule. + If empty, the rule applies to all destination ports. + items: + properties: + port: + description: Port is the destination port number. + format: int32 + type: integer + protocol: + description: Protocol is the transport protocol for + this port. + type: string + required: + - port + type: object + type: array + tls: + description: TLS defines transport security requirements + for this destination. + properties: + intercept: + description: Intercept configures explicit TLS interception + for matching egress traffic. + properties: + issuerSecretRef: + description: |- + IssuerSecretRef references the CA material used by the egress gateway to + issue certificates for intercepted TLS traffic. + properties: + name: + description: name is unique within a namespace + to reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + validateUpstream: + description: |- + ValidateUpstream controls whether the egress gateway validates the + upstream service certificate before proxying intercepted traffic. + type: boolean + type: object + mode: + description: Mode controls how TLS is handled for matching + egress traffic. + enum: + - Require + - Originate + - Intercept + - Disable + type: string + required: + description: Required controls whether matching egress + traffic must use TLS. + type: boolean + type: object + to: + description: To lists the destinations matched by this rule. + items: + properties: + host: + description: Host is the DNS name to match for egress + traffic. + type: string + ipBlock: + description: IPBlock is the IP range to match for + egress traffic. + properties: + cidr: + description: CIDR is an IP address range in CIDR + notation. + type: string + required: + - cidr + type: object + type: object + type: array + type: object + type: array + audit: + description: Audit configures egress logging and tracing for actors + created from this template. + properties: + logs: + description: Logs enables egress access logs for actors created + from this template. + type: boolean + redactHeaders: + description: RedactHeaders is the list of headers that must + be redacted from audit output. + items: + type: string + type: array + traces: + description: Traces enables egress tracing for actors created + from this template. + type: boolean + type: object + defaultAction: + description: DefaultAction is applied when no allow rule matches. + enum: + - Allow + - Deny + type: string + deny: + description: Deny contains destination rules actors created from + this template may not reach. + items: + properties: + credentials: + description: |- + Credentials configures explicit egress gateway credential injection for + matching outbound requests. + properties: + inject: + description: |- + Inject configures credentials that the egress gateway injects into + matching outbound requests. Values are referenced from Kubernetes Secrets; + the policy does not contain credential material. + items: + properties: + header: + description: Header is the outbound HTTP header + name to set. + type: string + valueFrom: + description: ValueFrom selects the source of the + injected credential value. + properties: + secretKeyRef: + description: SecretKeyRef selects a key in + a Kubernetes Secret. + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - header + - valueFrom + type: object + type: array + type: object + name: + description: Name is an optional human-readable identifier + for this rule. + type: string + ports: + description: |- + Ports is the list of destination ports matched by this rule. + If empty, the rule applies to all destination ports. + items: + properties: + port: + description: Port is the destination port number. + format: int32 + type: integer + protocol: + description: Protocol is the transport protocol for + this port. + type: string + required: + - port + type: object + type: array + tls: + description: TLS defines transport security requirements + for this destination. + properties: + intercept: + description: Intercept configures explicit TLS interception + for matching egress traffic. + properties: + issuerSecretRef: + description: |- + IssuerSecretRef references the CA material used by the egress gateway to + issue certificates for intercepted TLS traffic. + properties: + name: + description: name is unique within a namespace + to reference a secret resource. + type: string + namespace: + description: namespace defines the space within + which the secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + validateUpstream: + description: |- + ValidateUpstream controls whether the egress gateway validates the + upstream service certificate before proxying intercepted traffic. + type: boolean + type: object + mode: + description: Mode controls how TLS is handled for matching + egress traffic. + enum: + - Require + - Originate + - Intercept + - Disable + type: string + required: + description: Required controls whether matching egress + traffic must use TLS. + type: boolean + type: object + to: + description: To lists the destinations matched by this rule. + items: + properties: + host: + description: Host is the DNS name to match for egress + traffic. + type: string + ipBlock: + description: IPBlock is the IP range to match for + egress traffic. + properties: + cidr: + description: CIDR is an IP address range in CIDR + notation. + type: string + required: + - cidr + type: object + type: object + type: array + type: object + type: array + type: object pauseImage: description: |- PauseImage is the container to use as the root sandbox container. diff --git a/pkg/api/v1alpha1/actortemplate_types.go b/pkg/api/v1alpha1/actortemplate_types.go index 7c1272ca9..78ccafff5 100644 --- a/pkg/api/v1alpha1/actortemplate_types.go +++ b/pkg/api/v1alpha1/actortemplate_types.go @@ -134,6 +134,161 @@ type SnapshotsConfig struct { Location string `json:"location"` } +type EgressPolicyAction string + +const ( + EgressPolicyActionAllow EgressPolicyAction = "Allow" + EgressPolicyActionDeny EgressPolicyAction = "Deny" +) + +type EgressTLSMode string + +const ( + EgressTLSModeRequire EgressTLSMode = "Require" + EgressTLSModeOriginate EgressTLSMode = "Originate" + EgressTLSModeIntercept EgressTLSMode = "Intercept" + EgressTLSModeDisable EgressTLSMode = "Disable" +) + +type EgressTLSPolicy struct { + // Mode controls how TLS is handled for matching egress traffic. + // + // +kubebuilder:validation:Enum=Require;Originate;Intercept;Disable + // +optional + Mode EgressTLSMode `json:"mode,omitempty"` + + // Required controls whether matching egress traffic must use TLS. + // +optional + Required bool `json:"required,omitempty"` + + // Intercept configures explicit TLS interception for matching egress traffic. + // +optional + Intercept *EgressTLSInterceptPolicy `json:"intercept,omitempty"` +} + +type EgressTLSInterceptPolicy struct { + // IssuerSecretRef references the CA material used by the egress gateway to + // issue certificates for intercepted TLS traffic. + // +optional + IssuerSecretRef *corev1.SecretReference `json:"issuerSecretRef,omitempty"` + + // ValidateUpstream controls whether the egress gateway validates the + // upstream service certificate before proxying intercepted traffic. + // +optional + ValidateUpstream bool `json:"validateUpstream,omitempty"` +} + +type EgressIPBlock struct { + // CIDR is an IP address range in CIDR notation. + // +required + CIDR string `json:"cidr"` +} + +type EgressPolicyDestination struct { + // Host is the DNS name to match for egress traffic. + // +optional + Host string `json:"host,omitempty"` + + // IPBlock is the IP range to match for egress traffic. + // +optional + IPBlock *EgressIPBlock `json:"ipBlock,omitempty"` +} + +type EgressPort struct { + // Port is the destination port number. + // +required + Port int32 `json:"port"` + + // Protocol is the transport protocol for this port. + // +optional + Protocol corev1.Protocol `json:"protocol,omitempty"` +} + +type EgressCredentialPolicy struct { + // Inject configures credentials that the egress gateway injects into + // matching outbound requests. Values are referenced from Kubernetes Secrets; + // the policy does not contain credential material. + // +optional + Inject []EgressCredentialInjection `json:"inject,omitempty"` +} + +type EgressCredentialInjection struct { + // Header is the outbound HTTP header name to set. + // +required + Header string `json:"header"` + + // ValueFrom selects the source of the injected credential value. + // +required + ValueFrom EgressCredentialValueFrom `json:"valueFrom"` +} + +type EgressCredentialValueFrom struct { + // SecretKeyRef selects a key in a Kubernetes Secret. + // +optional + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` +} + +type EgressPolicyRule struct { + // Name is an optional human-readable identifier for this rule. + // +optional + Name string `json:"name,omitempty"` + + // To lists the destinations matched by this rule. + // +optional + To []EgressPolicyDestination `json:"to,omitempty"` + + // Ports is the list of destination ports matched by this rule. + // If empty, the rule applies to all destination ports. + // +optional + Ports []EgressPort `json:"ports,omitempty"` + + // TLS defines transport security requirements for this destination. + // +optional + TLS *EgressTLSPolicy `json:"tls,omitempty"` + + // Credentials configures explicit egress gateway credential injection for + // matching outbound requests. + // +optional + Credentials *EgressCredentialPolicy `json:"credentials,omitempty"` + + // TODO: Add L7 policy fields here when they are needed, such as path + // matches, rate limits, or header handling. +} + +type EgressAuditPolicy struct { + // Logs enables egress access logs for actors created from this template. + // +optional + Logs bool `json:"logs,omitempty"` + + // Traces enables egress tracing for actors created from this template. + // +optional + Traces bool `json:"traces,omitempty"` + + // RedactHeaders is the list of headers that must be redacted from audit output. + // +optional + RedactHeaders []string `json:"redactHeaders,omitempty"` +} + +type EgressPolicy struct { + // DefaultAction is applied when no allow rule matches. + // + // +kubebuilder:validation:Enum=Allow;Deny + // +optional + DefaultAction EgressPolicyAction `json:"defaultAction,omitempty"` + + // Allow contains destination rules actors created from this template may reach. + // +optional + Allow []EgressPolicyRule `json:"allow,omitempty"` + + // Deny contains destination rules actors created from this template may not reach. + // +optional + Deny []EgressPolicyRule `json:"deny,omitempty"` + + // Audit configures egress logging and tracing for actors created from this template. + // +optional + Audit *EgressAuditPolicy `json:"audit,omitempty"` +} + // ActorTemplateSpec defined desired spec of an actor. type ActorTemplateSpec struct { // PauseImage is the container to use as the root sandbox container. @@ -168,6 +323,11 @@ type ActorTemplateSpec struct { // // +required Runsc RunscConfig `json:"runsc,omitempty"` + + // EgressPolicy defines the default outbound network policy for actors + // created from this template. + // +optional + EgressPolicy *EgressPolicy `json:"egressPolicy,omitempty"` } type GCPAuthenticationConfig struct { diff --git a/pkg/api/v1alpha1/zz_generated.deepcopy.go b/pkg/api/v1alpha1/zz_generated.deepcopy.go index 4bbfe02b6..1385b115d 100644 --- a/pkg/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/api/v1alpha1/zz_generated.deepcopy.go @@ -19,7 +19,8 @@ package v1alpha1 import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -95,6 +96,11 @@ func (in *ActorTemplateSpec) DeepCopyInto(out *ActorTemplateSpec) { out.SnapshotsConfig = in.SnapshotsConfig out.WorkerPoolRef = in.WorkerPoolRef in.Runsc.DeepCopyInto(&out.Runsc) + if in.EgressPolicy != nil { + in, out := &in.EgressPolicy, &out.EgressPolicy + *out = new(EgressPolicy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActorTemplateSpec. @@ -113,7 +119,7 @@ func (in *ActorTemplateStatus) DeepCopyInto(out *ActorTemplateStatus) { in.TakeGoldenSnapshotAt.DeepCopyInto(&out.TakeGoldenSnapshotAt) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -177,6 +183,245 @@ func (in *Container) DeepCopy() *Container { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressAuditPolicy) DeepCopyInto(out *EgressAuditPolicy) { + *out = *in + if in.RedactHeaders != nil { + in, out := &in.RedactHeaders, &out.RedactHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressAuditPolicy. +func (in *EgressAuditPolicy) DeepCopy() *EgressAuditPolicy { + if in == nil { + return nil + } + out := new(EgressAuditPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressCredentialInjection) DeepCopyInto(out *EgressCredentialInjection) { + *out = *in + in.ValueFrom.DeepCopyInto(&out.ValueFrom) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressCredentialInjection. +func (in *EgressCredentialInjection) DeepCopy() *EgressCredentialInjection { + if in == nil { + return nil + } + out := new(EgressCredentialInjection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressCredentialPolicy) DeepCopyInto(out *EgressCredentialPolicy) { + *out = *in + if in.Inject != nil { + in, out := &in.Inject, &out.Inject + *out = make([]EgressCredentialInjection, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressCredentialPolicy. +func (in *EgressCredentialPolicy) DeepCopy() *EgressCredentialPolicy { + if in == nil { + return nil + } + out := new(EgressCredentialPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressCredentialValueFrom) DeepCopyInto(out *EgressCredentialValueFrom) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressCredentialValueFrom. +func (in *EgressCredentialValueFrom) DeepCopy() *EgressCredentialValueFrom { + if in == nil { + return nil + } + out := new(EgressCredentialValueFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressIPBlock) DeepCopyInto(out *EgressIPBlock) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressIPBlock. +func (in *EgressIPBlock) DeepCopy() *EgressIPBlock { + if in == nil { + return nil + } + out := new(EgressIPBlock) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressPolicy) DeepCopyInto(out *EgressPolicy) { + *out = *in + if in.Allow != nil { + in, out := &in.Allow, &out.Allow + *out = make([]EgressPolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Deny != nil { + in, out := &in.Deny, &out.Deny + *out = make([]EgressPolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Audit != nil { + in, out := &in.Audit, &out.Audit + *out = new(EgressAuditPolicy) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressPolicy. +func (in *EgressPolicy) DeepCopy() *EgressPolicy { + if in == nil { + return nil + } + out := new(EgressPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressPolicyDestination) DeepCopyInto(out *EgressPolicyDestination) { + *out = *in + if in.IPBlock != nil { + in, out := &in.IPBlock, &out.IPBlock + *out = new(EgressIPBlock) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressPolicyDestination. +func (in *EgressPolicyDestination) DeepCopy() *EgressPolicyDestination { + if in == nil { + return nil + } + out := new(EgressPolicyDestination) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressPolicyRule) DeepCopyInto(out *EgressPolicyRule) { + *out = *in + if in.To != nil { + in, out := &in.To, &out.To + *out = make([]EgressPolicyDestination, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = make([]EgressPort, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(EgressTLSPolicy) + (*in).DeepCopyInto(*out) + } + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = new(EgressCredentialPolicy) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressPolicyRule. +func (in *EgressPolicyRule) DeepCopy() *EgressPolicyRule { + if in == nil { + return nil + } + out := new(EgressPolicyRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressPort) DeepCopyInto(out *EgressPort) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressPort. +func (in *EgressPort) DeepCopy() *EgressPort { + if in == nil { + return nil + } + out := new(EgressPort) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressTLSInterceptPolicy) DeepCopyInto(out *EgressTLSInterceptPolicy) { + *out = *in + if in.IssuerSecretRef != nil { + in, out := &in.IssuerSecretRef, &out.IssuerSecretRef + *out = new(v1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressTLSInterceptPolicy. +func (in *EgressTLSInterceptPolicy) DeepCopy() *EgressTLSInterceptPolicy { + if in == nil { + return nil + } + out := new(EgressTLSInterceptPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressTLSPolicy) DeepCopyInto(out *EgressTLSPolicy) { + *out = *in + if in.Intercept != nil { + in, out := &in.Intercept, &out.Intercept + *out = new(EgressTLSInterceptPolicy) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressTLSPolicy. +func (in *EgressTLSPolicy) DeepCopy() *EgressTLSPolicy { + if in == nil { + return nil + } + out := new(EgressTLSPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvVar) DeepCopyInto(out *EnvVar) { *out = *in