From 15d959a6c8b809420cb86b67f17cf447a235f754 Mon Sep 17 00:00:00 2001 From: biondizzle Date: Wed, 10 Dec 2025 10:43:57 -0500 Subject: [PATCH] first commit --- Makefile | 114 +++++++++++++ README.md | 268 +++++++++++++++++++++++++++++++ TROUBLESHOOTING.md | 390 +++++++++++++++++++++++++++++++++++++++++++++ example.yaml | 139 ++++++++++++++++ go.mod | 57 +++++++ go.sum | 167 +++++++++++++++++++ main.go | 370 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1505 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 TROUBLESHOOTING.md create mode 100644 example.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..178572c --- /dev/null +++ b/Makefile @@ -0,0 +1,114 @@ +.PHONY: build push deploy certs test clean + +IMAGE_NAME ?= ewr.vultrcr.com/chansey/irsa-webhook +IMAGE_TAG ?= latest +NAMESPACE ?= irsa-system + +# Build the Go binary +build: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o webhook main.go + +# Build Docker image +docker-build: + docker build -t $(IMAGE_NAME):$(IMAGE_TAG) . + +# Push Docker image +docker-push: + docker push $(IMAGE_NAME):$(IMAGE_TAG) + +# Generate TLS certificates +certs: + chmod +x generate-certs.sh + ./generate-certs.sh + +# Deploy to Kubernetes +deploy: + kubectl apply -f deploy.yaml + +# Undeploy from Kubernetes +undeploy: + kubectl delete -f deploy.yaml + +# View logs +logs: + kubectl logs -n $(NAMESPACE) -l app=irsa-webhook -f + +# Test with example pod +test-example: + kubectl apply -f example.yaml + @echo "Waiting for pod to be ready..." + @kubectl wait --for=condition=Ready pod/example-app --timeout=60s || true + @echo "" + @echo "Pod logs:" + @kubectl logs example-app + +# Clean up test resources +clean-example: + kubectl delete -f example.yaml --ignore-not-found + +# Run Go tests +go-test: + go test -v ./... + +# Format Go code +fmt: + go fmt ./... + +# Run Go vet +vet: + go vet ./... + +# Download Go dependencies +deps: + go mod download + go mod tidy + +# Complete build and deploy pipeline +all: deps build docker-build docker-push certs deploy + +# Check webhook status +status: + @echo "=== Webhook Deployment ===" + @kubectl get deployment -n $(NAMESPACE) irsa-webhook + @echo "" + @echo "=== Webhook Pods ===" + @kubectl get pods -n $(NAMESPACE) -l app=irsa-webhook + @echo "" + @echo "=== Webhook Service ===" + @kubectl get svc -n $(NAMESPACE) irsa-webhook + @echo "" + @echo "=== MutatingWebhookConfiguration ===" + @kubectl get mutatingwebhookconfiguration irsa-webhook + +# Clean all resources +clean: + kubectl delete -f deploy.yaml --ignore-not-found + kubectl delete namespace $(NAMESPACE) --ignore-not-found + rm -f webhook + +# Restart webhook deployment +restart: + kubectl rollout restart deployment -n $(NAMESPACE) irsa-webhook + kubectl rollout status deployment -n $(NAMESPACE) irsa-webhook + +# Help +help: + @echo "Available targets:" + @echo " build - Build Go binary" + @echo " docker-build - Build Docker image" + @echo " docker-push - Push Docker image" + @echo " certs - Generate TLS certificates" + @echo " deploy - Deploy to Kubernetes" + @echo " undeploy - Remove from Kubernetes" + @echo " logs - View webhook logs" + @echo " test-example - Deploy and test example pod" + @echo " clean-example - Remove example pod" + @echo " go-test - Run Go tests" + @echo " fmt - Format Go code" + @echo " vet - Run Go vet" + @echo " deps - Download dependencies" + @echo " all - Complete build and deploy" + @echo " status - Check webhook status" + @echo " clean - Remove all resources" + @echo " restart - Restart webhook deployment" + @echo " help - Show this help" diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8ab15b --- /dev/null +++ b/README.md @@ -0,0 +1,268 @@ +# IRSA Mutating Admission Webhook for Kubernetes + +This webhook implements IAM Roles for Service Accounts (IRSA) for Kubernetes clusters, allowing pods to assume AWS IAM roles using projected service account tokens. + +## Features + +- Automatically injects AWS credentials configuration into pods +- Uses projected service account tokens with custom audience +- Supports multiple containers and init containers +- Follows security best practices +- No external dependencies beyond Kubernetes API + +## How It Works + +When a pod is created, the webhook: + +1. Extracts the ServiceAccount name from the pod spec +2. Fetches the ServiceAccount from the Kubernetes API +3. Checks for the `vultr.com/role-arn` annotation +4. If present, mutates the pod to inject: + - **Environment Variables:** + - `AWS_ROLE_ARN`: The IAM role ARN from the annotation + - `AWS_WEB_IDENTITY_TOKEN_FILE`: Path to the projected token + - `AWS_STS_REGIONAL_ENDPOINTS`: Set to "regional" + - **Volume:** A projected ServiceAccount token volume with audience "vultr" + - **Volume Mounts:** Mounts the token at `/var/run/secrets/vultr.com/serviceaccount` + +## Prerequisites + +- Kubernetes 1.20+ (for projected service account tokens) +- `kubectl` configured to access your cluster +- OpenSSL (for certificate generation) +- Go 1.21+ (for building from source) + +## Quick Start + +### 1. Build the Docker Image + +```bash +docker build -t your-registry/irsa-webhook:latest . +docker push your-registry/irsa-webhook:latest +``` + +Update `deploy.yaml` with your image location. + +### 2. Generate TLS Certificates + +The webhook requires TLS certificates to communicate with the Kubernetes API server: + +```bash +chmod +x generate-certs.sh +./generate-certs.sh +``` + +This script will: +- Generate a self-signed CA and certificate +- Create a Kubernetes secret with the certificates +- Update the MutatingWebhookConfiguration with the CA bundle + +### 3. Deploy the Webhook + +```bash +kubectl apply -f deploy.yaml +``` + +This creates: +- Namespace: `irsa-system` +- ServiceAccount with RBAC permissions +- Deployment with 2 replicas +- Service +- MutatingWebhookConfiguration + +### 4. Verify Deployment + +```bash +kubectl get pods -n irsa-system +kubectl logs -n irsa-system -l app=irsa-webhook +``` + +## Usage + +### Annotate ServiceAccount + +To enable IRSA for a ServiceAccount, add the `vultr.com/role-arn` annotation: + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-app + namespace: default + annotations: + vultr.com/role-arn: "arn:aws:iam::123456789012:role/my-app-role" +``` + +### Deploy a Pod + +Any pod using this ServiceAccount will automatically receive the AWS configuration: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: my-app + namespace: default +spec: + serviceAccountName: my-app + containers: + - name: app + image: amazon/aws-cli:latest + command: ["sleep", "3600"] +``` + +### Verify Injection + +Check that the pod has the injected configuration: + +```bash +# Check environment variables +kubectl exec my-app -- env | grep AWS + +# Check volume mount +kubectl exec my-app -- ls -la /var/run/secrets/vultr.com/serviceaccount + +# Test AWS credentials +kubectl exec my-app -- aws sts get-caller-identity +``` + +## Configuration + +### Environment Variables + +The webhook supports the following environment variables: + +- `TLS_CERT_PATH`: Path to TLS certificate (default: `/etc/webhook/certs/tls.crt`) +- `TLS_KEY_PATH`: Path to TLS private key (default: `/etc/webhook/certs/tls.key`) +- `PORT`: HTTPS port to listen on (default: `8443`) + +### Webhook Configuration + +Edit the `MutatingWebhookConfiguration` in `deploy.yaml`: + +- **failurePolicy**: Set to `Fail` for production to block pods if webhook is unavailable +- **timeoutSeconds**: Adjust timeout based on cluster performance +- **namespaceSelector**: Control which namespaces are affected + +## Security Considerations + +1. **Least Privilege**: The webhook ServiceAccount only has permissions to read ServiceAccounts +2. **TLS**: All communication is encrypted using TLS 1.2+ +3. **Non-root**: Container runs as non-root user (65532) +4. **Read-only filesystem**: Container has read-only root filesystem +5. **No privilege escalation**: Security context prevents privilege escalation + +## Troubleshooting + +### Webhook Not Mutating Pods + +1. Check webhook logs: + ```bash + kubectl logs -n irsa-system -l app=irsa-webhook + ``` + +2. Verify MutatingWebhookConfiguration: + ```bash + kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml + ``` + +3. Check if ServiceAccount has the annotation: + ```bash + kubectl get sa -o yaml + ``` + +### Certificate Issues + +If you see TLS errors, regenerate certificates: + +```bash +./generate-certs.sh +kubectl rollout restart deployment -n irsa-system irsa-webhook +``` + +### RBAC Permissions + +If webhook can't fetch ServiceAccounts, verify RBAC: + +```bash +kubectl auth can-i get serviceaccounts --as=system:serviceaccount:irsa-system:irsa-webhook --all-namespaces +``` + +## Development + +### Local Testing + +You can test the webhook logic locally: + +```go +package main + +import ( + "testing" + corev1 "k8s.io/api/core/v1" +) + +func TestGeneratePatches(t *testing.T) { + ws := &WebhookServer{} + pod := &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "test"}, + }, + }, + } + + patches, err := ws.generatePatches(pod, "arn:aws:iam::123456789012:role/test") + if err != nil { + t.Fatalf("Failed to generate patches: %v", err) + } + + if len(patches) == 0 { + t.Error("Expected patches to be generated") + } +} +``` + +### Building from Source + +```bash +go mod download +go build -o webhook main.go +``` + +## Architecture + +``` +┌─────────────┐ +│ Kubernetes │ +│ API Server │ +└──────┬──────┘ + │ + │ AdmissionReview Request + │ + ▼ +┌─────────────────┐ +│ IRSA Webhook │ +│ │ +│ 1. Parse Pod │ +│ 2. Get SA │◄────┐ +│ 3. Check Anno │ │ +│ 4. Gen Patches │ │ +└─────────────────┘ │ + │ + ┌─────┴──────┐ + │ K8s API │ + │ (Get SA) │ + └────────────┘ +``` + +## License + +MIT + +## Contributing + +Contributions welcome! Please ensure: +- Code follows Go best practices +- Add tests for new functionality +- Update documentation as needed \ No newline at end of file diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..b4832e4 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,390 @@ +# Troubleshooting Guide + +## Common Issues and Solutions + +### 1. Webhook Not Responding + +**Symptoms:** +- Pods fail to create with timeout errors +- Events show webhook timeout +- `kubectl get pods` hangs + +**Diagnosis:** +```bash +# Check webhook pod status +kubectl get pods -n irsa-system + +# View webhook logs +kubectl logs -n irsa-system -l app=irsa-webhook + +# Check webhook service +kubectl get svc -n irsa-system irsa-webhook +kubectl get endpoints -n irsa-system irsa-webhook +``` + +**Solutions:** + +1. **Webhook pods not running:** + ```bash + kubectl describe pods -n irsa-system -l app=irsa-webhook + # Fix image pull issues, resource constraints, etc. + ``` + +2. **TLS certificate issues:** + ```bash + # Regenerate certificates + ./generate-certs.sh + kubectl rollout restart deployment -n irsa-system irsa-webhook + ``` + +3. **Service not routing correctly:** + ```bash + # Check if service selectors match pod labels + kubectl get svc -n irsa-system irsa-webhook -o yaml + kubectl get pods -n irsa-system -l app=irsa-webhook --show-labels + ``` + +### 2. Pods Not Being Mutated + +**Symptoms:** +- Pods create successfully but don't have injected configuration +- Environment variables missing +- Volume not mounted + +**Diagnosis:** +```bash +# Check if ServiceAccount has annotation +kubectl get sa -o yaml | grep vultr.com/role-arn + +# Check webhook configuration +kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml + +# View webhook logs for the specific pod creation +kubectl logs -n irsa-system -l app=irsa-webhook --tail=100 +``` + +**Solutions:** + +1. **ServiceAccount annotation missing:** + ```bash + kubectl annotate sa \ + vultr.com/role-arn="arn:aws:iam::123456789012:role/your-role" + ``` + +2. **Namespace excluded from webhook:** + Check the `namespaceSelector` in the MutatingWebhookConfiguration: + ```bash + kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml + ``` + +3. **Webhook not receiving requests:** + ```bash + # Check webhook logs for incoming requests + kubectl logs -n irsa-system -l app=irsa-webhook --tail=50 + + # Verify webhook configuration matches service + kubectl get mutatingwebhookconfiguration irsa-webhook -o jsonpath='{.webhooks[0].clientConfig}' + ``` + +### 3. RBAC Permission Errors + +**Symptoms:** +- Webhook logs show "forbidden" or "unauthorized" errors +- Error fetching ServiceAccounts + +**Diagnosis:** +```bash +# Check webhook ServiceAccount permissions +kubectl auth can-i get serviceaccounts \ + --as=system:serviceaccount:irsa-system:irsa-webhook \ + --all-namespaces + +# View RBAC resources +kubectl get clusterrole irsa-webhook -o yaml +kubectl get clusterrolebinding irsa-webhook -o yaml +``` + +**Solutions:** + +1. **Missing RBAC permissions:** + ```bash + # Reapply RBAC configuration + kubectl apply -f deploy.yaml + ``` + +2. **ServiceAccount not bound to role:** + ```bash + kubectl get clusterrolebinding irsa-webhook -o yaml + # Verify subjects include the correct ServiceAccount + ``` + +### 4. TLS/Certificate Issues + +**Symptoms:** +- "x509: certificate signed by unknown authority" +- "TLS handshake error" +- Webhook returns 401 or 403 + +**Diagnosis:** +```bash +# Check certificate in secret +kubectl get secret -n irsa-system irsa-webhook-certs -o yaml + +# Verify CA bundle in webhook config +kubectl get mutatingwebhookconfiguration irsa-webhook \ + -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d +``` + +**Solutions:** + +1. **Regenerate certificates:** + ```bash + ./generate-certs.sh + ``` + +2. **Manually update CA bundle:** + ```bash + CA_BUNDLE=$(kubectl get secret -n irsa-system irsa-webhook-certs \ + -o jsonpath='{.data.ca\.crt}') + + kubectl patch mutatingwebhookconfiguration irsa-webhook \ + --type='json' \ + -p="[{'op': 'replace', 'path': '/webhooks/0/clientConfig/caBundle', 'value':'${CA_BUNDLE}'}]" + ``` + +3. **Verify certificate SANs:** + ```bash + kubectl get secret -n irsa-system irsa-webhook-certs \ + -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -text -noout + ``` + +### 5. AWS Credential Issues + +**Symptoms:** +- Pods can't authenticate with AWS +- "Unable to locate credentials" error +- "InvalidIdentityToken" error from AWS STS + +**Diagnosis:** +```bash +# Check injected environment variables +kubectl exec -- env | grep AWS + +# Verify token file exists +kubectl exec -- ls -la /var/run/secrets/vultr.com/serviceaccount/ + +# Check token contents (first 50 chars) +kubectl exec -- head -c 50 /var/run/secrets/vultr.com/serviceaccount/token + +# Test AWS STS +kubectl exec -- aws sts get-caller-identity +``` + +**Solutions:** + +1. **Token not mounted:** + - Verify pod has the volume and volume mount + - Check webhook logs for mutation + - Delete and recreate the pod + +2. **IAM role trust policy issue:** + Ensure your IAM role has the correct trust policy: + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/YOUR_OIDC_PROVIDER" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "YOUR_OIDC_PROVIDER:aud": "vultr" + } + } + } + ] + } + ``` + +3. **Wrong audience in token:** + - Verify the projected token has audience "vultr" + - Check webhook configuration uses correct tokenAudience constant + +### 6. Performance Issues + +**Symptoms:** +- Pod creation is slow +- Webhook timeout warnings +- High resource usage + +**Diagnosis:** +```bash +# Check webhook resource usage +kubectl top pods -n irsa-system + +# View webhook latency in logs +kubectl logs -n irsa-system -l app=irsa-webhook | grep "Processing pod" + +# Check for throttling +kubectl describe pods -n irsa-system -l app=irsa-webhook +``` + +**Solutions:** + +1. **Increase webhook timeout:** + ```bash + kubectl patch mutatingwebhookconfiguration irsa-webhook \ + --type='json' \ + -p='[{"op": "replace", "path": "/webhooks/0/timeoutSeconds", "value": 30}]' + ``` + +2. **Scale webhook deployment:** + ```bash + kubectl scale deployment -n irsa-system irsa-webhook --replicas=3 + ``` + +3. **Increase resource limits:** + Edit deploy.yaml and increase CPU/memory limits: + ```yaml + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 1000m + memory: 512Mi + ``` + +### 7. JSON Patch Generation Errors + +**Symptoms:** +- "Failed to generate patches" in webhook logs +- Malformed patch errors +- Array index out of bounds + +**Diagnosis:** +```bash +# Enable verbose logging (add to deployment) +# Set LOG_LEVEL=debug in environment + +# Check specific pod that failed +kubectl logs -n irsa-system -l app=irsa-webhook --tail=100 | grep -A 10 "Failed" +``` + +**Solutions:** + +1. **Review pod specification:** + - Ensure pod spec is valid JSON + - Check for unusual container configurations + +2. **Update webhook logic:** + - Fix any bugs in generatePatches function + - Add error handling for edge cases + +### 8. Multiple Webhooks Conflict + +**Symptoms:** +- Pod mutations from other webhooks interfering +- Unexpected pod configuration +- Volume/env var conflicts + +**Diagnosis:** +```bash +# List all mutating webhooks +kubectl get mutatingwebhookconfigurations + +# Check webhook order +kubectl get mutatingwebhookconfigurations -o yaml | grep -A 5 "name:" +``` + +**Solutions:** + +1. **Adjust webhook order:** + Webhooks are processed alphabetically by name. Rename if needed: + ```bash + # Add a prefix to control order + kubectl patch mutatingwebhookconfiguration irsa-webhook \ + --type='json' \ + -p='[{"op": "replace", "path": "/metadata/name", "value": "01-irsa-webhook"}]' + ``` + +2. **Add reinvocationPolicy:** + ```yaml + webhooks: + - name: irsa.vultr.com + reinvocationPolicy: IfNeeded # or Never + ``` + +## Debug Commands Cheat Sheet + +```bash +# View all webhook-related resources +kubectl get all -n irsa-system +kubectl get mutatingwebhookconfiguration irsa-webhook +kubectl get clusterrole irsa-webhook +kubectl get clusterrolebinding irsa-webhook + +# Test webhook directly +kubectl run test-pod --image=nginx --dry-run=client -o yaml | \ + kubectl create -f - --namespace=default + +# Watch webhook logs in real-time +kubectl logs -n irsa-system -l app=irsa-webhook -f + +# Check webhook pod health +kubectl get pods -n irsa-system -l app=irsa-webhook -o wide +kubectl describe pods -n irsa-system -l app=irsa-webhook + +# View recent events +kubectl get events -n irsa-system --sort-by='.lastTimestamp' + +# Test ServiceAccount annotation +kubectl get sa -A -o jsonpath='{range .items[?(@.metadata.annotations.vultr\.com/role-arn)]}{.metadata.namespace}{" "}{.metadata.name}{" "}{.metadata.annotations.vultr\.com/role-arn}{"\n"}{end}' + +# Validate webhook configuration +kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml | grep -E "(caBundle|service|path|port)" +``` + +## Getting Help + +If you're still experiencing issues: + +1. **Collect diagnostic information:** + ```bash + # Run this script and save output + kubectl get all -n irsa-system > diagnostics.txt + kubectl logs -n irsa-system -l app=irsa-webhook --tail=200 >> diagnostics.txt + kubectl get mutatingwebhookconfiguration irsa-webhook -o yaml >> diagnostics.txt + kubectl get events -n irsa-system >> diagnostics.txt + ``` + +2. **Check webhook version:** + ```bash + kubectl get deployment -n irsa-system irsa-webhook -o jsonpath='{.spec.template.spec.containers[0].image}' + ``` + +3. **Review logs with timestamps:** + ```bash + kubectl logs -n irsa-system -l app=irsa-webhook --timestamps=true --tail=100 + ``` + +4. **Test in isolation:** + - Create a separate test namespace + - Deploy a simple test pod + - Monitor webhook behavior + +## Prevention + +**Best Practices to Avoid Issues:** + +1. Always test in a non-production cluster first +2. Set `failurePolicy: Ignore` during initial deployment +3. Monitor webhook performance and logs +4. Keep certificates up to date (rotate every 365 days) +5. Use resource limits to prevent webhook from consuming too much +6. Implement readiness and liveness probes +7. Scale webhook deployment for high-traffic clusters +8. Document all ServiceAccount annotations diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000..3e4c244 --- /dev/null +++ b/example.yaml @@ -0,0 +1,139 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: irsa-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: irsa-webhook + namespace: irsa-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: irsa-webhook +rules: +- apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: irsa-webhook +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: irsa-webhook +subjects: +- kind: ServiceAccount + name: irsa-webhook + namespace: irsa-system +--- +apiVersion: v1 +kind: Service +metadata: + name: irsa-webhook + namespace: irsa-system +spec: + selector: + app: irsa-webhook + ports: + - port: 443 + targetPort: 8443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: irsa-webhook + namespace: irsa-system + labels: + app: irsa-webhook +spec: + replicas: 2 + selector: + matchLabels: + app: irsa-webhook + template: + metadata: + labels: + app: irsa-webhook + spec: + serviceAccountName: irsa-webhook + containers: + - name: webhook + image: your-registry/irsa-webhook:latest + imagePullPolicy: Always + ports: + - containerPort: 8443 + name: webhook + env: + - name: TLS_CERT_PATH + value: /etc/webhook/certs/tls.crt + - name: TLS_KEY_PATH + value: /etc/webhook/certs/tls.key + volumeMounts: + - name: webhook-certs + mountPath: /etc/webhook/certs + readOnly: true + livenessProbe: + httpGet: + path: /health + port: 8443 + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 8443 + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65532 + capabilities: + drop: + - ALL + volumes: + - name: webhook-certs + secret: + secretName: irsa-webhook-certs +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: irsa-webhook +webhooks: +- name: irsa.vultr.com + clientConfig: + service: + name: irsa-webhook + namespace: irsa-system + path: /mutate + caBundle: ${CA_BUNDLE} # Replace with base64-encoded CA certificate + rules: + - operations: ["CREATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + admissionReviewVersions: ["v1"] + sideEffects: None + timeoutSeconds: 10 + failurePolicy: Ignore # Change to Fail for production if needed + namespaceSelector: + matchExpressions: + - key: irsa-webhook + operator: NotIn + values: ["disabled"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..02156e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,57 @@ +module github.com/example/irsa-webhook + +go 1.24.0 + +require ( + k8s.io/api v0.34.3 + k8s.io/apimachinery v0.34.3 + k8s.io/client-go v0.34.3 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f46bed9 --- /dev/null +++ b/go.sum @@ -0,0 +1,167 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..92172f7 --- /dev/null +++ b/main.go @@ -0,0 +1,370 @@ +package main + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +const ( + roleArnAnnotation = "vultr.com/role-arn" + tokenVolumeName = "vultr-irsa-token" + tokenMountPath = "/var/run/secrets/vultr.com/serviceaccount" + tokenFileName = "token" + tokenAudience = "vultr" + envAWSRoleArn = "AWS_ROLE_ARN" + envAWSWebIdentityToken = "AWS_WEB_IDENTITY_TOKEN_FILE" + envAWSSTSRegionalEndpoint = "AWS_STS_REGIONAL_ENDPOINTS" +) + +var ( + tokenExpirationSeconds = int64(86400) // 24 hours +) + +type WebhookServer struct { + client *kubernetes.Clientset +} + +// JSONPatch represents a JSON Patch operation +type JSONPatch struct { + Op string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +func main() { + // Create in-cluster Kubernetes client + config, err := rest.InClusterConfig() + if err != nil { + log.Fatalf("Failed to create in-cluster config: %v", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatalf("Failed to create Kubernetes client: %v", err) + } + + server := &WebhookServer{ + client: clientset, + } + + // Setup HTTP handlers + mux := http.NewServeMux() + mux.HandleFunc("/mutate", server.handleMutate) + mux.HandleFunc("/health", handleHealth) + + // TLS configuration + tlsCertPath := getEnv("TLS_CERT_PATH", "/etc/webhook/certs/tls.crt") + tlsKeyPath := getEnv("TLS_KEY_PATH", "/etc/webhook/certs/tls.key") + port := getEnv("PORT", "8443") + + // Load TLS certificates + cert, err := tls.LoadX509KeyPair(tlsCertPath, tlsKeyPath) + if err != nil { + log.Fatalf("Failed to load TLS certificates: %v", err) + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + + httpServer := &http.Server{ + Addr: fmt.Sprintf(":%s", port), + TLSConfig: tlsConfig, + Handler: mux, + } + + log.Printf("Starting webhook server on port %s", port) + if err := httpServer.ListenAndServeTLS("", ""); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} + +func (ws *WebhookServer) handleMutate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Read request body + body, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("Failed to read request body: %v", err) + http.Error(w, "Failed to read request", http.StatusBadRequest) + return + } + defer r.Body.Close() + + // Parse AdmissionReview request + admissionReview := &admissionv1.AdmissionReview{} + if err := json.Unmarshal(body, admissionReview); err != nil { + log.Printf("Failed to unmarshal admission review: %v", err) + http.Error(w, "Failed to parse admission review", http.StatusBadRequest) + return + } + + // Validate request + if admissionReview.Request == nil { + log.Printf("Admission review request is nil") + http.Error(w, "Invalid admission review request", http.StatusBadRequest) + return + } + + // Process the request and create response + admissionResponse := ws.mutate(admissionReview.Request) + + // Create response AdmissionReview + responseAdmissionReview := &admissionv1.AdmissionReview{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "admission.k8s.io/v1", + Kind: "AdmissionReview", + }, + Response: admissionResponse, + } + responseAdmissionReview.Response.UID = admissionReview.Request.UID + + // Marshal and send response + responseBytes, err := json.Marshal(responseAdmissionReview) + if err != nil { + log.Printf("Failed to marshal admission response: %v", err) + http.Error(w, "Failed to create response", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(responseBytes) +} + +func (ws *WebhookServer) mutate(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { + // Create base response + response := &admissionv1.AdmissionResponse{ + Allowed: true, + } + + // Only handle Pod objects + if request.Kind.Kind != "Pod" { + log.Printf("Skipping non-Pod object: %s", request.Kind.Kind) + return response + } + + // Parse Pod from request + pod := &corev1.Pod{} + if err := json.Unmarshal(request.Object.Raw, pod); err != nil { + log.Printf("Failed to unmarshal pod: %v", err) + response.Allowed = false + response.Result = &metav1.Status{ + Message: fmt.Sprintf("Failed to parse pod: %v", err), + } + return response + } + + log.Printf("Processing pod: %s/%s, ServiceAccount: %s", + request.Namespace, pod.Name, pod.Spec.ServiceAccountName) + + // Get ServiceAccount name (default to "default" if not specified) + serviceAccountName := pod.Spec.ServiceAccountName + if serviceAccountName == "" { + serviceAccountName = "default" + } + + // Fetch ServiceAccount from Kubernetes API + serviceAccount, err := ws.client.CoreV1().ServiceAccounts(request.Namespace).Get( + context.Background(), + serviceAccountName, + metav1.GetOptions{}, + ) + if err != nil { + log.Printf("Failed to fetch ServiceAccount %s/%s: %v", + request.Namespace, serviceAccountName, err) + // Don't block pod creation if we can't fetch the SA + return response + } + + // Check for role ARN annotation + roleArn, exists := serviceAccount.Annotations[roleArnAnnotation] + if !exists || roleArn == "" { + log.Printf("ServiceAccount %s/%s does not have %s annotation, skipping mutation", + request.Namespace, serviceAccountName, roleArnAnnotation) + return response + } + + log.Printf("Found role ARN annotation: %s", roleArn) + + // Generate JSON patches to mutate the pod + patches, err := ws.generatePatches(pod, roleArn) + if err != nil { + log.Printf("Failed to generate patches: %v", err) + response.Allowed = false + response.Result = &metav1.Status{ + Message: fmt.Sprintf("Failed to generate patches: %v", err), + } + return response + } + + // Marshal patches to JSON + patchBytes, err := json.Marshal(patches) + if err != nil { + log.Printf("Failed to marshal patches: %v", err) + response.Allowed = false + response.Result = &metav1.Status{ + Message: fmt.Sprintf("Failed to marshal patches: %v", err), + } + return response + } + + response.Patch = patchBytes + patchType := admissionv1.PatchTypeJSONPatch + response.PatchType = &patchType + + log.Printf("Successfully generated %d patches for pod %s/%s", + len(patches), request.Namespace, pod.Name) + + return response +} + +func (ws *WebhookServer) generatePatches(pod *corev1.Pod, roleArn string) ([]JSONPatch, error) { + var patches []JSONPatch + + // 1. Add projected volume for service account token + tokenVolume := corev1.Volume{ + Name: tokenVolumeName, + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + ServiceAccountToken: &corev1.ServiceAccountTokenProjection{ + Audience: tokenAudience, + ExpirationSeconds: &tokenExpirationSeconds, + Path: tokenFileName, + }, + }, + }, + }, + }, + } + + // Check if volumes array exists + if pod.Spec.Volumes == nil { + // Initialize volumes array + patches = append(patches, JSONPatch{ + Op: "add", + Path: "/spec/volumes", + Value: []corev1.Volume{tokenVolume}, + }) + } else { + // Append to existing volumes + patches = append(patches, JSONPatch{ + Op: "add", + Path: "/spec/volumes/-", + Value: tokenVolume, + }) + } + + // 2. Process each container + for i := range pod.Spec.Containers { + containerPatches := ws.generateContainerPatches(i, roleArn, &pod.Spec.Containers[i]) + patches = append(patches, containerPatches...) + } + + // 3. Process init containers if they exist + for i := range pod.Spec.InitContainers { + containerPatches := ws.generateContainerPatches(i, roleArn, &pod.Spec.InitContainers[i]) + // Adjust paths for init containers + for j := range containerPatches { + containerPatches[j].Path = "/spec/initContainers" + + containerPatches[j].Path[len("/spec/containers"):] + } + patches = append(patches, containerPatches...) + } + + return patches, nil +} + +func (ws *WebhookServer) generateContainerPatches(index int, roleArn string, container *corev1.Container) []JSONPatch { + var patches []JSONPatch + basePath := fmt.Sprintf("/spec/containers/%d", index) + + // Add volume mount + volumeMount := corev1.VolumeMount{ + Name: tokenVolumeName, + MountPath: tokenMountPath, + ReadOnly: true, + } + + if container.VolumeMounts == nil { + // Initialize volume mounts array + patches = append(patches, JSONPatch{ + Op: "add", + Path: basePath + "/volumeMounts", + Value: []corev1.VolumeMount{volumeMount}, + }) + } else { + // Append to existing volume mounts + patches = append(patches, JSONPatch{ + Op: "add", + Path: basePath + "/volumeMounts/-", + Value: volumeMount, + }) + } + + // Add environment variables + envVars := []corev1.EnvVar{ + { + Name: envAWSRoleArn, + Value: roleArn, + }, + { + Name: envAWSWebIdentityToken, + Value: fmt.Sprintf("%s/%s", tokenMountPath, tokenFileName), + }, + { + Name: envAWSSTSRegionalEndpoint, + Value: "regional", + }, + } + + if container.Env == nil { + // Initialize env array + patches = append(patches, JSONPatch{ + Op: "add", + Path: basePath + "/env", + Value: envVars, + }) + } else { + // Append to existing env vars + for _, envVar := range envVars { + patches = append(patches, JSONPatch{ + Op: "add", + Path: basePath + "/env/-", + Value: envVar, + }) + } + } + + return patches +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +}