Repository: sse-secure-systems/connaisseur (go)

Issue:
enable redis as optional feature
**Describe the feature**
Since 3.4.0 the a redis is shipped by default and can not be disabled. 
In my opinion it should be possible to disable the redis completely or at least use an external redis.

**Optional: Is your feature request related to a problem? Please describe.**
The problem is that all cache/db services in my cluster must run cluster external (as a service).

**Optional: Implementation ideas**
Nothing fancy in helm but a few values to enable/disable the feature.


Pull request:
feat: Configurable cache expiry
<!--- Provide a general summary of your changes in the Title above -->

<!--- Reference respective issue if it exists -->
Fixes #1587 in its basic form, though it doesn't allow configuring a remote Redis

## Description
This PR makes the cache expiry time of entries in the Redis configurable. If cache expiry is set to 0, the Redis deployment is cut from Connaisseur's deployment.


## Checklist
<!--- Mark as done if a point is not necessary. Feel free to reach out if help on any items in the checklist is needed. -->

- [x] PR is rebased to/aimed at branch `develop`
- [x] PR follows [Contributing Guide](https://github.com/sse-secure-systems/connaisseur/blob/master/docs/CONTRIBUTING.md)
- [x] Added tests (if necessary)
- [x] Extended README/Documentation (if necessary)
- [x] Adjusted versions of image and Helm chart in `Chart.yaml` (if necessary)


Patch:
diff --git a/charts/connaisseur/Chart.yaml b/charts/connaisseur/Chart.yaml
index d0d7c0e1..6537332f 100644
--- a/charts/connaisseur/Chart.yaml
+++ b/charts/connaisseur/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
 name: connaisseur
 description: Helm chart for Connaisseur - a Kubernetes admission controller to integrate container image signature verification and trust pinning into a cluster.
 type: application
-version: 2.4.0
-appVersion: 3.4.0
+version: 2.5.0
+appVersion: 3.5.0
 keywords:
   - container image
   - signature
diff --git a/charts/connaisseur/templates/deployment.yaml b/charts/connaisseur/templates/deployment.yaml
index e43ca1da..0e6959f8 100644
--- a/charts/connaisseur/templates/deployment.yaml
+++ b/charts/connaisseur/templates/deployment.yaml
@@ -49,9 +49,9 @@ spec:
               scheme: HTTPS
             periodSeconds: 5
             failureThreshold: 30
-          securityContext: 
+          securityContext:
             {{- toYaml .Values.kubernetes.deployment.securityContext | nindent 12 }}
-          resources: 
+          resources:
             {{- toYaml .Values.kubernetes.deployment.resources | nindent 12 }}
           volumeMounts:
             - name: certs
@@ -60,20 +60,26 @@ spec:
             - name: app-config
               mountPath: /app/config
               readOnly: true
+            {{ if gt (dig "features" "cache" "expirySeconds" 30 .Values.application | int) 0 -}}
             - name: redis-certs
               mountPath: /app/redis-certs/tls.crt
               readOnly: true
               subPath: tls.crt
+            {{- end }}
             {{- include "connaisseur.alertMounts" . | nindent 12 }}
             {{- include "connaisseur.validatorSecretMounts" . | nindent 12 }}
           envFrom:
           - configMapRef:
               name: {{ include "connaisseur.envName" . }}
+          {{ if gt (dig "features" "cache" "expirySeconds" 30 .Values.application | int) 0 -}}
           - secretRef:
               name: {{ include "connaisseur.redisSecret" . }}
+          {{- end }}
           env:
+          {{ if gt (dig "features" "cache" "expirySeconds" 30 .Values.application | int) 0 -}}
           - name: REDIS_HOST
             value: {{ include "connaisseur.redisService" . }}
+          {{- end }}
           {{ if .Values.kubernetes.deployment.envs -}}
           - secretRef:
               name: {{ include "connaisseur.envSecretName" . }}
@@ -82,9 +88,11 @@ spec:
         - name: certs
           secret:
             secretName: {{ include "connaisseur.TLSName" . }}
+        {{ if gt (dig "features" "cache" "expirySeconds" 30 .Values.application | int) 0 -}}
         - name: redis-certs
           secret:
             secretName: {{ include "connaisseur.redisTLS" . }}
+        {{- end }}
         - name: app-config
           configMap:
             name: {{ include "connaisseur.appConfigName" . -}}
diff --git a/charts/connaisseur/templates/env.yaml b/charts/connaisseur/templates/env.yaml
index 163d3f0c..462a43be 100644
--- a/charts/connaisseur/templates/env.yaml
+++ b/charts/connaisseur/templates/env.yaml
@@ -11,6 +11,9 @@ data:
   AUTOMATIC_UNCHANGED_APPROVAL: {{ print .automaticUnchangedApproval | default "false" | quote }}
   DETECTION_MODE: {{ print .detectionMode | default "false" | quote }}
   RESOURCE_VALIDATION_MODE: {{ print .resourceValidationMode | default "all" | quote }}
+  {{- with .cache }}
+  CACHE_EXPIRY_SECONDS: {{ print .expirySeconds | default 30 | quote }}
+  {{- end }}
   {{- end }}
   LOG_LEVEL: {{ .Values.application.logLevel | default "info" | quote }}
 ---
diff --git a/charts/connaisseur/templates/redis.yaml b/charts/connaisseur/templates/redis.yaml
index 38b4caee..e322dd9c 100644
--- a/charts/connaisseur/templates/redis.yaml
+++ b/charts/connaisseur/templates/redis.yaml
@@ -1,3 +1,4 @@
+{{- if gt (dig "features" "cache" "expirySeconds" 30 .Values.application | int) 0 -}}
 {{- $svc := (include "connaisseur.redisService" .) -}}
 {{- $tlsName := (include "connaisseur.redisTLS" .) -}}
 {{- $input := dict "deployment" .Values.kubernetes.redis "tlsName" $tlsName "svc" $svc "namespace" .Release.Namespace -}}
@@ -133,3 +134,4 @@ spec:
       name: http
   selector:
     {{- include "connaisseur.redisSelectorLabels" . | nindent 4 }}
+{{- end -}}
diff --git a/charts/connaisseur/values.yaml b/charts/connaisseur/values.yaml
index 533117bf..fa58c284 100644
--- a/charts/connaisseur/values.yaml
+++ b/charts/connaisseur/values.yaml
@@ -153,6 +153,8 @@ application:
       mode: ignore # 'ignore' or 'validate'
     # validate all resources or only pods (and warn on others)
     resourceValidationMode: "all" # 'all' or 'podsOnly'
+    cache:
+      expirySeconds: 30
 
   logLevel: info
 
diff --git a/cmd/connaisseur/main.go b/cmd/connaisseur/main.go
index e2a75707..980cd67f 100644
--- a/cmd/connaisseur/main.go
+++ b/cmd/connaisseur/main.go
@@ -39,7 +39,7 @@ func startServer(config *config.Config, alerting *alerting.Config) {
 	mux.HandleFunc("/start", handler.HandleStart)
 	mux.HandleFunc("/metrics", promhttp.Handler().ServeHTTP)
 
-	cache := caching.NewRedis()
+	cache := caching.NewCacher()
 
 	var apiClient dynamic.Interface
 	if utils.FeatureFlagOn(constants.AutomaticChildApproval) {
diff --git a/docs/features/automatic_unchanged_approval.md b/docs/features/automatic_unchanged_approval.md
index 892fd7df..76f047df 100644
--- a/docs/features/automatic_unchanged_approval.md
+++ b/docs/features/automatic_unchanged_approval.md
@@ -19,7 +19,7 @@ The creation of resources on the other hand remains unchanged and will enforce v
 
 ## Configuration options
 
-`automaticUnchangedApproval` in `helm/value.yaml` under `application.features` supports the following values:
+`automaticUnchangedApproval` in `charts/connaisseur/values.yaml` under `application.features` supports the following values:
 
 | Key | Default | Required | Description |
 | - | - | - | - |
diff --git a/docs/features/caching.md b/docs/features/caching.md
index bff5eafe..9e977c47 100644
--- a/docs/features/caching.md
+++ b/docs/features/caching.md
@@ -3,4 +3,24 @@
 Connaisseur utilizes [Redis](https://github.com/redis/redis) as a cache.
 For each image reference the resolved digest or validation error is cached.
 This drastically boosts the performance of Connaisseur compared to older non-caching variants.
-The expiration for keys in the cache is set to 30 seconds.
+The expiration for keys in the cache defaults to 30 seconds, but can be tweaked.
+If set to 0, no caching will be performed and the cache will not be deployed as part of Connaisseur.
+
+## Configuration options
+
+`cache` in `charts/connaisseur/values.yaml` under `application.features` supports the following configuration:
+
+| Key | Default | Required | Description |
+| - | - | - | - |
+| `expirySeconds` | `30` | - | Number of seconds for which validation results are cached. If set to 0, the Connaisseur deployment will omit the caching infrastructure in its entirety. |
+
+## Example
+
+In `charts/connaisseur/values.yaml`:
+
+```yaml
+application:
+  features:
+    cache:
+      expirySeconds: 30
+```
\ No newline at end of file
diff --git a/internal/caching/caching.go b/internal/caching/caching.go
index 2e335bcf..40185791 100644
--- a/internal/caching/caching.go
+++ b/internal/caching/caching.go
@@ -3,13 +3,10 @@ package caching
 import (
 	"connaisseur/internal/constants"
 	"context"
-	"crypto/tls"
-	"crypto/x509"
-	"fmt"
 	"os"
+	"strconv"
 	"time"
 
-	"github.com/redis/go-redis/v9"
 	"github.com/sirupsen/logrus"
 )
 
@@ -18,77 +15,29 @@ type Cacher interface {
 	Del(ctx context.Context, keys ...string) error
 	Get(ctx context.Context, key string) (string, error)
 	Keys(ctx context.Context, pattern string) ([]string, error)
-	Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
+	Set(ctx context.Context, key string, value interface{}) error
 	Ping(ctx context.Context) error
 }
 
-type Redis struct {
-	redisClient *redis.Client
-}
-
-func (r Redis) Close() error {
-	return r.redisClient.Close()
-}
-
-func (r Redis) Del(ctx context.Context, keys ...string) error {
-	return r.redisClient.Del(ctx, keys...).Err()
-}
-
-func (r Redis) Get(ctx context.Context, key string) (string, error) {
-	return r.redisClient.Get(ctx, key).Result()
-}
-
-func (r Redis) Keys(ctx context.Context, pattern string) ([]string, error) {
-	return r.redisClient.Keys(ctx, pattern).Result()
-}
-
-func (r Redis) Set(
-	ctx context.Context,
-	key string,
-	value interface{},
-	expiration time.Duration,
-) error {
-	return r.redisClient.Set(ctx, key, value, expiration).Err()
-}
-
-func (r Redis) Ping(ctx context.Context) error {
-	_, err := r.redisClient.Ping(ctx).Result()
-	if err != nil {
-		logrus.Errorf("redis ping failed: %s", err)
-		return err
-	}
-	return nil
-}
-
 func NewCacher() Cacher {
-	return NewRedis()
-}
-
-func NewRedis() Redis {
-	rdb := redisClient()
-	return Redis{redisClient: rdb}
-}
-
-func redisClient() *redis.Client {
-	host := os.Getenv("REDIS_HOST")
-	pass := os.Getenv("REDIS_PASSWORD")
-
-	cert, err := os.ReadFile(fmt.Sprintf("%s/tls.crt", constants.RedisCertDir))
-	if err != nil {
-		logrus.Fatalf("could not read redis cert: %s", err)
+	configuredExpirySeconds, ok := os.LookupEnv(constants.CacheExpirySeconds)
+
+	var expirySeconds int64
+	if !ok {
+		expirySeconds = constants.DefaultCacheExpirySeconds
+	} else {
+		parsedExpirySeconds, err := strconv.ParseInt(configuredExpirySeconds, 10, 64)
+		if err != nil {
+			logrus.Warnf("Couldn't parse cache expiry configuration '%s', defaulting to %d", configuredExpirySeconds, constants.DefaultCacheExpirySeconds)
+			expirySeconds = constants.DefaultCacheExpirySeconds
+		} else {
+			expirySeconds = parsedExpirySeconds
+		}
 	}
-	certPool := x509.NewCertPool()
-	if !certPool.AppendCertsFromPEM(cert) {
-		logrus.Fatal("failed to append redis certificate")
+	// If expiry is set to 0 or less, use a "cache" that doesn't do anything
+	if expirySeconds <= 0 {
+		return EmptyCache{}
 	}
 
-	rdb := redis.NewClient(&redis.Options{
-		Addr:     fmt.Sprintf("%s:%d", host, constants.DefaultRedisPort),
-		Password: pass,
-		TLSConfig: &tls.Config{
-			RootCAs:    certPool,
-			MinVersion: tls.VersionTLS12,
-		},
-	})
-	return rdb
+	return NewRedis(time.Duration(expirySeconds) * time.Second)
 }
diff --git a/internal/caching/empty.go b/internal/caching/empty.go
new file mode 100644
index 00000000..cf552138
--- /dev/null
+++ b/internal/caching/empty.go
@@ -0,0 +1,32 @@
+package caching
+
+import (
+	"context"
+	"fmt"
+)
+
+type EmptyCache struct{}
+
+func (c EmptyCache) Close() error {
+	return nil
+}
+
+func (c EmptyCache) Del(ctx context.Context, keys ...string) error {
+	return nil
+}
+
+func (c EmptyCache) Get(ctx context.Context, key string) (string, error) {
+	return "", fmt.Errorf("cache disabled")
+}
+
+func (c EmptyCache) Keys(ctx context.Context, pattern string) ([]string, error) {
+	return nil, nil
+}
+
+func (c EmptyCache) Set(ctx context.Context, key string, value interface{}) error {
+	return nil
+}
+
+func (c EmptyCache) Ping(ctx context.Context) error {
+	return nil
+}
diff --git a/internal/caching/redis.go b/internal/caching/redis.go
new file mode 100644
index 00000000..3ceb8f67
--- /dev/null
+++ b/internal/caching/redis.go
@@ -0,0 +1,81 @@
+package caching
+
+import (
+	"connaisseur/internal/constants"
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/redis/go-redis/v9"
+	"github.com/sirupsen/logrus"
+)
+
+type Redis struct {
+	redisClient *redis.Client
+	expiry      time.Duration
+}
+
+func (r Redis) Close() error {
+	return r.redisClient.Close()
+}
+
+func (r Redis) Del(ctx context.Context, keys ...string) error {
+	return r.redisClient.Del(ctx, keys...).Err()
+}
+
+func (r Redis) Get(ctx context.Context, key string) (string, error) {
+	return r.redisClient.Get(ctx, key).Result()
+}
+
+func (r Redis) Keys(ctx context.Context, pattern string) ([]string, error) {
+	return r.redisClient.Keys(ctx, pattern).Result()
+}
+
+func (r Redis) Set(
+	ctx context.Context,
+	key string,
+	value interface{},
+) error {
+	return r.redisClient.Set(ctx, key, value, r.expiry).Err()
+}
+
+func (r Redis) Ping(ctx context.Context) error {
+	_, err := r.redisClient.Ping(ctx).Result()
+	if err != nil {
+		logrus.Errorf("redis ping failed: %s", err)
+		return err
+	}
+	return nil
+}
+
+func NewRedis(expiry time.Duration) Redis {
+	rdb := redisClient()
+	return Redis{redisClient: rdb, expiry: expiry}
+}
+
+func redisClient() *redis.Client {
+	host := os.Getenv("REDIS_HOST")
+	pass := os.Getenv("REDIS_PASSWORD")
+
+	cert, err := os.ReadFile(fmt.Sprintf("%s/tls.crt", constants.RedisCertDir))
+	if err != nil {
+		logrus.Fatalf("could not read redis cert: %s", err)
+	}
+	certPool := x509.NewCertPool()
+	if !certPool.AppendCertsFromPEM(cert) {
+		logrus.Fatal("failed to append redis certificate")
+	}
+
+	rdb := redis.NewClient(&redis.Options{
+		Addr:     fmt.Sprintf("%s:%d", host, constants.DefaultRedisPort),
+		Password: pass,
+		TLSConfig: &tls.Config{
+			RootCAs:    certPool,
+			MinVersion: tls.VersionTLS12,
+		},
+	})
+	return rdb
+}
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
index 7a800d1f..b344a942 100644
--- a/internal/constants/constants.go
+++ b/internal/constants/constants.go
@@ -22,6 +22,8 @@ const (
 	DockerAuthFile             = ".dockerconfigjson"
 	DefaultDockerRegistry      = "index.docker.io"
 	DefaultRedisPort           = 6379
+	DefaultCacheExpirySeconds  = 30
+	CacheExpirySeconds         = "CACHE_EXPIRY_SECONDS"
 	EmptyAuthRegistry          = "EMPTYAUTH"
 )
 
@@ -40,7 +42,6 @@ const (
 const (
 	// Timeouts
 	HTTPTimeoutSeconds         = 30
-	CacheExpirySeconds         = 30
 	ValidationTimeoutSeconds   = 29 // Keep below 30 such that we can respond before k8s API times out request to Connaisseur
 	TLSHandshakeTimeoutSeconds = 10
 )
diff --git a/internal/handler/validation/skip.go b/internal/handler/validation/skip.go
index 81ea7c88..179a3e75 100644
--- a/internal/handler/validation/skip.go
+++ b/internal/handler/validation/skip.go
@@ -28,7 +28,7 @@ func Skip(
 	previousImages []string,
 	parentImageFunc func(ctx context.Context) []string,
 ) (skip bool, skipReason, validatedDigest string, validationError error) {
-	if automaticUnchangedApproval(ctx, image, previousImages) {
+	if automaticUnchangedApproval(image, previousImages) {
 		return true, "unchanged image reference", image.Digest(), nil
 	}
 
@@ -46,11 +46,9 @@ func Skip(
 }
 
 // automaticUnchangedApproval checks if the AUTOMATIC_UNCHANGED_APPROVAL feature flag is turned on
-// and if the image is present in the list of old images stored in the context. If both conditions
-// are true, the function returns true, indicating that
-// the validation check should be skipped.
+// and if the image is present in the list of old images. If both conditions are true,
+// the function returns true, indicating that the validation check should be skipped.
 func automaticUnchangedApproval(
-	ctx context.Context,
 	img *image.Image,
 	previousImages []string,
 ) bool {
diff --git a/internal/handler/validation/validation.go b/internal/handler/validation/validation.go
index 5322340d..ad308eaa 100644
--- a/internal/handler/validation/validation.go
+++ b/internal/handler/validation/validation.go
@@ -10,7 +10,6 @@ import (
 	"context"
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/sirupsen/logrus"
 )
@@ -247,7 +246,6 @@ func ValidateImage(ctx context.Context, in ValidationInput, out chan<- Validatio
 				utils.JsonEscapeString(img.Digest()),
 				utils.JsonEscapeString(cacheErr),
 			),
-			constants.CacheExpirySeconds*time.Second,
 		)
 		if err != nil {
 			logrus.Warnf("error caching digest: %v", err)


Test patch:
diff --git a/internal/caching/caching_test.go b/internal/caching/caching_test.go
index 402ed85c..936a1df7 100644
--- a/internal/caching/caching_test.go
+++ b/internal/caching/caching_test.go
@@ -2,81 +2,28 @@ package caching
 
 import (
 	"connaisseur/internal/constants"
-	"context"
-	"os"
-	"os/exec"
 	"testing"
 
-	"github.com/alicebob/miniredis/v2"
-	"github.com/redis/go-redis/v9"
 	"github.com/stretchr/testify/assert"
 )
 
-const PRE = "../../test/testdata/"
-
-func TestRedis(t *testing.T) {
+func TestMain(m *testing.M) {
 	constants.RedisCertDir = PRE + "caching/00_cert"
+}
 
+func TestNewCacherDefault(t *testing.T) {
 	c := NewCacher()
 	assert.IsType(t, Redis{}, c)
-	re := c.(Redis)
-
-	r := miniredis.RunT(t)
-	rdb := redis.NewClient(&redis.Options{
-		Addr: r.Addr(),
-	})
-	re.redisClient = rdb
-
-	defer re.Close()
-
-	ctx := context.Background()
-	re.Set(ctx, "key", "value", 0)
-
-	val, _ := re.Get(ctx, "key")
-	assert.Equal(t, "value", val)
-
-	vals, _ := re.Keys(ctx, "*")
-	assert.Equal(t, []string{"key"}, vals)
-
-	re.Del(ctx, "key")
-	_, err := re.Get(ctx, "key")
-	assert.Error(t, err)
-
-	err = re.Ping(ctx)
-	assert.NoError(t, err)
-
-	re.Close()
-	err = re.Ping(ctx)
-	assert.Error(t, err)
 }
 
-func TestRedisErrors(t *testing.T) {
-	// This test is a bit tricky. We want to test the error handling of the
-	// redis client, but we can't do that in the same process, because the
-	// command will do an os.Exit(1) on error. So we start a new process
-	// with the TEST_CASE environment variable set to 1 or 2, which will
-	// cause the redis client to fail. We then check if the process exited
-	// with an error.
-	switch os.Getenv("TEST_CASE") {
-	case "1":
-		constants.RedisCertDir = PRE + "caching/01_err"
-		redisClient()
-	case "2":
-		constants.RedisCertDir = PRE + "not_found"
-		redisClient()
-	default:
-		cmd := exec.Command(os.Args[0], "-test.run=TestRedisErrors")
-		cmd.Env = append(os.Environ(), "TEST_CASE=1")
-		err := cmd.Run()
-		e, ok := err.(*exec.ExitError)
-		assert.True(t, ok)
-		assert.False(t, e.Success())
+func TestNewCacherCachingDisabled(t *testing.T) {
+	t.Setenv(constants.CacheExpirySeconds, "0")
+	c := NewCacher()
+	assert.IsType(t, EmptyCache{}, c)
+}
 
-		cmd = exec.Command(os.Args[0], "-test.run=TestRedisErrors")
-		cmd.Env = append(os.Environ(), "TEST_CASE=2")
-		err = cmd.Run()
-		e, ok = err.(*exec.ExitError)
-		assert.True(t, ok)
-		assert.False(t, e.Success())
-	}
+func TestNewCacherInvalidConfig(t *testing.T) {
+	t.Setenv(constants.CacheExpirySeconds, "not a number")
+	c := NewCacher()
+	assert.IsType(t, Redis{}, c)
 }
diff --git a/internal/caching/empty_test.go b/internal/caching/empty_test.go
new file mode 100644
index 00000000..36cf2bf6
--- /dev/null
+++ b/internal/caching/empty_test.go
@@ -0,0 +1,7 @@
+package caching
+
+import "testing"
+
+func TestIsCacher(t *testing.T) {
+	var _ Cacher = EmptyCache{}
+}
diff --git a/internal/caching/redis_test.go b/internal/caching/redis_test.go
new file mode 100644
index 00000000..39f03920
--- /dev/null
+++ b/internal/caching/redis_test.go
@@ -0,0 +1,82 @@
+package caching
+
+import (
+	"connaisseur/internal/constants"
+	"context"
+	"os"
+	"os/exec"
+	"testing"
+
+	"github.com/alicebob/miniredis/v2"
+	"github.com/redis/go-redis/v9"
+	"github.com/stretchr/testify/assert"
+)
+
+const PRE = "../../test/testdata/"
+
+func TestRedis(t *testing.T) {
+	constants.RedisCertDir = PRE + "caching/00_cert"
+
+	c := NewCacher()
+	assert.IsType(t, Redis{}, c)
+	re := c.(Redis)
+
+	r := miniredis.RunT(t)
+	rdb := redis.NewClient(&redis.Options{
+		Addr: r.Addr(),
+	})
+	re.redisClient = rdb
+
+	defer re.Close()
+
+	ctx := context.Background()
+	re.Set(ctx, "key", "value")
+
+	val, _ := re.Get(ctx, "key")
+	assert.Equal(t, "value", val)
+
+	vals, _ := re.Keys(ctx, "*")
+	assert.Equal(t, []string{"key"}, vals)
+
+	re.Del(ctx, "key")
+	_, err := re.Get(ctx, "key")
+	assert.Error(t, err)
+
+	err = re.Ping(ctx)
+	assert.NoError(t, err)
+
+	re.Close()
+	err = re.Ping(ctx)
+	assert.Error(t, err)
+}
+
+func TestRedisErrors(t *testing.T) {
+	// This test is a bit tricky. We want to test the error handling of the
+	// redis client, but we can't do that in the same process, because the
+	// command will do an os.Exit(1) on error. So we start a new process
+	// with the TEST_CASE environment variable set to 1 or 2, which will
+	// cause the redis client to fail. We then check if the process exited
+	// with an error.
+	switch os.Getenv("TEST_CASE") {
+	case "1":
+		constants.RedisCertDir = PRE + "caching/01_err"
+		redisClient()
+	case "2":
+		constants.RedisCertDir = PRE + "not_found"
+		redisClient()
+	default:
+		cmd := exec.Command(os.Args[0], "-test.run=TestRedisErrors")
+		cmd.Env = append(os.Environ(), "TEST_CASE=1")
+		err := cmd.Run()
+		e, ok := err.(*exec.ExitError)
+		assert.True(t, ok)
+		assert.False(t, e.Success())
+
+		cmd = exec.Command(os.Args[0], "-test.run=TestRedisErrors")
+		cmd.Env = append(os.Environ(), "TEST_CASE=2")
+		err = cmd.Run()
+		e, ok = err.(*exec.ExitError)
+		assert.True(t, ok)
+		assert.False(t, e.Success())
+	}
+}
diff --git a/internal/handler/mutate_test.go b/internal/handler/mutate_test.go
index 1ed02d3f..5f81e3df 100644
--- a/internal/handler/mutate_test.go
+++ b/internal/handler/mutate_test.go
@@ -14,7 +14,6 @@ import (
 	"strconv"
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
@@ -565,7 +564,7 @@ func TestMutateReviewCached(t *testing.T) {
 
 	for idx, tc := range testCases {
 		// Prepare Redis
-		err := cache.Set(ctx, tc.cacheKey, tc.cacheValue, 30*time.Second)
+		err := cache.Set(ctx, tc.cacheKey, tc.cacheValue)
 		assert.Nil(t, err, "test case %d", idx+1)
 
 		ar := testhelper.RetrieveAdmissionReview(PRE + "admission_requests/" + tc.admissionFile + ".json")
diff --git a/internal/handler/validation/skip_test.go b/internal/handler/validation/skip_test.go
index 165f8897..d372fa82 100644
--- a/internal/handler/validation/skip_test.go
+++ b/internal/handler/validation/skip_test.go
@@ -272,13 +272,12 @@ func TestAutomaticUnchangedApproval(t *testing.T) {
 	}
 
 	for idx, tc := range testCases {
-		ctx := context.Background()
 		image, err := image.New(tc.img)
 		if err != nil {
 			panic(fmt.Errorf("Image for test must be valid: %s", err))
 		}
 		t.Setenv(constants.AutomaticUnchangedApproval, strconv.FormatBool(tc.enabled))
-		result := automaticUnchangedApproval(ctx, image, tc.oldImages)
+		result := automaticUnchangedApproval(image, tc.oldImages)
 		assert.Equal(t, tc.expected, result, idx+1)
 	}
 }
diff --git a/test/testhelper/mock_cache.go b/test/testhelper/mock_cache.go
index 1ef6da6c..2b3a28bf 100644
--- a/test/testhelper/mock_cache.go
+++ b/test/testhelper/mock_cache.go
@@ -2,6 +2,7 @@ package testhelper
 
 import (
 	"connaisseur/internal/caching"
+	"connaisseur/internal/constants"
 	"context"
 	"fmt"
 	"testing"
@@ -51,9 +52,8 @@ func (c FailingCache) Set(
 	ctx context.Context,
 	key string,
 	value interface{},
-	expiration time.Duration,
 ) error {
-	return c.client.Set(ctx, key, value, expiration).Err()
+	return c.client.Set(ctx, key, value, constants.DefaultCacheExpirySeconds*time.Second).Err()
 }
 
 func (c FailingCache) Ping(ctx context.Context) error {