Add sidecar injection webhook proposal#100
Add sidecar injection webhook proposal#100tombentley wants to merge 20 commits intokroxylicious:mainfrom
Conversation
|
Discussed at Community Call 23rd April. Tom is looking for feedback. |
| | `kroxylicious.io/config-generation` | The `metadata.generation` of the `KroxyliciousSidecarConfig` at injection time | | ||
| | `kroxylicious.io/injection-timestamp` | ISO-8601 timestamp of when the sidecar was injected | | ||
|
|
||
| Because the webhook only mutates pods at creation time, configuration changes to `KroxyliciousSidecarConfig` do not propagate to running pods. This matches how Istio and Linkerd handle sidecar injection. Users must restart pods to pick up new configuration. |
There was a problem hiding this comment.
Not really. Because the proxy configuration is built from the KroxyliciousSidecarConfig at injection time it means that any subsequent changes to the KroxyliciousSidecarConfig are ignored until the next time the pod get created. That creates a lot of problems:
- The user who has RBAC on the
KroxyliciousSidecarConfigmight not have RBAC on thePod,Deploymentetc. So it assumes some kind of communication between those users. Such communication might not be possible in practice. - Erroneous changes to the
KroxyliciousSidecarConfigare only discovered at some unbounded time later.
The right way to solve this would be for the proxy to phone home to obtain a configuration from a control plane, and to receive updates to that configuration without the need for Pod restarts. But I really want to avoid boiling the ocean here by blocking this alpha sidecar feature on having such a control plane. I think the control plane piece is something we could add later (also as an alpha feature) and then maybe they would become stable features together later on.
|
Thanks @tombentley, looks like a solid proposal. I've left a few comments to be considered. |
tombentley
left a comment
There was a problem hiding this comment.
Thanks @k-wall @robobario, I think I've addresses your comments, though with a watering down of some of the initial ambition wrt trust boundaries. PTAL.
| | `kroxylicious.io/config-generation` | The `metadata.generation` of the `KroxyliciousSidecarConfig` at injection time | | ||
| | `kroxylicious.io/injection-timestamp` | ISO-8601 timestamp of when the sidecar was injected | | ||
|
|
||
| Because the webhook only mutates pods at creation time, configuration changes to `KroxyliciousSidecarConfig` do not propagate to running pods. This matches how Istio and Linkerd handle sidecar injection. Users must restart pods to pick up new configuration. |
There was a problem hiding this comment.
Not really. Because the proxy configuration is built from the KroxyliciousSidecarConfig at injection time it means that any subsequent changes to the KroxyliciousSidecarConfig are ignored until the next time the pod get created. That creates a lot of problems:
- The user who has RBAC on the
KroxyliciousSidecarConfigmight not have RBAC on thePod,Deploymentetc. So it assumes some kind of communication between those users. Such communication might not be possible in practice. - Erroneous changes to the
KroxyliciousSidecarConfigare only discovered at some unbounded time later.
The right way to solve this would be for the proxy to phone home to obtain a configuration from a control plane, and to receive updates to that configuration without the need for Pod restarts. But I really want to avoid boiling the ocean here by blocking this alpha sidecar feature on having such a control plane. I think the control plane piece is something we could add later (also as an alpha feature) and then maybe they would become stable features together later on.
Add PRs kroxylicious#96, kroxylicious#98, kroxylicious#99, kroxylicious#100, kroxylicious#101, and kroxylicious#103 which were opened after the initial script was created. Note: PR kroxylicious#100 is already correctly named (100-sidecar-injection-webhook.md). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This change simplifies the proposal numbering system by using PR numbers as proposal identifiers, eliminating number collisions and removing the need for a separate allocation process. Changes: - Simplified proposals/README.md to focus on author workflow - Removed index tables (directory listing serves as the index) - Streamlined instructions for creating and renaming proposals - Updated proposal template with workflow instructions - Require PR number in title format: # <PR#> - <Title> - Moved workflow instructions into comment block - Added GitHub workflow to automatically check proposal numbering - Validates both filename and title format - Updates PR description when proposal files don't match PR number - Provides exact commands to fix naming issues - Removes warning once corrected - Handles both added and renamed files - Runs on all PRs (ready for mandatory status check) - Added notification script for existing open PRs - After merge, run notify-open-prs.sh to ask authors to rebase - Workflow will automatically guide them through renaming - Updated with all current open proposal PRs (kroxylicious#70, kroxylicious#82, kroxylicious#83, kroxylicious#85, kroxylicious#88, kroxylicious#93, kroxylicious#94, kroxylicious#96, kroxylicious#98, kroxylicious#99, kroxylicious#100, kroxylicious#101, kroxylicious#103) Proposals 001-019 retain their original numbers. Assisted-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Keith Wall <kwall@apache.org>
This change simplifies the proposal numbering system by using PR numbers as proposal identifiers, eliminating number collisions and removing the need for a separate allocation process. Changes: - Simplified proposals/README.md to focus on author workflow - Removed index tables (directory listing serves as the index) - Streamlined instructions for creating and renaming proposals - Updated proposal template with workflow instructions - Require PR number in title format: # <PR#> - <Title> - Moved workflow instructions into comment block - Added GitHub workflow to automatically check proposal numbering - Validates both filename and title format - Updates PR description when proposal files don't match PR number - Provides exact commands to fix naming issues - Removes warning once corrected - Handles both added and renamed files - Runs on all PRs (ready for mandatory status check) - Added notification script for existing open PRs - After merge, run notify-open-prs.sh to ask authors to rebase - Workflow will automatically guide them through renaming - Updated with all current open proposal PRs (#70, #82, #83, #85, #88, #93, #94, #96, #98, #99, #100, #101, #103) Proposals 001-019 retain their original numbers. Assisted-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Keith Wall <kwall@apache.org>
|
Hi! We've updated the proposal numbering system to use PR numbers as proposal identifiers. Action required: Please rebase your PR on Once you rebase, you'll need to rename your proposal file and update the title: git mv proposals/100-sidecar-injection-webhook.md proposals/100-injection-webhook.md
# Update title: remove any old number prefix and add PR number
sed -i.bak '0,/^# /{s/^# \([0-9]\{3\}\|xxx\|nnn\|000\) - /# 100 - /; t; s/^# /# 100 - /}' proposals/100-injection-webhook.md && rm proposals/100-injection-webhook.md.bak
git add proposals/100-injection-webhook.md
git commit -m "Rename proposal to use PR number"
git pushThe GitHub workflow will automatically check your proposal file naming after you push and update this PR description if any corrections are still needed. See proposals/README.md for the updated workflow. |
|
Correction: The previous notification had an incorrect filename. Here are the correct commands: git mv proposals/100-sidecar-injection-webhook.md proposals/100-sidecar-injection-webhook.md
# Update title: remove any old number prefix and add PR number
sed -i.bak '0,/^# /{s/^# \([0-9]\{3\}\|xxx\|nnn\|000\) - /# 100 - /; t; s/^# /# 100 - /}' proposals/100-sidecar-injection-webhook.md && rm proposals/100-sidecar-injection-webhook.md.bak
git add proposals/100-sidecar-injection-webhook.md
git commit -m "Rename proposal to use PR number"
git pushSorry for the confusion! |
Use sidecar.kroxylicious.io/config-generation (recording the KroxyliciousSidecarConfig's metadata.generation) instead of a simple status flag. Serves as both idempotency guard and drift detection mechanism. Drop injection-timestamp. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Document the KroxyliciousSidecarConfig status subresource (Ready condition, observedGeneration) to match the implementation. Update module names and shared dependencies to reflect actual repo structure. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Document pod-level securityContext (runAsNonRoot, seccompProfile: RuntimeDefault). Fix probe port from 9190 to management port (9082). Add actual probe parameters from implementation. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Status update failures should be retried, not silently swallowed. Remove pod-level securityContext from webhook; admins should use PodSecurity admission policies instead to avoid webhook ordering conflicts. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Per review consensus, delegated plugin image selection is too high risk (arbitrary code execution in the proxy JVM). Admin-specified plugins via spec.plugins remain unaffected. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Move targetBootstrapServers, bootstrapPort, nodeIdRange, and targetClusterTls under a virtualClusters list. Remove delegated annotations section. Rewrite target cluster selection as a virtual clusters section. Update trust model, port allocation table, and future delegation to reflect the simplified alpha scope. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Add design for secretMounts field: motivation (secrets must not be in pod annotations), trust model progression (filesystem isolation now, network isolation later), and rationale for secretMounts over generic volume mounts. Update CRD example. Expand FEATURE_GATES rationale: the API server version does not reveal which feature gates are enabled, so explicit configuration avoids additional RBAC. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Use sidecar.kroxylicious.io/injection: disabled for pod opt-out, matching the namespace label key. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
The current container-name check is weak against adversarial pods and does not detect config drift during reinvocation. Document comparing the proxy-config annotation against the expected output as a stronger alternative, noting that Jackson serialisation is deterministic. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Update CRD group from kroxylicious.io to sidecar.kroxylicious.io to avoid conflicts with the operator. Add skip labelling section describing the injection-skipped label for operator observability. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Move Config injection and Configuration drift detection before Status, since Status references the config-generation annotation those sections introduce. Fix "an internal errors" typo. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Assisted-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Tom Bentley <tbentley@redhat.com>
Signed-off-by: Tom Bentley <tbentley@redhat.com>
|
@robobario @SamBarker @k-wall I've tried to address your previous comments, and also bring this into live with the implementation PR. Please take another look. |
|
|
||
| **Why not reuse the operator's CRDs?** The operator CRDs model a shared proxy deployment with ingress networking, multi-cluster support, and cross-resource references. The sidecar use case is fundamentally simpler — localhost binding, no ingress, a single virtual cluster in the alpha. Coupling them would constrain both models. | ||
|
|
||
| The webhook admin creates one per namespace. The following edge cases are handled: |
There was a problem hiding this comment.
| The webhook admin creates one per namespace. The following edge cases are handled: | |
| The webhook admin creates one or more per namespace. The following edge cases are handled: |
Sounds a bit like you can only create one currently, but then the edge cases cover the multi case.
| - MutatingWebhookConfiguration | ||
| - cert-manager Certificate (optional) | ||
|
|
||
| **TLS**: Kubernetes requires HTTPS for admission webhooks. The primary path uses cert-manager with a self-signed issuer. A manual alternative (admin provides cert/key Secret) is documented. The webhook watches cert files for rotation and reloads the SSLContext. |
There was a problem hiding this comment.
should we mention that initially we will only support PEMs with PKCS8 RSA/EC private keys, and whatever the cert format is we can parse.
|
|
||
| ### Webhook deployment | ||
|
|
||
| The webhook is packaged as a container image and deployed as a single-replica `Deployment` in a dedicated `kroxylicious-webhook` namespace. Install manifests are provided for: |
There was a problem hiding this comment.
should probably mention multi-replica, PDB we've added for availability
| | `already-injected` | A container named `kroxylicious-proxy` already exists in the pod | | ||
| | `multiple-configs` | Multiple `KroxyliciousSidecarConfig` resources exist in the namespace and no explicit config was selected via annotation | | ||
|
|
||
| Pods that opted out via `sidecar.kroxylicious.io/injection: disabled` are not labelled — they already carry a label that identifies them. |
There was a problem hiding this comment.
Three suggestions on the injection-skipped label:
1. Add opted-out for explicitly opted-out pods.
The sidecar.kroxylicious.io/injection: disabled label expresses intent (set by
the app owner); injection-skipped is observed state (set by the webhook). They
are semantically different: without an ack label there's no way to distinguish
"the webhook honoured the opt-out" from "the webhook hasn't processed this pod
yet." Adding injection-skipped=opted-out also means operators get a single
uniform query for all non-injected pods regardless of reason:
kubectl get pods -l sidecar.kroxylicious.io/injection-skipped
2. Revisit the label values for clarity without the webhook's context.
For an SRE encountering these at 2am:
| Current | Suggested | Rationale |
|---|---|---|
no-config |
no-sidecar-config |
Makes clear it's the KroxyliciousSidecarConfig CR that's absent, not some internal webhook config |
multiple-configs |
ambiguous-sidecar-config |
"Multiple" describes the cause; "ambiguous" describes the problem the operator needs to resolve |
already-injected |
container-name-conflict |
The current name implies a previous injection pass; the actual cause is a pre-existing container with a conflicting name |
3. Reinvocation should be silent.
The configuration drift detection section describes a second skip path: the
webhook skips injection if the sidecar.kroxylicious.io/config-generation
annotation is already present (idempotency guard on reinvocation). The current
text implies this would set injection-skipped=already-injected, but reinvocation
is expected behaviour — adding the label would pollute the signal for operators
querying -l sidecar.kroxylicious.io/injection-skipped. Reinvocation should
produce no label.
There was a problem hiding this comment.
On (1): opted-out pods are excluded by objectSelector on the MutatingWebhookConfiguration; they never reach the webhook so it can't label them. Changing the exclusion mechanism (e.g. moving opt-out from objectSelector to in-webhook logic) would mean every pod creation in the namespace hits the webhook regardless. I think the current approach is the right trade-off.
On (2): Yes, we can use better values; I'll revise.
On (3): You're right, reinvocation is expected behaviour and shouldn't set the label. I'll make that explicit.
|
|
||
| The current idempotency check (container name) is weak: it can be circumvented by a pod owner pre-adding a container named `kroxylicious-proxy`, and it does not detect configuration drift during reinvocation. A stronger approach is to compare the `sidecar.kroxylicious.io/proxy-config` annotation on the pod against the configuration the webhook would generate right now. If the annotation is absent or differs, the webhook injects (or re-injects); if it matches, injection is skipped. | ||
|
|
||
| This works because `ProxyConfigGenerator.generateConfig()` is deterministic: given the same `KroxyliciousSidecarConfigSpec` inputs, Jackson serialisation produces identical YAML. String equality on the annotation value is therefore a reliable idempotency check within the same webhook version. This also handles the reinvocation case (`reinvocationPolicy: IfNeeded`): if another mutating webhook modifies the pod after initial injection, the webhook can verify that the proxy config annotation is still correct. |
There was a problem hiding this comment.
This paragraph is back-to-front: it cites the current implementation to justify
the design rather than stating the property the design requires. The proposal
should describe the requirement, and leave it to the implementation to satisfy it.
Something like: for annotation comparison to be a reliable idempotency check,
config generation must be deterministic — the same KroxyliciousSidecarConfigSpec
must always produce byte-identical serialized output within a given webhook
version. The implementation must guarantee this; how it does so is not a concern
of this proposal.
The version caveat also deserves calling out explicitly as a constraint: a webhook
upgrade that changes serialization output means existing pods' annotations no
longer match what the webhook would generate, making them appear as configuration
drift even though the KroxyliciousSidecarConfig itself hasn't changed. The
proposal should state this consequence — and note that remediation requires pods
to cycle, since nothing proactively re-injects running pods.
There was a problem hiding this comment.
I'll rewrite this to state the determinism requirement without referencing implementation details, and add an explicit note about the webhook-upgrade consequence: changed serialisation output means existing pods appear as drift, requiring pod cycling to remediate.
| |-----------|---------| | ||
| | `Ready` | The webhook has observed and accepted this configuration. `observedGeneration` on the condition tracks which generation was acknowledged. | | ||
|
|
||
| The webhook sets `Ready=True` (reason `Accepted`) when it first observes the config via its informer. The condition is only updated when the generation changes, avoiding unnecessary status writes. Status update failures (e.g. conflicts) should be retried; persistent failures should be surfaced via logging so that operators can investigate. A status update failure does not block pod admission, but a `KroxyliciousSidecarConfig` with a stale or missing `Ready` condition indicates a problem that needs attention. |
There was a problem hiding this comment.
The proposal only defines Ready=True. If the webhook observes a config and can
determine it is invalid (e.g. missing required fields, cases 1–3 from the edge
cases section), setting Ready=False with a descriptive reason and message would
surface the problem directly on the CRD — where an operator would naturally look
— rather than requiring them to find the relevant log line. As it stands, a
misconfigured KroxyliciousSidecarConfig is either acknowledged (Ready=True)
or invisible; there is no condition state that says "I have seen this and it is
wrong."
There was a problem hiding this comment.
Good point. I'll add Ready=False (reason Invalid, with a descriptive message) for configs that the webhook can determine are invalid at observation time. This naturally covers structural problems detectable by the webhook's own validation beyond what the CRD schema catches (e.g. referencing a non-existent Secret).
For the per-namespace edge cases (no-config, multiple-configs): these aren't properties of a single CRD, so they don't fit naturally into CRD status. They're already covered by the injection-skipped label on affected pods. I think that's the right place for those.
| ### Native sidecar containers | ||
|
|
||
| On Kubernetes 1.29+ (where the `SidecarContainers` feature gate is enabled by default), the webhook injects the proxy as a native sidecar — an init container with `restartPolicy: Always`. This gives proper startup ordering (proxy starts before the application) and shutdown ordering (proxy stops after the application). On older clusters, the webhook falls back to injecting into `spec.containers`. | ||
|
|
There was a problem hiding this comment.
FEATURE_GATES is ambiguous — it reads as though it controls the webhook's own
internal feature flags rather than describing which Kubernetes API server feature
gates are active on the cluster. K8S_FEATURE_GATES would be clearer about the
subject.
It would also be worth stating explicitly that this is an escape hatch for when
version-based detection is insufficient, not the recommended default. Deployers
who set it are taking on the responsibility of keeping it in sync with their
cluster configuration — the proposal should acknowledge that risk.
There was a problem hiding this comment.
I'll rename to K8S_FEATURE_GATES and add a note that this is an escape hatch for clusters where version-based detection is insufficient, with deployers responsible for keeping it in sync.
| - `terminationMessagePolicy: FallbackToLogsOnError` | ||
|
|
||
| The webhook does not set a pod-level `securityContext`. Pod-level security policies (e.g. `runAsNonRoot`, `seccompProfile`) are the responsibility of the cluster admin via Kubernetes `PodSecurity` admission (`pod-security.kubernetes.io/enforce: restricted` namespace label) or equivalent policy enforcement. Setting pod-level security context from a mutating webhook risks ordering conflicts with other webhooks. | ||
|
|
There was a problem hiding this comment.
The proposal only addresses one direction: the sidecar's security context is never
weakened below what the webhook would set. It doesn't address the reverse — a pod
with a weaker pod-level security context (e.g. seccompProfile: Unconfined to
support io_uring). Since the webhook doesn't set a container-level seccomp profile
on the sidecar, the sidecar would silently inherit a permissive pod-level one.
Not a blocker for alpha, but worth keeping in mind: the proxy itself is a candidate
for io_uring in the future (for performance), so we may actually want a way to
configure the sidecar's seccomp profile explicitly rather than inheriting from the
pod. The KroxyliciousSidecarConfig spec is the natural place for that.
There was a problem hiding this comment.
Good catch. For alpha, I think we should set seccompProfile: RuntimeDefault explicitly on the sidecar container, so it doesn't inherit a permissive pod-level profile. Container-level seccomp overrides pod-level, so this won't affect the app container.
Agreed that KroxyliciousSidecarConfig is the right place for explicit seccomp configuration if/when we need it (e.g. for io_uring). I'll add a note about that as a future consideration.
|
|
||
| The mount path is derived automatically from the `name` field — the admin does not specify `mountPath` directly. This keeps the sidecar's filesystem layout under webhook control, consistent with the operator's approach of using fixed base paths for secret volumes. | ||
|
|
||
| **Why `secretMounts` over generic `sidecarVolumes`/`sidecarVolumeMounts`?** It signals clear intent (admin-controlled secrets for the sidecar), limits the surface to Secrets rather than arbitrary volumes, and is easier to validate. The field can be generalised later without breaking the existing API. |
There was a problem hiding this comment.
secretMounts[].secretName is a cross-resource reference — without validating
that the referenced Secret exists in the pod's namespace, a
KroxyliciousSidecarConfig with a bad reference passes admission silently and
only fails when Kubernetes tries to mount the volume at scheduling time. This
validation needs to be part of the alpha implementation, not deferred. It is also
exactly the case where Ready=False would be most valuable — see separate
comment on § Status.
The operator has validation logic for LocalObjectReference fields that handles
this; the webhook should follow the same pattern.
| ### Virtual clusters | ||
|
|
||
| The `virtualClusters` list defines the target Kafka clusters that the sidecar proxy will serve. Each entry specifies a name, target bootstrap address, localhost listening port, node ID range, and optional TLS configuration. | ||
|
|
There was a problem hiding this comment.
Really like the forward-looking list structure here. Active/passive DR is a
compelling use case — the sidecar exposes both live and DR clusters, and failover
is a config change plus pod restart. Could be a great showcase for the project.
Rename injection-skipped label values to reference CRD kind directly, rewrite idempotency section to state requirements not implementation, add Ready=False for invalid configs, rename FEATURE_GATES to K8S_FEATURE_GATES, and add explicit seccompProfile: RuntimeDefault. Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
Assisted-by: Claude Opus 4.6 noreply@anthropic.com