Skip to content

Commit

Permalink
fix merge conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
eumel8 committed Sep 4, 2023
2 parents 398de12 + 57ef1ab commit 74ba575
Show file tree
Hide file tree
Showing 5,835 changed files with 1,034 additions and 1,670,853 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
cosignwebhook
grumpywebhook
chart/caas-values.yaml
vendor/
8 changes: 3 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# build stage
FROM golang:1.21 AS build-env
RUN mkdir -p /go/src/github.com/eumel8/cosignwebhook
WORKDIR /go/src/github.com/eumel8/cosignwebhook
COPY . .
WORKDIR /app
COPY . /app
RUN useradd -u 10001 webhook
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o cosignwebhook

#FROM scratch
FROM alpine:latest
COPY --from=build-env /go/src/github.com/eumel8/cosignwebhook/cosignwebhook .
COPY --from=build-env /app .
COPY --from=build-env /etc/passwd /etc/passwd
USER webhook
ENTRYPOINT ["/cosignwebhook"]
69 changes: 50 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@ Kubernetes Validation Admission Controller to verify Cosign Image signatures.

<img src="cosignwebhook.png" alt="cosignwebhook" width="680"/>

Watch POD creating in deployments, looking for the first container image and a present RSA publik key to verify.
This webhook watches for pod creation in deployments and verifies the first container image it finds with an existing
RSA public key (if present).

# Installation with Helm

```bash
helm -n cosignwebhook upgrade -i cosignwebhook oci://ghcr.io/eumel8/charts/cosignwebhook --versi
on 2.0.0 --create-namespace
on 3.0.0 --create-namespace
```

this installation has some advantages:

* auto generate TLS key pair
* setup ServiceMonitor and GrafanaDashboard
* automatic generation of TLS key pair
* automatic setup of ServiceMonitor and Grafana dashboards

If you use your own image, you'll have to sign it first. Don't forget to change the `cosign.scwebhook.key` value to your
public key, used to sign the image.

# Installation with manifest

As Cluster Admin create a namespace and install the Admission Controller:
As cluster admin, create a namespace and install the admission controller:

