diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b8803606..24b9b4cac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,65 +1,72 @@
-## v18.2.0 (2025-07-12)
+## v18.2.1 (2025-07-28)
+
+### Bug fixes
+
+- Fix cache key sanitation issues, esp. re. "cache key files" [!5741](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5741)
+
+## v18.2.0 (2025-07-28)
 
 ### New features
 
-- Add reference to z/OS on the main runner install page [!5647](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5647)
-- Thread job request correlation ID to git operations [!5653](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5653)
-- Add functionality to retrieve secrets from AWS SecretsManager [!5587](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5587) (Markus Siebert @m-s-db)
 - Update docs on how to use env variables for S3 cache access [!5648](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5648)
 - Improve runner_name metric label coverage [!5609](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5609) (Josh Smith @jsmith25)
+- Add functionality to retrieve secrets from AWS SecretsManager [!5587](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5587) (Markus Siebert @m-s-db)
+- Add reference to z/OS on the main runner install page [!5647](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5647)
+- Thread job request correlation ID to git operations [!5653](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5653)
 - Log usage of deault image for Docker and K8S executors [!5688](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5688)
 
 ### Bug fixes
 
-- Tighten cache key sanitation [!5719](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5719)
-- Add troubleshooting guide for GCS workload identity [!5651](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5651)
-- Skip pre and post checkout hooks for empty [!5677](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5677)
-- Docs: Fix protocol_port default for SSH [!5701](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5701)
 - Revert MRs 5531 and 5676 [!5715](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5715)
-- Reimplement ShortenToken [!5681](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5681)
+- Skip pre and post checkout hooks for empty [!5677](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5677)
 - Put the fips binary in the fips runner image [!5669](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5669)
+- Add `-depth` option to `find -exec rm` invocations [!5692](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5692)
+- Add troubleshooting guide for GCS workload identity [!5651](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5651)
+- Fix cache key sanitation issues, esp. re. "cache key files" [!5741](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5741)
+- Reimplement ShortenToken [!5681](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5681)
+- Fix protocol_port default for SSH [!5701](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5701)
+- Fix kubernetes executor helper image override log [!5655](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5655) (Ricard Bejarano @ricardbejarano)
 - Set `helper_image_flavor` to `ubi-fips` when fips mode is enabled [!5698](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5698)
 - Ensure BuildErrors have FailureReason [!5676](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5676)
-- Fix kubernetes executor helper image override log [!5655](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5655) (Ricard Bejarano @ricardbejarano)
-- Add `-depth` option to `find -exec rm` invocations [!5692](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5692)
+- Tighten cache key sanitation [!5719](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5719)
 
 ### Maintenance
 
-- Add known SELinux issue regarding tmp and pip to the Podman docs [!5661](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5661)
+- Add .markdownlint-cli2.yaml for doc-locale [!5690](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5690)
+- Remove outdated information [!5691](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5691)
+- Add overprovisioning note, fix typos [!5656](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5656)
+- Chore: refactor TestAttachPodNotRunning [!5650](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5650) (Muhammad Daffa Dinaya @mdaffad)
+- Update PowerShell UTF8 integration test [!5493](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5493) (Guillaume Chauvel @guillaume.chauvel)
 - Switch jobs to Kubernetes [!5631](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5631)
+- Bump golang to 1.24.4 [!5668](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5668)
+- Add additonal info about SHA-pinned images [!5700](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5700)
+- Update step-runner version to 0.13.0 [!5705](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5705)
+- Update file _index.md [!5665](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5665)
+- Improve prometheus scraping metrics docs including Operator [!5657](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5657)
+- Add known SELinux issue regarding tmp and pip to the Podman docs [!5661](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5661)
+- Add configure runner on OCI to the index page [!5649](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5649)
 - Updated the documents [!5596](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5596) (Jithin Vijayan @jithin.vijayan)
-- Update docker machine versions [!5672](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5672)
-- Add autoscaler IP address logging options [!5519](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5519) (Brayden White @bwhite117)
-- Docs: Improve prometheus scraping metrics docs including Operator [!5657](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5657)
-- Update default ruby version in examples [!5693](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5693)
 - Add aardvark-dns bug notice to podman guide [!5689](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5689) (Felix @f.preuschoff)
-- Update region from us-central-1 to eu-central-1 [!5713](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5713)
 - Run fewer vale lint rules on i18n (translation) docs MRs [!5699](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5699)
-- Add additonal info about SHA-pinned images [!5700](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5700)
-- Bump golang to 1.24.4 [!5668](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5668)
-- chore: refactor TestAttachPodNotRunning [!5650](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5650) (Muhammad Daffa Dinaya @mdaffad)
-- Update access a private registry from kubernetes executor [!5622](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5622)
-- Add configure runner on OCI to the index page [!5649](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5649)
 - Clean up runner docs [!5697](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5697)
