Documentation
¶
Overview ¶
Package podmutate contains the controller's pod-mutating admission webhook. It does these things on pod CREATE, mostly mirroring Istio's sidecar injector:
- Namespace auto-injection: a pod created in a namespace labeled aether.io/managed=true is given the aether.io/managed=true POD label so the CNI meshes it — no per-pod label needed. A pod that explicitly sets aether.io/managed=false opts OUT (left unmanaged), so individual workloads (Jobs, the prober, infra) can be excluded from an otherwise-managed namespace.
- dnsConfig ndots: injects a low ndots into managed pods so a mesh FQDN (<svc>.<meshDomain>, e.g. 2 dots) is tried as an absolute name BEFORE the cluster.local search list. Without it the k8s default ndots:5 makes the resolver apply the search domains first; glibc tolerates the fall-through to the bare name, but musl (Alpine) trips on the churn and fails to resolve mesh names. The chart ships ndots injection ON (`controller.injectPodNdots: true`) alongside mesh DNS.
- dnsConfig timeout/attempts: bounds what a single LOST DNS datagram costs. The resolver a managed pod talks to is node-local (the CNI DNATs :53 to the node's mesh-DNS), so a query that gets no answer is not a slow answer, it is a dropped one — and the stock resolv.conf default makes the client wait a full 5s before retransmitting. Because Go's resolver singleflights by name, that one drop stalls EVERY concurrent lookup of that name for the whole 5s, turning a lost packet into a seconds-long per-name outage (issue #726: a mesh-DNS surge handoff closes the predecessor's SO_REUSEPORT socket, the datagram in flight to it is discarded, and the client eats 5s). A 1s retransmit against a node-local resolver is generous, and 3 attempts leave a 3s worst case against the 10s the defaults allow.
- Egress identity gate (#1053, see identitygate.go): the aether-identity-ready init container, placed first, which holds the pod's app containers until SPIRE has issued the pod's SVID — so the app cannot send before the node proxy has a client certificate for it. Opt out per pod with aether.io/identity-gate=false; the chart turns it off mesh-wide with controller.webhook.identityGate.enabled=false.
- UDS carrier check (proposal 039 Phase 2, see udscarrier.go): a managed pod whose Unix socket could never be delivered — an endpoint.aether.io/uds-socket annotation naming an emptyDir (the removed carrier) or an undeclared volume, a file name over the AF_UNIX budget, a csi.aether.io volume without securityContext.fsGroup, or two of them — is DENIED with a message naming the fix. This is the one thing the webhook refuses rather than mutates.
The webhook is wired with two rules: an objectSelector (aether.io/managed=true pods, in any namespace) and a namespaceSelector (pods in aether.io/managed=true namespaces). Both dispatch here; Handle is idempotent for either entry point.
Index ¶
Constants ¶
const ( // IdentityGateContainerName is the init container the webhook injects. It is // also the idempotency key: a pod that already carries an init container of // this name is left alone. IdentityGateContainerName = "aether-identity-ready" // IdentityGateVolumeName is the csi.spiffe.io volume that exposes the SPIRE // agent's Workload API socket — to the init container ONLY. IdentityGateVolumeName = "aether-identity-gate-spiffe" // IdentityGateCommand is where the binary sits in the agent image // (//agent/cmd/agent:agent_image, extra layer). IdentityGateCommand = "/identity-ready" )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type IdentityGate ¶
type IdentityGate struct {
// Image runs /identity-ready: the agent image, already on every node.
Image string
// PullPolicy of the init container.
PullPolicy corev1.PullPolicy
// WorkloadSocket is the SPIRE Workload API socket path INSIDE the init
// container; the CSI volume is mounted at its directory.
WorkloadSocket string
// Timeout, when non-zero, makes identity-ready give up (exit 1) after that
// long without an SVID. Zero waits forever: the pod stays in Init (fail
// closed).
Timeout time.Duration
// Resources of the init container.
Resources corev1.ResourceRequirements
}
IdentityGate configures the egress identity gate (#1053): an init container that holds a mesh-managed pod's app containers until SPIRE has issued the pod's X.509 SVID, so the app cannot send before the node proxy has a client certificate for it. It is the egress twin of the inbound-readiness promotion gate, which holds an endpoint UNHEALTHY until an mTLS handshake with the pod's own inbound listener succeeds.
func (*IdentityGate) Validate ¶
func (g *IdentityGate) Validate() error
Validate reports a configuration the webhook cannot inject from.
type Mutator ¶
type Mutator struct {
NDots string
// Gate is the egress identity gate; nil disables it.
Gate *IdentityGate
Log *slog.Logger
}
Mutator injects the mesh resolv.conf options into a pod on CREATE: ndots=NDots plus the retransmit budget (timeout/attempts). NDots is the dot count of <svc>.<meshDomain> (= the label count of meshDomain; 2 for aether.internal), so mesh FQDNs are resolved absolute-first while shorter k8s names keep their search behavior.
func NewMutator ¶
NewMutator builds the pod-ndots mutator.
func (*Mutator) Handle ¶
Handle reaches here for a pod matched either by the managed-pod objectSelector or the managed-namespace namespaceSelector. It (1) ensures the aether.io/managed label so the CNI meshes the pod — unless the pod explicitly opts out with aether.io/managed=false — and (2) injects the mesh resolv.conf options into managed pods, and (3) injects the egress identity gate unless disabled or opted out. A pod that opts out is left entirely untouched. Idempotent: re-admission (or a pod that already carries the label / the options) produces no spurious patch.
func (*Mutator) WithIdentityGate ¶
func (m *Mutator) WithIdentityGate(g *IdentityGate) *Mutator
WithIdentityGate enables the egress identity gate (#1053); nil disables it.