```bash
kubectl create namespace cosignwebhook
Expand All @@ -36,39 +40,64 @@ generate-certs.sh --service cosignwebhook --webhook cosignwebhook --namespace co

# Usage

To use the webhook, you need to first sign your images with cosign, and then use **one** of the following validation
possibilities:

## Public key as environment variable

Add your Cosign public key as env var in container spec of the first container:

```yaml
env:
- name: COSIGNPUBKEY
value: |
- name: COSIGNPUBKEY
value: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGOrnlJ1lFxAFTY2LF1vCuVHNZr9H
QryRDinn+JhPrDYR2wqCP+BUkeWja+RWrRdmskA0AffxBzaQrN/SwZI6fA==
-----END PUBLIC KEY-----
```
or create a secret and reference it in the deployment
## Public key as secret reference
Instead of hardcoding the public key in the deployment, you can also use a secret reference. The key and the secret may
be named freely, as long as the secret contains a valid public key.
```yaml
apiVersion: v1
kind: Secret
data:
COSIGNPUBKEY: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFS1BhWUhnZEVEQ3ltcGx5emlIdkJ5UjNxRkhZdgppaWxlMCtFMEtzVzFqWkhJa1p4UWN3aGsySjNqSm5VdTdmcjcrd05DeENkVEdYQmhBSTJveE1LbWx3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tkind: Secret
COSIGNPUBKEY: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFS1BhWUhnZEVEQ3ltcGx5emlIdkJ5UjNxRkhZdgppaWxlMCtFMEtzVzFqWkhJa1p4UWN3aGsySjNqSm5VdTdmcjcrd05DeENkVEdYQmhBSTJveE1LbWx3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t
metadata:
name: cosignwebhook
type: Opaque
```
```yaml
env:
- name: COSIGNPUBKEY
valueFrom:
secretKeyRef:
name: cosignwebhook
key: COSIGNPUBKEY
- name: COSIGNPUBKEY
valueFrom:
secretKeyRef:
name: cosignwebhook
key: COSIGNPUBKEY
```
Note: The secret MUST be named `cosignwebhook` and the data values MIST be names `COSIGNPUBKEY`
## Public key as default secret for namespace
Create a default secret for all your images in a namespace, which the webhook will always search for, when validating
images in this namespace:
```yaml
apiVersion: v1
kind: Secret
data:
COSIGNPUBKEY: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFS1BhWUhnZEVEQ3ltcGx5emlIdkJ5UjNxRkhZdgppaWxlMCtFMEtzVzFqWkhJa1p4UWN3aGsySjNqSm5VdTdmcjcrd05DeENkVEdYQmhBSTJveE1LbWx3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t
metadata:
name: cosignwebhook
type: Opaque
```
The name of the secret must be `cosignwebhook` and the key `COSIGNPUBKEY`. The value of `COSIGNPUBKEY` must match the
public key used to sign the image you're deploying.

# Test

Expand All @@ -81,19 +110,21 @@ kubectl -n cosignwebhook apply -f manifests/demoapp.yaml

# TODO

* Support private images [x]
* Support multiple container/keys
* [x] Support private images
* [x] Support multiple container/keys

## local build
# Local build

```bash
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o cosignwebhook
```

## Credits

Frank Kloeker f.kloeker@telekom.de

Life is for sharing. If you have an issue with the code or want to improve it, feel free to open an issue or an pull request.
Life is for sharing. If you have an issue with the code or want to improve it, feel free to open an issue or an pull
request.

The Operator is inspired by [@pipo02mix](https://github.com/pipo02mix/grumpy), a good place
to learn fundamental things about Admission Controllert
4 changes: 2 additions & 2 deletions chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: cosignwebhook
description: A Helm chart for Cosign Webhook Admission Controller
type: application
version: 3.0.0
appVersion: "3.0.0"
version: 3.1.0
appVersion: "3.1.0"
maintainers:
- name: eumel8
email: f.kloeker@telekom.de
Expand Down
86 changes: 50 additions & 36 deletions cosignwebhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"time"

log "github.com/gookit/slog"
v1 "k8s.io/api/admission/v1"
Expand All @@ -23,8 +24,8 @@ import (
"github.com/google/go-containerregistry/pkg/authn/k8schain"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"github.com/sigstore/cosign/v2/pkg/cosign"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"

"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
Expand All @@ -39,7 +40,19 @@ const (
// CosignServerHandler listen to admission requests and serve responses
// build certs here: https://raw.githubusercontent.com/openshift/external-dns-operator/fb77a3c547a09cd638d4e05a7b8cb81094ff2476/hack/generate-certs.sh
// generate-certs.sh --service cosignwebhook --webhook cosignwebhook --namespace cosignwebhook --secret cosignwebhook
type CosignServerHandler struct{}
type CosignServerHandler struct {
cs kubernetes.Interface
}

func NewCosignServerHandler() *CosignServerHandler {
cs, err := restClient()
if err != nil {
log.Errorf("Can't init rest client: %v", err)
}
return &CosignServerHandler{
cs: cs,
}
}

// create restClient for get secrets and create events
func restClient() (*kubernetes.Clientset, error) {
Expand Down Expand Up @@ -84,50 +97,52 @@ func getPod(byte []byte) (*corev1.Pod, *v1.AdmissionReview, error) {
return &pod, &arRequest, nil
}

// get pubKey from Env
func getEnv(pod *corev1.Pod) (string, error) {
// getPubKeyFromEnv grabs the public key from Pod's environment
func (csh *CosignServerHandler) getPubKeyFromEnv(pod *corev1.Pod) (string, error) {
for i := 0; i < len(pod.Spec.Containers[0].Env); i++ {
value := pod.Spec.Containers[0].Env[i].Value
if pod.Spec.Containers[0].Env[i].Name == cosignEnvVar {
return value, nil

if len(pod.Spec.Containers[0].Env[i].Value) != 0 {
log.Debugf("Found public key in env var %s/%s", pod.Namespace, pod.Name)
return pod.Spec.Containers[0].Env[i].Value, nil
}

if pod.Spec.Containers[0].Env[i].ValueFrom.SecretKeyRef != nil {
log.Debugf("Found public key in secret of %s/%s", pod.Namespace, pod.Name)
return csh.getSecretValue(pod.Namespace,
pod.Spec.Containers[0].Env[i].ValueFrom.SecretKeyRef.Name,
pod.Spec.Containers[0].Env[i].ValueFrom.SecretKeyRef.Key,
)
}
}
}
return "", fmt.Errorf("no env var found %s/%s", pod.Namespace, pod.Name)
return "", fmt.Errorf("no env var found in %s/%s", pod.Namespace, pod.Name)
}

// get pubKey Secrets value by given name with kubernetes in-cluster client
func getSecret(namespace string, name string) (string, error) {
clientset, err := restClient()
if err != nil {
log.Errorf("Can't init rest client for secret: %v", err)
return "", err
}
secret, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
// getSecretValue returns the value of passed key for the secret with passed name in passed namespace
func (csh *CosignServerHandler) getSecretValue(namespace string, name string, key string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
secret, err := csh.cs.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
log.Debugf("Can't get secret %s/%s : %v", namespace, name, err)
return "", err
}
value := secret.Data[cosignEnvVar]
if value == nil {
log.Debugf("Secret value empty for %s/%s", namespace, name)
value := secret.Data[key]
if len(value) == 0 {
log.Errorf("Secret value of %q is empty for %s/%s", key, namespace, name)
return "", nil
}
/*
decodedValue, err := base64.StdEncoding.DecodeString(string(value))
if err != nil {
log.Errorf("Can't decode value ", err)
return "", err
}
*/
log.Debugf("Found public key in secret %s/%s, value: %s", namespace, name, value)
return string(value), nil
}

func (cs *CosignServerHandler) healthz(w http.ResponseWriter, r *http.Request) {
func (csh *CosignServerHandler) healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}

func (cs *CosignServerHandler) serve(w http.ResponseWriter, r *http.Request) {
func (csh *CosignServerHandler) serve(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
if data, err := io.ReadAll(r.Body); err == nil {
Expand Down Expand Up @@ -165,24 +180,25 @@ func (cs *CosignServerHandler) serve(w http.ResponseWriter, r *http.Request) {
}

// Get public key from environment var
pubKey, err := getEnv(pod)
pubKey, err := csh.getPubKeyFromEnv(pod)
if err != nil {
log.Debugf("Could not get public key from environment variable in %s/%s: %v. Trying to get public key from secret", pod.Namespace, pod.Name, err)
}

// If no public key get here, try to load from secret
// If no public key get here, try to load default secret
if len(pubKey) == 0 {
pubKey, err = getSecret(pod.Namespace, "cosignwebhook")
pubKey, err = csh.getSecretValue(pod.Namespace, "cosignwebhook", cosignEnvVar)
if err != nil {
log.Debugf("Could not get public key from secret in %s/%s: %v", pod.Namespace, pod.Name, err)
}
}

// Still no public key, we don't care. Otherwise POD won't start, if we return with 403
// Still no public key, we don't care. Otherwise, POD won't start if we return with 403
if len(pubKey) == 0 {
// log.Errorf("No public key set in %s/%s", pod.Namespace, pod.Name)
// return OK if no key is set, so user don't want a verification
// otherwise set failurePolicy: Skip in ValidatingWebhookConfiguration
log.Debugf("No public key found for %s/%s, skipping verification", pod.Namespace, pod.Name)
resp, err := json.Marshal(admissionResponse(200, true, "Success", "Cosign image skipped", arRequest))
if err != nil {
log.Errorf("Can't encode response: %v", err)
Expand All @@ -194,7 +210,6 @@ func (cs *CosignServerHandler) serve(w http.ResponseWriter, r *http.Request) {
}
return
}
// log.Info("Successfully got public key")

// Lookup image name of first container
image := pod.Spec.Containers[0].Image
Expand Down Expand Up @@ -280,9 +295,8 @@ func (cs *CosignServerHandler) serve(w http.ResponseWriter, r *http.Request) {
&cosign.CheckOpts{
RegistryClientOpts: remoteOpts,
SigVerifier: cosignLoadKey,
// add settings for cosign 2.0
// IgnoreSCT: true,
// SkipTlogVerify: true,
IgnoreSCT: true,
IgnoreTlog: true,
})

// this is always false,
Expand All @@ -303,7 +317,7 @@ func (cs *CosignServerHandler) serve(w http.ResponseWriter, r *http.Request) {
} else {
// count successful verifies for prometheus metric
verifiedProcessed.Inc()
log.Infof("Image successful verified: %s/%s", pod.Namespace, pod.Name)
log.Infof("Image verified successfully: %s/%s", pod.Namespace, pod.Name)
resp, err := json.Marshal(admissionResponse(200, true, "Success", "Cosign image verified", arRequest))
// Verify Image successful, needs to allow pod start
if err != nil {
Expand Down
Loading

0 comments on commit 74ba575

Please sign in to comment.