A Kubernetes Validating Admission Webhook built with Bun that validates container images exist in registries and replicates them to a target registry before allowing pods to be created.
Your dev cluster isn't air-gapped but you want to catch all images being deployed, even when hardcoded in system components like monitoring agents, CNI plugins, or operators. Image Replicator logs every image reference passing through the cluster, giving you a complete inventory.
Replicate all container images to your internal registry before pods can start. Perfect for secure environments where external registry access is blocked by firewall rules or network policies.
- Image Existence Validation: Verifies all container images exist in their registries before pods are scheduled
- Multi-Registry Support: Works with Docker Hub, GitHub Container Registry (GHCR), Google Container Registry (GCR), Amazon ECR, Azure ACR, and any OCI-compliant registry
- Authentication: Supports username/password and token-based authentication
- Comprehensive Coverage: Validates images in:
- Pods
- Deployments
- StatefulSets
- DaemonSets
- ReplicaSets
- Jobs
- CronJobs
- Init containers
- Ephemeral containers
- Kubernetes cluster (1.19+)
kubectlconfigured to access your cluster- Docker (for building the image)
- Bun (for local development)
bun installEdit deploy/secret-credentials.yaml with your registry credentials:
apiVersion: v1
kind: Secret
metadata:
name: registry-credentials
namespace: image-replicator
type: Opaque
stringData:
# Option 1: Docker config.json format
DOCKER_CONFIG_JSON: |
{
"auths": {
"docker.io": {
"username": "your-username",
"password": "your-password-or-token"
},
"ghcr.io": {
"username": "your-github-username",
"password": "your-github-pat"
}
}
}
# Option 2: Individual registry credentials
REGISTRY_DOCKERHUB_URL: "docker.io"
REGISTRY_DOCKERHUB_USERNAME: "your-username"
REGISTRY_DOCKERHUB_PASSWORD: "your-password"There are two deployment options:
cert-manager handles TLS certificate generation and rotation automatically.
# Make scripts executable
chmod +x scripts/*.sh
# Deploy with cert-manager (will install cert-manager if not present)
./scripts/deploy-cert-manager.sh# Make scripts executable
chmod +x scripts/*.sh
# Deploy to Kubernetes (generates self-signed certs)
./scripts/deploy.sh# This should be DENIED (image doesn't exist)
kubectl run test-invalid --image=this-image-does-not-exist:v999
# This should be ALLOWED (image exists)
kubectl run test-valid --image=nginx:latest| Variable | Description | Default |
|---|---|---|
PORT |
Webhook HTTPS port | 8443 |
HEALTH_PORT |
Health check HTTP port | 8080 |
TLS_CERT_PATH |
Path to TLS certificate | /certs/tls.crt |
TLS_KEY_PATH |
Path to TLS private key | /certs/tls.key |
SKIP_TLS |
Disable TLS (development only) | false |
Set DOCKER_CONFIG_JSON environment variable:
{
"auths": {
"registry.example.com": {
"username": "user",
"password": "pass"
}
}
}REGISTRY_<NAME>_URL=registry.example.com
REGISTRY_<NAME>_USERNAME=user
REGISTRY_<NAME>_PASSWORD=pass
# or
REGISTRY_<NAME>_TOKEN=tokenUsed when no specific registry match is found:
DEFAULT_REGISTRY_URL=docker.io
DEFAULT_REGISTRY_USERNAME=user
DEFAULT_REGISTRY_PASSWORD=passsequenceDiagram
participant User
participant K8s as Kubernetes API
participant Webhook as Image Replicator
participant Source as Source Registry<br/>(Docker Hub, GCR, etc.)
participant Target as Target Registry<br/>(ACR, ECR, Harbor, etc.)
User->>K8s: Create Pod/Deployment
K8s->>Webhook: AdmissionReview Request
Note over Webhook: Extract container images<br/>from Pod spec
loop For each image
Webhook->>Source: Check image exists
Source-->>Webhook: Image found
Webhook->>Target: Check if already cloned
alt Image exists in target
Target-->>Webhook: Already exists
else Image missing in target
Target-->>Webhook: Not found
Note over Webhook,Target: Clone image
Webhook->>Source: GET manifest & layers
Source-->>Webhook: Image data
Webhook->>Target: PUT manifest & layers
Target-->>Webhook: Image pushed
Note over Webhook: Increment clone metrics
end
end
Webhook->>K8s: AdmissionReview Response (Allow)
K8s->>User: Pod created successfully
Note over Webhook: Expose Prometheus metrics<br/>at /metrics endpoint
To skip validation for specific resources, add the label:
metadata:
labels:
image-replicator.io/skip: "true"To skip validation for entire namespaces, the webhook is configured to ignore:
kube-systemkube-publickube-node-leaseimage-replicator
# Generate development certificates
./scripts/generate-certs.sh
# Run with TLS
TLS_CERT_PATH=./certs/tls.crt TLS_KEY_PATH=./certs/tls.key bun run dev
# Run without TLS (development only)
SKIP_TLS=true bun run dev# Send a test admission review
curl -X POST https://localhost:8443/validate \
-H "Content-Type: application/json" \
-k \
-d '{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "test-123",
"kind": {"group": "", "version": "v1", "kind": "Pod"},
"resource": {"group": "", "version": "v1", "resource": "pods"},
"requestKind": {"group": "", "version": "v1", "kind": "Pod"},
"requestResource": {"group": "", "version": "v1", "resource": "pods"},
"namespace": "default",
"operation": "CREATE",
"userInfo": {"username": "admin", "uid": "1", "groups": []},
"object": {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name": "test-pod"},
"spec": {
"containers": [
{"name": "nginx", "image": "nginx:latest"}
]
}
},
"dryRun": false
}
}'┌─────────────────────────────────────────────────────────────┐
│ Kubernetes API Server │
└─────────────────────────────────────────────────────────────┘
│
│ AdmissionReview
▼
┌─────────────────────────────────────────────────────────────┐
│ ValidatingWebhookConfiguration │
│ │
│ Rules: │
│ - Pods, Deployments, Jobs, CronJobs, etc. │
│ - CREATE and UPDATE operations │
└─────────────────────────────────────────────────────────────┘
│
│ HTTPS POST /validate
▼
┌─────────────────────────────────────────────────────────────┐
│ Image Validator Webhook (Bun) │
│ │
│ 1. Extract images from object spec │
│ 2. For each image: │
│ - Parse registry, repository, tag │
│ - Authenticate with registry │
│ - Check if manifest exists │
│ 3. Return allow/deny response │
└─────────────────────────────────────────────────────────────┘
│
│ Registry API v2
▼
┌─────────────────────────────────────────────────────────────┐
│ Container Registries │
│ │
│ Docker Hub │ GHCR │ GCR │ ECR │ ACR │ Custom │
└─────────────────────────────────────────────────────────────┘
.
├── src/
│ ├── index.ts # Main entry point, webhook server
│ ├── types/
│ │ ├── index.ts
│ │ ├── kubernetes.ts # Kubernetes type definitions
│ │ └── registry.ts # Registry type definitions
│ ├── services/
│ │ ├── index.ts
│ │ └── registry-client.ts # Registry API client
│ ├── handlers/
│ │ ├── index.ts
│ │ └── admission.ts # Admission review handler
│ └── utils/
│ ├── index.ts
│ ├── image-parser.ts # Image reference parser
│ └── credentials.ts # Credential loading
├── deploy/
│ ├── namespace.yaml
│ ├── deployment.yaml
│ ├── secret-credentials.yaml
│ ├── webhook-config.yaml
│ └── cert-manager/ # cert-manager based deployment
│ ├── kustomization.yaml
│ ├── issuer.yaml # CA issuer configuration
│ ├── certificate.yaml # Webhook TLS certificate
│ └── webhook-config.yaml # Webhook config with CA injection
├── scripts/
│ ├── generate-certs.sh # Manual TLS certificate generation
│ ├── deploy.sh # Manual deployment script
│ └── deploy-cert-manager.sh # cert-manager deployment script
├── Dockerfile
├── package.json
├── tsconfig.json
└── README.md
-
Check webhook configuration:
kubectl get validatingwebhookconfigurations image-replicator -o yaml
-
Verify CA bundle is set correctly
-
Check webhook pod logs:
kubectl logs -l app=image-replicator -n image-replicator -f
- Verify credentials are correctly set in the secret
- Check if the registry requires specific authentication (e.g., GCR needs
_json_keyas username) - Test credentials manually:
docker login registry.example.com
-
Check certificate status:
kubectl get certificate -n image-replicator kubectl describe certificate image-replicator-cert -n image-replicator
-
Check if CA bundle was injected:
kubectl get validatingwebhookconfiguration image-replicator -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d
-
Check cert-manager logs:
kubectl logs -l app=cert-manager -n cert-manager kubectl logs -l app=cainjector -n cert-manager
-
Force certificate renewal:
kubectl delete certificate image-replicator-cert -n image-replicator kubectl apply -k deploy/cert-manager
-
Regenerate certificates:
./scripts/generate-certs.sh
-
Update the webhook configuration with new CA bundle
-
Restart webhook pods:
kubectl rollout restart deployment/image-replicator -n image-replicator
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
# Fork and clone
git clone https://github.com/YOUR_USERNAME/image-replicator.git
cd image-replicator
# Install dependencies
bun install
# Run tests
bun test
# Type check
bun run type-check
# Make your changes and submit a PRThis project follows the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.
See SECURITY.md for our security policy and how to report vulnerabilities.
- Built with Bun for high performance
- Inspired by Kubernetes best practices and CNCF projects
- Thanks to all contributors
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Copyright 2025 Martin Slanina
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.