-- Fix OS version package support docs [!5703](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5703)
-- Add .markdownlint-cli2.yaml for doc-locale [!5690](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5690)
+- Clarify documentation on reading S3 credentials from the environment [!5671](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5671)
+- Add formatting target for easy fixes with golangci-lint [!5658](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5658)
 - Fix this test [!5682](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5682)
 - Update mockery to latest version 3.3.4 and generate mocks [!5646](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5646)
-- Remove outdated information [!5691](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5691)
-- Update file _index.md [!5665](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5665)
-- Update SLSA build type documentation [!5639](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5639)
 - Document tarzstd as an argument for CACHE_COMPRESSION_FORMAT [!5673](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5673)
-- Clarify documentation on reading S3 credentials from the environment [!5671](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5671)
+- Update access a private registry from kubernetes executor [!5622](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5622)
+- Small typo fixes [!5652](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5652)
+- Update default ruby version in examples [!5693](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5693)
+- Refactor gitlab client request job tests to table tests [!5666](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5666)
+- Fix docs pipelines for forks [!5664](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5664)
+- Update docker machine versions [!5672](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5672)
+- Fix OS version package support docs [!5703](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5703)
+- Update region from us-central-1 to eu-central-1 [!5713](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5713)
+- Update SLSA build type documentation [!5639](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5639)
 - Add Kubernetes executors docs for helper container memory sizing [!5659](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5659)
+- Add autoscaler IP address logging options [!5519](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5519) (Brayden White @bwhite117)
 - Runner doc restructuring: Revamp the admin section index page [!5678](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5678)
-- Add formatting target for easy fixes with golangci-lint [!5658](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5658)
-- Add overprovisioning note, fix typos [!5656](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5656)
-- Refactor gitlab client request job tests to table tests [!5666](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5666)
 - Fix indent in config.toml example. [!5667](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5667) (Mathieu Gouin @mathieugouin)
-- Update PowerShell UTF8 integration test [!5493](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5493) (Guillaume Chauvel @guillaume.chauvel)
-- Update step-runner version to 0.13.0 [!5705](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5705)
-- Fix docs pipelines for forks [!5664](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5664)
-- Small typo fixes [!5652](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/5652)
 
 ## v18.1.0 (2025-06-19)
 
