Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ kubectl apply -f deploy/apiservice.yaml

## ⚙️ Configuration

> **⚠️ Important:** Make sure the `groupName` in your Helm values/deployment matches **exactly** the `groupName` in your ClusterIssuer. This is a common source of configuration errors.

### 1. Create a ClusterIssuer

```yaml
Expand Down Expand Up @@ -117,6 +119,9 @@ spec:
| `dialTimeout` | Connection timeout (seconds) | no | `10` |
| `tlsSecretRef` | Name of Kubernetes secret containing TLS certs | no | - |
| `tlsSecretNamespace` | Namespace of the TLS secret | no | challenge namespace |
| `tlsCAKey` | Key name for CA certificate in the secret | no | `ca.crt` |
| `tlsCertKey` | Key name for client certificate in the secret | no | `tls.crt` |
| `tlsKeyKey` | Key name for client private key in the secret | no | `tls.key` |
| `tlsInsecureSkipVerify` | Skip TLS verification (not recommended) | no | `false` |

## 🔐 TLS Configuration
Expand Down Expand Up @@ -261,23 +266,28 @@ REGISTRY=ghcr.io/your-org make docker-push

## 🐛 Troubleshooting

### DNS challenge not resolving
**⚠️ Important:** The `groupName` must match exactly in:
1. Helm values (or deployment ENV)
2. ClusterIssuer `webhook.groupName` field

1. Verify that etcd is accessible from the webhook
2. Check the prefix in the configuration
3. Verify that CoreDNS uses the same prefix
Common error: `"failed to load config: config is required"` - See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed debugging steps.

### Authentication error
### Quick Checks

Check the etcd credentials in the Issuer configuration.
```bash
# Verify groupName matches everywhere
kubectl get deployment cert-manager-webhook-etcd -n cert-manager -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="GROUP_NAME")].value}'
kubectl get clusterissuer letsencrypt-prod -o yaml | grep groupName

### Webhook not starting
# Check APIService status
kubectl get apiservice | grep acme

```bash
kubectl describe pod -n cert-manager -l app.kubernetes.io/name=cert-manager-webhook-etcd
# Check webhook logs
kubectl logs -n cert-manager -l app.kubernetes.io/name=cert-manager-webhook-etcd
```

For detailed troubleshooting, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md).

## 📄 License

Apache License 2.0
Expand Down
10 changes: 9 additions & 1 deletion deploy/examples/issuer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,17 @@ spec:
# TLS configuration - reference to a Kubernetes secret
tlsSecretRef: "etcd-tls-certs"
tlsSecretNamespace: "etcd"
# Optional: Custom key names in the TLS secret (defaults shown)
# tlsCAKey: "ca.crt" # Key name for CA certificate
# tlsCertKey: "tls.crt" # Key name for client certificate
# tlsKeyKey: "tls.key" # Key name for client private key
# Example with custom key names:
# tlsCAKey: "etcd-ca.crt"
# tlsCertKey: "etcd-server.crt"
# tlsKeyKey: "etcd-server.key"
---
# Example TLS Secret for etcd connection
# The secret should contain:
# The secret should contain (key names are configurable via tlsCAKey, tlsCertKey, tlsKeyKey):
# - ca.crt: CA certificate to verify etcd server
# - tls.crt: Client certificate (for mTLS)
# - tls.key: Client private key (for mTLS)
Expand Down
36 changes: 29 additions & 7 deletions pkg/solver/solver.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ type EtcdConfig struct {
TLSSecretRef string `json:"tlsSecretRef,omitempty"`
// TLSSecretNamespace is the namespace of the TLS secret (defaults to challenge namespace)
TLSSecretNamespace string `json:"tlsSecretNamespace,omitempty"`
// TLSCAKey is the key name for CA certificate in the secret (default: ca.crt)
TLSCAKey string `json:"tlsCAKey,omitempty"`
// TLSCertKey is the key name for client certificate in the secret (default: tls.crt)
TLSCertKey string `json:"tlsCertKey,omitempty"`
// TLSKeyKey is the key name for client private key in the secret (default: tls.key)
TLSKeyKey string `json:"tlsKeyKey,omitempty"`
// TLSInsecureSkipVerify skips TLS certificate verification (not recommended for production)
TLSInsecureSkipVerify bool `json:"tlsInsecureSkipVerify,omitempty"`
// TLSCA is the CA certificate in PEM format (alternative to using a secret)
Expand Down Expand Up @@ -300,29 +306,45 @@ func (e *EtcdDNSSolver) loadTLSConfigFromSecret(cfg *EtcdConfig, ch *v1alpha1.Ch
InsecureSkipVerify: cfg.TLSInsecureSkipVerify,
}

// Determine key names (use defaults if not specified)
caKey := cfg.TLSCAKey
if caKey == "" {
caKey = "ca.crt"
}
certKey := cfg.TLSCertKey
if certKey == "" {
certKey = "tls.crt"
}
keyKey := cfg.TLSKeyKey
if keyKey == "" {
keyKey = "tls.key"
}

klog.V(2).Infof("Using TLS secret keys: CA=%s, Cert=%s, Key=%s", caKey, certKey, keyKey)

// Load CA certificate if present
if caCert, ok := secret.Data["ca.crt"]; ok {
if caCert, ok := secret.Data[caKey]; ok {
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to parse CA certificate")
return nil, fmt.Errorf("failed to parse CA certificate from key '%s'", caKey)
}
tlsConfig.RootCAs = caCertPool
klog.V(2).Info("CA certificate loaded from secret")
klog.V(2).Infof("CA certificate loaded from secret key '%s'", caKey)
}

// Load client certificate and key if present (for mTLS)
clientCert, certOk := secret.Data["tls.crt"]
clientKey, keyOk := secret.Data["tls.key"]
clientCert, certOk := secret.Data[certKey]
clientKey, keyOk := secret.Data[keyKey]

if certOk && keyOk {
cert, err := tls.X509KeyPair(clientCert, clientKey)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate and key: %v", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
klog.V(2).Info("Client certificate and key loaded from secret")
klog.V(2).Infof("Client certificate and key loaded from secret keys '%s' and '%s'", certKey, keyKey)
} else if certOk || keyOk {
return nil, fmt.Errorf("both tls.crt and tls.key must be present in the secret for client authentication")
return nil, fmt.Errorf("both '%s' and '%s' must be present in the secret for client authentication", certKey, keyKey)
}

return tlsConfig, nil
Expand Down