diff --git a/shells/abstract.go b/shells/abstract.go
index 88db425b6..62888be45 100644
--- a/shells/abstract.go
+++ b/shells/abstract.go
@@ -11,6 +11,7 @@ import (
 	"strconv"
 	"strings"
 	"time"
+	"unicode"
 
 	"gitlab.com/gitlab-org/gitlab-runner/cache"
 	"gitlab.com/gitlab-org/gitlab-runner/common"
@@ -77,11 +78,13 @@ func (b *AbstractShell) cacheFile(build *common.Build, userKey string) (string,
 	}
 
 	// Deduce cache key
-	// We deliberately don't use path.Join, to not have path traversal issues with e.g. a job nome like
-	// `some/../../job/name` and a git ref like `foo/../bar` would result in `../job/name/bar`.
-	key := build.JobInfo.Name + "/" + build.GitInfo.Ref
+	var key string
 	if userKey != "" {
 		key = build.GetAllVariables().ExpandValue(userKey)
+	} else {
+		// we root the path, to not have path traversal issues
+		path := path.Join("/", build.JobInfo.Name, build.GitInfo.Ref)
+		key = path[1:]
 	}
 
 	// Ignore cache without the key
@@ -185,8 +188,7 @@ func (b *AbstractShell) extractCacheOrFallbackCachesWrapper(
 ) {
 	allowedCacheKeys := []string{}
 
-	build := info.Build
-	buildVars := build.GetAllVariables()
+	buildVars := info.Build.GetAllVariables()
 	addCacheKey := func(key string, err error) {
 		if key != "" {
 			allowedCacheKeys = append(allowedCacheKeys, key)
@@ -197,16 +199,16 @@ func (b *AbstractShell) extractCacheOrFallbackCachesWrapper(
 	}
 
 	// the "default" cache key
-	addCacheKey(sanitizeCacheKey(build, cacheKey))
+	addCacheKey(sanitizeCacheKey(cacheKey))
 
 	// the fallback cache keys from the cache config
 	for _, cacheKey := range cacheOptions.FallbackKeys {
-		addCacheKey(sanitizeCacheKey(build, buildVars.ExpandValue(cacheKey)))
+		addCacheKey(sanitizeCacheKey(buildVars.ExpandValue(cacheKey)))
 	}
 
 	// the fallback key from CACHE_FALLBACK_KEY
 	// we sanitize it and check if it's not pointing to a protected cache.
-	fallbackCacheKey, err := sanitizeCacheKey(build, buildVars.Value("CACHE_FALLBACK_KEY"))
+	fallbackCacheKey, err := sanitizeCacheKey(buildVars.Value("CACHE_FALLBACK_KEY"))
 	blockedSuffix := "-protected"
 	if !strings.HasSuffix(fallbackCacheKey, blockedSuffix) {
 		addCacheKey(fallbackCacheKey, err)
@@ -226,94 +228,70 @@ func (b *AbstractShell) extractCacheOrFallbackCachesWrapper(
 
 // sanitizeCacheKey replicates some cache key rules from GitLab and adds additional validations for known-bad cache
 // keys.
-// Special care is taken for the defaulted key (ie. `jobName/gitRef`, when no user key is provided): we sanitize both
-// parts and validate them individually.
-//
-// Accepting the added complexity by in-lining some helper funcs in favor of not polluting the global namespace.
-//
-//nolint:gocognit
-func sanitizeCacheKey(build *common.Build, cacheKey string) (sanitizedKey string, err error) {
+// It accepts that the cache keys can be paths and:
+//   - replaces the URL encoded version of `/` & `.` with their ASCII ones
+//   - replaces all `\` with `/`
+//   - ensures there are no path traversals possible "outside of the base path"
+//   - ensures the last path element is not empty or ends in a space
+func sanitizeCacheKey(cacheKey string) (sanitizedKey string, err error) {
 	if cacheKey == "" {
 		return "", nil
 	}
 
-	const slashReplace = "__"
-
-	key := strings.TrimSpace(cacheKey)
-
-	blockDots := func(key string, isPartial bool) error {
-		if !slices.Contains([]string{".", "..", "%2e", "%2e%2e", ".%2e", "%2e."}, strings.ToLower(key)) {
-			return nil
+	replaceEncodedSlashes := func(s string) string {
+		for _, slash := range []string{"%2f", "%2F"} {
+			s = strings.ReplaceAll(s, slash, "/")
 		}
-		return &cacheKeyError{key, "must not be '.', '..' or their URL-encoded equivalent", isPartial}
+		return s
 	}
-	replaceAllSlashes := func(key string) string {
-		for _, slash := range []string{"/", "%2f", "%2F"} {
-			key = strings.ReplaceAll(key, slash, slashReplace)
+	replaceEncodedDots := func(s string) string {
+		for _, dot := range []string{"%2e", "%2E"} {
+			s = strings.ReplaceAll(s, dot, ".")
 		}
-		return key
+		return s
 	}
-
-	if err := blockDots(key, false); err != nil {
-		return "", err
+	toSlash := func(s string) string {
+		return strings.ReplaceAll(s, `\`, `/`)
+	}
+	trimSpaceRight := func(s string) string {
+		return strings.TrimRightFunc(s, unicode.IsSpace)
 	}
 
-	isDefaultedKey := cacheKey == build.JobInfo.Name+"/"+build.GitInfo.Ref
-	if isDefaultedKey {
-		// For the defaulted key we need to sanitize the two parts of the key individually.
-		// We rebuild the key here with the raw data from the build and sanitize & validate both parts before joining them
-		// together and rebuild the key.
-		jobName := strings.TrimSpace(replaceAllSlashes(build.JobInfo.Name))
-		gitRef := strings.TrimSpace(replaceAllSlashes(build.GitInfo.Ref))
-		for _, part := range []string{jobName, gitRef} {
-			if part == "" {
-				return "", &cacheKeyError{part, "could not be sanitized", true}
-			}
-			if err := blockDots(part, true); err != nil {
-				return "", err
-			}
+	// We root the path, so that path traversals outside of the base path, e.g. resulting in a key like `../../foo/bar`,
+	// aren't possible
+	// Note: path.Join calls path.Clean internally
+	cleaned := path.Join(
+		"/", toSlash(replaceEncodedSlashes(replaceEncodedDots(cacheKey))),
+	)
+
+	var key string
+	for {
+		if cleaned == "" {
+			break
 		}
-		key = jobName + "/" + gitRef
-	} else if strings.Contains(cacheKey, "/") || strings.Contains(strings.ToLower(cacheKey), "%2f") {
-		// for non-defaulted keys, we don't allow `/` at all
-		return "", &cacheKeyError{cacheKey, "must not contain '/' or its URL-encoded equivalent", false}
-	}
 
-	// If the build environment is Windows, then `\` is a path separator, which we're not allowing, so for backwards
-	// compatibility we convert them to __.
-	//
-	// This does mean that "cache__key" and "cache\key" collide and that we're going to immediately invalidate any
-	// existing cache that uses `\`: the hope is that this is unlikely. However, we at least print a warning to the user,
-	// that we've changed the cache key.
-	//
-	// We could disallow `\` entirely, but the problem is that we've never documented that this is a disallowed character.
-	// For `/`, it's documented that this isn't allowed, and any use of it is just circumvention.
-	key = strings.ReplaceAll(key, `\`, slashReplace)
+		dir, file := path.Split(cleaned)
+		file = trimSpaceRight(file)
+
+		if file == "" {
+			cleaned = dir[:len(dir)-1] // cut off the trailing `/` from dir and continue with that
+			continue
+		}
+
+		key = path.Join(dir[1:], file) // cut off the leading `/` from dir, because we rooted the path initially
+		break
+	}
 
 	if key == "" {
-		return "", &cacheKeyError{cacheKey, "could not be sanitized", false}
+		return "", fmt.Errorf("cache key %q could not be sanitized", cacheKey)
 	}
 	if key != cacheKey {
-		return key, &cacheKeyError{cacheKey, fmt.Sprintf("sanitized to %q", key), false}
+		return key, fmt.Errorf("cache key %q sanitized to %q", cacheKey, key)
 	}
 
 	return key, nil
 }
 
-type cacheKeyError struct {
-	rawKey    string
-	err       string
-	isPartial bool
-}
-
-func (cke *cacheKeyError) Error() string {
-	target := "cache key"
-	if cke.isPartial {
-		target += " part"
-	}
-	return fmt.Sprintf("%s %q %s", target, cke.rawKey, cke.err)
-}
-
 func (b *AbstractShell) addExtractCacheCommand(
 	ctx context.Context,
 	w ShellWriter,
@@ -1272,6 +1250,15 @@ func (b *AbstractShell) archiveCache(
 			w.Noticef("Skipping cache archiving due to %v", err)
 			continue
 		}
+		// we also want to sanitize the cache key on upload, so that we actually use the same keys on up and download, ie.
+		// it round-trips
+		cacheKey, err = sanitizeCacheKey(cacheKey)
+		if err != nil {
+			w.Warningf(err.Error())
+		}
+		if cacheKey == "" {
+			continue
+		}
 
 		cacheOptions.Policy = common.CachePolicy(info.Build.GetAllVariables().ExpandValue(string(cacheOptions.Policy)))
 
diff --git a/shells/abstract_test.go b/shells/abstract_test.go
index ec1b51ae6..5dc988484 100644
--- a/shells/abstract_test.go
+++ b/shells/abstract_test.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"path"
 	"path/filepath"
+	"runtime"
 	"slices"
 	"strings"
 	"testing"
@@ -1529,30 +1530,29 @@ func TestAbstractShell_extractCacheWithDefaultFallbackKey(t *testing.T) {
 		"using sanitized fallback key": {
 			cacheType:                 "test",
 			cacheKey:                  "test-cache-key",
-			cacheFallbackKeyVarValue:  `  hello....there  `,
+			cacheFallbackKeyVarValue:  `hello.%2e..there  `,
 			expectedCacheKeys:         []string{"test-cache-key", "hello....there"},
-			expectedAdditionalWarning: []any{`cache key "  hello....there  " sanitized to "hello....there"`},
+			expectedAdditionalWarning: []any{`cache key "hello.%2e..there  " sanitized to "hello....there"`},
 		},
 		"using something that looks like a windows path": {
 			cacheType:                 "test",
 			cacheKey:                  "test-cache-key",
 			cacheFallbackKeyVarValue:  `looks\like\a\win\path`,
-			expectedCacheKeys:         []string{"test-cache-key", "looks__like__a__win__path"},
-			expectedAdditionalWarning: []any{`cache key "looks\\like\\a\\win\\path" sanitized to "looks__like__a__win__path"`},
+			expectedCacheKeys:         []string{"test-cache-key", "looks/like/a/win/path"},
+			expectedAdditionalWarning: []any{`cache key "looks\\like\\a\\win\\path" sanitized to "looks/like/a/win/path"`},
 		},
 		"using path-like fallback cache key": {
-			cacheType:                 "test",
-			cacheKey:                  "test-cache-key",
-			cacheFallbackKeyVarValue:  `foo/bar/baz`,
-			expectedCacheKeys:         []string{"test-cache-key"},
-			expectedAdditionalWarning: []any{`cache key "foo/bar/baz" must not contain '/' or its URL-encoded equivalent`},
+			cacheType:                "test",
+			cacheKey:                 "test-cache-key",
+			cacheFallbackKeyVarValue: `foo/bar/baz`,
+			expectedCacheKeys:        []string{"foo/bar/baz", "test-cache-key"},
 		},
 		"using invalid fallback cache key": {
 			cacheType:                 "test",
 			cacheKey:                  "test-cache-key",
 			cacheFallbackKeyVarValue:  `..`,
 			expectedCacheKeys:         []string{"test-cache-key"},
-			expectedAdditionalWarning: []any{`cache key ".." must not be '.', '..' or their URL-encoded equivalent`},
+			expectedAdditionalWarning: []any{`cache key ".." could not be sanitized`},
 		},
 		"using reserved suffix": {
 			cacheType:                 "test",
@@ -1572,9 +1572,9 @@ func TestAbstractShell_extractCacheWithDefaultFallbackKey(t *testing.T) {
 		},
 		"empty cache key, with invalid falback": {
 			cacheType:                 "test",
-			cacheFallbackKeyVarValue:  "some-fallback/invalid",
+			cacheFallbackKeyVarValue:  ".",
 			expectedCacheKeys:         []string{"some-job-name/some-ref-name"},
-			expectedAdditionalWarning: []any{`cache key "some-fallback/invalid" must not contain '/' or its URL-encoded equivalent`},
+			expectedAdditionalWarning: []any{`cache key "." could not be sanitized`},
 		},
 		"GoCloud cache with allowed key value": {
 			cacheType:                "goCloudTest",
@@ -1600,9 +1600,20 @@ func TestAbstractShell_extractCacheWithDefaultFallbackKey(t *testing.T) {
 		},
 		"GoCloud empty cache key, with invalid falback": {
 			cacheType:                 "goCloudTest",
-			cacheFallbackKeyVarValue:  "some-fallback/invalid",
+			cacheFallbackKeyVarValue:  " /  ",
 			expectedCacheKeys:         []string{"some-job-name/some-ref-name"},
-			expectedAdditionalWarning: []any{`cache key "some-fallback/invalid" must not contain '/' or its URL-encoded equivalent`},
+			expectedAdditionalWarning: []any{`cache key " /  " could not be sanitized`},
+		},
+		"cache:key:files": {
+			cacheType:         "test",
+			cacheKey:          "0_project/dependencies-7ab1ff8ddd4179468d07100f16b6f19f91b645a8-non_protected",
+			expectedCacheKeys: []string{"0_project/dependencies-7ab1ff8ddd4179468d07100f16b6f19f91b645a8-non_protected"},
+		},
+		"cache:key:files sanitized": {
+			cacheType:                 "test",
+			cacheKey:                  "0_project/foo/../dependencies-7ab1ff8ddd4179468d07100f16b6f19f91b645a8-non_protected",
+			expectedCacheKeys:         []string{"0_project/dependencies-7ab1ff8ddd4179468d07100f16b6f19f91b645a8-non_protected"},
+			expectedAdditionalWarning: []any{`cache key "0_project/foo/../dependencies-7ab1ff8ddd4179468d07100f16b6f19f91b645a8-non_protected" sanitized to "0_project/dependencies-7ab1ff8ddd4179468d07100f16b6f19f91b645a8-non_protected"`},
 		},
 	}
 
@@ -2190,6 +2201,113 @@ func TestAbstractShell_cachePolicy(t *testing.T) {
 	}
 }
 
+func TestAbstractShell_archiveCache_keySanitation(t *testing.T) {
+	tests := map[string]struct {
+		rawCacheKey               string
+		jobName                   string
+		gitRef                    string
+		expectedToSkipUpload      bool
+		expectedSanitationWarning string
+		expectedLocalFile         map[string]string
+		expectedCacheKey          string
+	}{
+		"defaulted cache key": {
+			jobName: "some-job-name",
+			gitRef:  "some-git-ref",
+			expectedLocalFile: map[string]string{
+				"linux":   "../cacheDir/some-job-name/some-git-ref/cache.zip",
+				"windows": `..\cacheDir\some-job-name\some-git-ref\cache.zip`,
+			},
+			expectedCacheKey: "some-job-name/some-git-ref",
+		},
+		"defaulted cache key sanitized": {
+			jobName: `some\job\name`,
+			gitRef:  "some/git/ref",
+			expectedLocalFile: map[string]string{
+				"linux":   `../cacheDir/some\job\name/some/git/ref/cache.zip`,
+				"windows": `..\cacheDir\some\job\name\some\git\ref\cache.zip`,
+			},
+			expectedCacheKey:          "some/job/name/some/git/ref",
+			expectedSanitationWarning: `cache key "some\\job\\name/some/git/ref" sanitized to "some/job/name/some/git/ref"`,
+		},
+		"cache key": {
+			rawCacheKey: "hola",
+			expectedLocalFile: map[string]string{
+				"windows": `..\cacheDir\hola\cache.zip`,
+				"linux":   "../cacheDir/hola/cache.zip",
+			},
+			expectedCacheKey: "hola",
+		},
+		"cache key sanitized": {
+			rawCacheKey: `this/../key/will/be\sanitized\  `,
+			expectedLocalFile: map[string]string{
+				"linux":   `../cacheDir/key/will/be\sanitized\  /cache.zip`,
+				"windows": `..\cacheDir\key\will\be\sanitized\  \cache.zip`,
+			},
+			expectedCacheKey:          "key/will/be/sanitized",
+			expectedSanitationWarning: `cache key "this/../key/will/be\\sanitized\\  " sanitized to "key/will/be/sanitized"`,
+		},
+		"cannot be sanitized": {
+			rawCacheKey:               "/",
+			expectedToSkipUpload:      true,
+			expectedSanitationWarning: `cache key "/" could not be sanitized`,
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			info := common.ShellScriptInfo{
+				RunnerCommand: "some-runner-command",
+				Build: &common.Build{
+					CacheDir: "/some/cacheDir",
+					BuildDir: "/some/buildDir",
+					Runner:   &common.RunnerConfig{},
+					JobResponse: common.JobResponse{
+						JobInfo: common.JobInfo{Name: test.jobName},
+						GitInfo: common.GitInfo{Ref: test.gitRef},
+						Cache: common.Caches{
+							{
+								When:  common.CacheWhenAlways,
+								Paths: common.ArtifactPaths{"foo/bar", "foo/barz"},
+								Key:   test.rawCacheKey,
+							},
+						},
+					},
+				},
+			}
+			shell := AbstractShell{}
+			w := NewMockShellWriter(t)
+
+			if warning := test.expectedSanitationWarning; warning != "" {
+				w.On("Warningf", warning)
+			}
+
+			if !test.expectedToSkipUpload {
+				w.On("IfCmd", "some-runner-command", "--version").Once()
+				w.On("Noticef", "Creating cache %s...", test.expectedCacheKey).Once()
+				w.On("IfCmdWithOutput",
+					"some-runner-command", "cache-archiver",
+					"--file", test.expectedLocalFile[runtime.GOOS],
+					"--timeout", "10",
+					"--path", "foo/bar",
+					"--path", "foo/barz",
+				).Once()
+				w.On("Noticef", "Created cache").Once()
+				w.On("Else").Once()
+				w.On("Warningf", "Failed to create cache").Once()
+				w.On("EndIf").Once()
+				w.On("Else").Once()
+				w.On("Warningf", "Missing %s. %s is disabled.", "some-runner-command", "Creating cache").Once()
+				w.On("EndIf")
+			}
+
+			_, err := shell.archiveCache(context.TODO(), w, info, true)
+
+			assert.NoError(t, err, "expected achiveCache to succeed")
+		})
+	}
+}
+
 func TestAbstractShell_writeSubmoduleUpdateCmdPath(t *testing.T) {
 	tests := map[string]struct {
 		paths string
@@ -3242,137 +3360,77 @@ func TestSanitizeCacheKey(t *testing.T) {
 		errMsg          string
 	}{
 		{rawKey: "fallback_key", expectedKey: "fallback_key"},
-		{rawKey: "fallback_key/", errMsg: "must not contain"},
+		{rawKey: "fallback_key/", expectedKey: "fallback_key", errMsg: "sanitized to"},
 		{rawKey: "fallback_key ", expectedKey: "fallback_key", errMsg: "sanitized to"},
-		{rawKey: "fallback_key\\", expectedKey: "fallback_key__", errMsg: "sanitized to"},
-		{rawKey: "fallback_key/ \\", errMsg: "must not contain"},
-		{rawKey: "fallback_key/ / \\  \\", errMsg: "must not contain"},
-		{rawKey: "fallback_key/o", errMsg: "must not contain"},
-		{rawKey: "fallback_key / \\o", errMsg: "must not contain"},
+		{rawKey: "fallback_key\\", expectedKey: "fallback_key", errMsg: "sanitized to"},
+		{rawKey: "fallback_key/ \\", expectedKey: "fallback_key", errMsg: "sanitized to"},
+		{rawKey: "fallback_key/ / \\  \\", expectedKey: "fallback_key", errMsg: "sanitized to"},
+		{rawKey: "fallback_key/o", expectedKey: "fallback_key/o"},
+		{rawKey: "fallback_key / \\o", expectedKey: "fallback_key / /o", errMsg: "sanitized to"},
 
 		{rawKey: ""},
-		{rawKey: "\\", expectedKey: "__", errMsg: "sanitized to"},
-		{rawKey: "\\.", expectedKey: "__.", errMsg: "sanitized to"},
-		{rawKey: "/", errMsg: "must not contain"},
+		{rawKey: "\\", errMsg: "could not be sanitized"},
+		{rawKey: "\\.", errMsg: "could not be sanitized"},
+		{rawKey: "/", errMsg: "could not be sanitized"},
 		{rawKey: " ", errMsg: "could not be sanitized"},
-		{rawKey: ".", errMsg: "must not be"},
-		{rawKey: "..", errMsg: "must not be"},
+		{rawKey: ".", errMsg: "could not be sanitized"},
+		{rawKey: "..", errMsg: "could not be sanitized"},
 		{rawKey: "...", expectedKey: "..."},
 
 		// %2F == '/', %2E == '.', %5C == `\`
-		{rawKey: "something %2F something", errMsg: "must not contain"},
-		{rawKey: "%2E", errMsg: "must not be"},
-		{rawKey: "%2E%2E", errMsg: "must not be"},
-		{rawKey: "%2E%2E%2E", expectedKey: "%2E%2E%2E"},
-		{rawKey: "something %2f something", errMsg: "must not contain"},
-		{rawKey: "%2e", errMsg: "must not be"},
-		{rawKey: "%2e%2E", errMsg: "must not be"},
-		{rawKey: ".%2E", errMsg: "must not be"},
-		{rawKey: "%2e.", errMsg: "must not be"},
-		{rawKey: "%2E%2e%2E", expectedKey: "%2E%2e%2E"},
+		{rawKey: "something %2F something", expectedKey: "something / something", errMsg: "sanitized to"},
+		{rawKey: "something %2f something", expectedKey: "something / something", errMsg: "sanitized to"},
+		{rawKey: "%2E", errMsg: "could not be sanitized"},
+		{rawKey: "%2E%2E", errMsg: "could not be sanitized"},
+		{rawKey: "%2E%2E%2E", expectedKey: "...", errMsg: "sanitized to"},
+		{rawKey: "%2e", errMsg: "could not be sanitized"},
+		{rawKey: "%2e%2E", errMsg: "could not be sanitized"},
+		{rawKey: ".%2E", errMsg: "could not be sanitized"},
+		{rawKey: "%2e.", errMsg: "could not be sanitized"},
+		{rawKey: "%2E%2e%2E", expectedKey: "...", errMsg: "sanitized to"},
 		// This is allowed, we don't know of any problem with this neither on local storage nor on remote/cloud.
 		{rawKey: "%5C", expectedKey: "%5C"},
 		{rawKey: "%5c", expectedKey: "%5c"},
 
-		{rawKey: "\n foo bar \t\r", expectedKey: "foo bar", errMsg: "sanitized to"},
-
-		// For the defaulted cache key
-		// Some tests are quite contrived, but after all the job name and the git ref are user provided data, so we need to
-		// be careful.
-		{
-			rawKey:          "some-job/some-ref",
-			jobNameOverride: ptr("some-job"),
-			gitRefOverride:  ptr("some-ref"),
-			expectedKey:     "some-job/some-ref",
-		},
-		{
-			rawKey:          "some%2f../job/some/ref/.",
-			jobNameOverride: ptr("some%2f../job"),
-			gitRefOverride:  ptr("some/ref/."),
-			expectedKey:     "some__..__job/some__ref__.",
-			errMsg:          "sanitized to",
-		},
-		{
-			rawKey:          ".../....",
-			jobNameOverride: ptr("..."),
-			gitRefOverride:  ptr("...."),
-			expectedKey:     ".../....",
-		},
-		{
-			rawKey:          "../.",
-			jobNameOverride: ptr(".."),
-			gitRefOverride:  ptr("."),
-			errMsg:          "must not be",
-		},
-		{
-			rawKey:          `job\name/git\ref`,
-			jobNameOverride: ptr(`job\name`),
-			gitRefOverride:  ptr(`git\ref`),
-			expectedKey:     "job__name/git__ref",
-			errMsg:          "sanitized to",
-		},
-		{
-			rawKey:          `/`,
-			jobNameOverride: ptr(``),
-			gitRefOverride:  ptr(``),
-			errMsg:          "could not be sanitized",
-		},
-		{
-			rawKey:          ` / `,
-			jobNameOverride: ptr(` `),
-			gitRefOverride:  ptr(` `),
-			errMsg:          "could not be sanitized",
-		},
-		{
-			rawKey:          ` foo / bar `,
-			jobNameOverride: ptr(` foo `),
-			gitRefOverride:  ptr(` bar `),
-			expectedKey:     "foo/bar",
-			errMsg:          "sanitized to",
-		},
-
-		{
-			rawKey:          `//`,
-			jobNameOverride: ptr(``),
-			gitRefOverride:  ptr(`/`),
-			errMsg:          "could not be sanitized",
-		},
-		{
-			rawKey:          `//`,
-			jobNameOverride: ptr(`/`),
-			gitRefOverride:  ptr(``),
-			errMsg:          "could not be sanitized",
-		},
-		{
-			rawKey:          `//\`,
-			jobNameOverride: ptr(`/`),
-			gitRefOverride:  ptr(`\`),
-			expectedKey:     "__/__",
-			errMsg:          "sanitized to",
-		},
+		{rawKey: "\t foo bar \t\r", expectedKey: "\t foo bar", errMsg: "sanitized to"},
+
+		{rawKey: "some-job/some-ref", expectedKey: "some-job/some-ref"},
+		{rawKey: "some%2f../job/some/ref/.", expectedKey: "job/some/ref", errMsg: "sanitized to"},
+		{rawKey: ".../....", expectedKey: ".../...."},
+		{rawKey: "../.", errMsg: "could not be sanitized"},
+		{rawKey: `job\name/git\ref`, expectedKey: "job/name/git/ref", errMsg: "sanitized to"},
+		{rawKey: ` / `, errMsg: "could not be sanitized"},
+		{rawKey: ` foo / bar `, expectedKey: " foo / bar", errMsg: "sanitized to"},
+		{rawKey: `//`, errMsg: "could not be sanitized"},
+		{rawKey: `//\`, errMsg: "could not be sanitized"},
+
+		{rawKey: "foo/./bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "foo/blipp/../bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "/foo/bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "//foo/bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "./foo/bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "../foo/bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: ".../foo/bar", expectedKey: ".../foo/bar"},
+		{rawKey: "foo/bar/..", expectedKey: "foo", errMsg: "sanitized to"},
+		{rawKey: "foo/bar/../..", errMsg: "could not be sanitized"},
+		{rawKey: "foo/bar/../../../.././blerp", expectedKey: "blerp", errMsg: "sanitized to"},
+
+		{rawKey: "foo\\.\\bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "foo\\blipp\\..\\bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "\\foo\\bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "\\\\foo\\bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: ".\\foo\\bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "..\\foo\\bar", expectedKey: "foo/bar", errMsg: "sanitized to"},
+		{rawKey: "...\\foo\\bar", expectedKey: ".../foo/bar", errMsg: "sanitized to"},
+		{rawKey: "foo\\bar\\..", expectedKey: "foo", errMsg: "sanitized to"},
+		{rawKey: "foo\\bar\\..\\..", errMsg: "could not be sanitized"},
+		{rawKey: "foo\\bar\\..\\..\\..\\..\\.\\blerp", expectedKey: "blerp", errMsg: "sanitized to"},
 	}
 
 	for i, test := range tests {
 		name := fmt.Sprintf("%d: %s", i, test.rawKey)
 		t.Run(name, func(t *testing.T) {
-			build := &common.Build{
-				JobResponse: common.JobResponse{
-					JobInfo: common.JobInfo{
-						Name: "some-default-job-name",
-					},
-					GitInfo: common.GitInfo{
-						Ref: "some-default-git-ref",
-					},
-				},
-			}
-			if test.jobNameOverride != nil {
-				build.JobResponse.JobInfo.Name = *test.jobNameOverride
-			}
-			if test.gitRefOverride != nil {
-				build.JobResponse.GitInfo.Ref = *test.gitRefOverride
-			}
-
-			actual, err := sanitizeCacheKey(build, test.rawKey)
+			actual, err := sanitizeCacheKey(test.rawKey)
 			if test.errMsg == "" {
 				assert.NoError(t, err)
 			} else {
@@ -3383,10 +3441,6 @@ func TestSanitizeCacheKey(t *testing.T) {
 	}
 }
 
-func ptr[T any](v T) *T {
-	return &v
-}
-
 func TestAbstractShell_writeGitCleanup(t *testing.T) {
 	v := common.AppVersion
 	userAgent := fmt.Sprintf("http.userAgent=%s %s %s/%s", v.Name, v.Version, v.OS, v.Architecture)
