Container Registry: Tag immutability feature
## Context
The GitLab Container Registry is a crucial component of many CI/CD pipelines, allowing teams to store and manage Docker images seamlessly. Currently, users can freely create and push tagged images, but there is no feature to enforce tag immutability once a tag is created. This introduces a risk of tags being overwritten, either accidentally or intentionally, leading to potential inconsistencies, security vulnerabilities, and deployment issues in environments that rely on specific image tags for stability.
## Motivation
Tag immutability is a critical feature for organizations aiming to maintain consistent and secure container environments. Immutable tags ensure that once an image is tagged (e.g., `v1.0.0`), the tag cannot be altered or replaced. This reduces the risk of deploying unintended or unverified changes and promotes trust in the integrity of tagged images.
Supporting tag immutability in the GitLab Container Registry would provide several benefits:
- **Security**: Prevents the alteration of image tags, mitigating the risk of malicious or accidental image changes.
- **Stability**: Ensures that tagged images remain consistent across environments, improving deployment reliability.
- **Compliance**: Facilitates meeting regulatory and internal compliance requirements by guaranteeing the integrity of tagged versions.
Many other container registries, such as Docker Hub and Amazon ECR, already offer tag immutability as a core feature, and GitLab's users would benefit from this as well.
### How does it differ from [Tag Protection](https://gitlab.com/groups/gitlab-org/-/epics/15608)?
While both _Protected Tags_ and _Tag Immutability_ contribute to maintaining image integrity, they serve different purposes. _Protected Tags_ restrict _who_ can create, update or delete certain tags based on roles, while _Tag Immutability_ ensures that once a tag is created, it can't be updated by _anyone_.
Therefore, these features complement each other but solve distinct problems. Here is a graphical representation on how they will be evaluated in the context of an image push:
```mermaid
graph TD
A[User attempts to push a tag] --> B{Does the user have the required role for push?}
B -- Yes --> C{Does the tag already exist?}
B -- No --> D[Push denied: Insufficient permissions]
C -- Yes --> E{Is the tag marked as immutable?}
C -- No --> F[Tag is created successfully]
E -- Yes --> G[Push denied: Tag is immutable]
E -- No --> H[Tag is overwritten successfully]
```
## Scope
Initially, these policies will apply to **all** container repositories under a project, with future iterations potentially allowing different policies for each repository.
## Limitations
Multiple immutable rules can be defined, allowing different policies for different tag patterns. In the first iteration, there is a limit of **5** rules (either immutable or [protected](https://gitlab.com/groups/gitlab-org/-/epics/15608) rules) per project and a maximum length of **100** characters for patterns.
The creation and deletion of immutable rules is limited to `Owner`+ roles. It's not possible to edit immutable rules.
## Implementation
### Rails
* Allow `Owner`+ users to create and delete immutable rules;
* Allow `Maintainers+` users to view immutable rules;
* Persist the tag name pattern of immutable rules in relation to a `Project`;
* Disable delete button for immutable tags in the UI;
* Refuse deletions for immutable tags through the Rails REST/GraphQL APIs;
* List protected tag rules first in the UI, followed by immutable rules;
* Ignore/skip immutable tags during [cleanup policies](https://docs.gitlab.com/ee/development/packages/cleanup_policies.html#container-registry);
* Adjust emitted JWT tokens to pass immutable rule patterns to the registry (see below). This applies to all JWT with write permissions, including the ones used internally.
#### JWT Auth
Aside from the changes above, we'll need to modify the JWT tokens minted in Rails to use them as "envelope" for the immutable rules sent for validations on the registry side. The reasoning and workflow is the same as for protected tags. Read more about it [here](https://gitlab.com/groups/gitlab-org/-/epics/15608#jwt-auth) if needed. Here we'll focus on the required changes only.
To allow for a distinction between protected vs immutable rules, instead of adding the immutable patterns to the existing `access.meta.tag_deny_access_patterns` object for protected tags, we'll add a new `access.meta.tag_immutable_patterns` array. This will contain the regexp pattern strings of all configured immutable rules.
For example, if I'm a member with registry access in the `my-group/my-project` project, and there are two immutable rules with patterns `^latest$` and `^v\d+\.\d+\.\d+$`, when I request a token with `push` permissions from Rails, the token will look like:
```json
{
"access": [
{
"actions": [
"push"
],
"meta": {
"project_id": 1234567,
"project_path": "my-group/my-project",
"root_namespace_id": 7654321,
"tag_immutable_patterns": [
"^latest$",
"^v\d+\\.\d+\\.\d+$"
]
},
"name": "my-group/my-project",
"type": "repository"
}
],
//...
}
```
This tells the registry that I'm not allowed to create or update any tag matching `^latest$` or `^v\d+\.\d+\.\d+$`, because those are marked as immutable.
`access.meta.tag_immutable_patterns` will be present only if _all_ of the following conditions are met:
1. The feature is enabled for the target project/root group;
2. The requested action is `push`, `delete` or `*`;
3. There is at least one immutable tag rule configured for the target project.
### Registry
Within the registry we'll need to parse the new `tag_immutable_patterns` attribute from the JWT tokens and use that to allow/disallow tag creation/update requests.
Based on the example above, if, while in possession of this token I attempt to create or update the `latest` tag within the `my-group/my-project` container repository, the registry should deny such requests.
As we're defining a regex pattern in Ruby and evaluating it in Go, we have to make sure we fallback to denying requests if for some reason the pattern evaluation fails in the latter.
### Caveats
As with protected tags, a limitation to be aware of is that registry tokens have a configurable validity period, and changes in immutable rules are only guaranteed to take effect after this period expires. By default, this is set to 5 minutes, but on GitLab.com, it's currently set to 15 minutes. Although standard registry clients will request a new token for each operation, it's technically possible for a user to reuse an existing token until it expires if manually crafting requests to the registry. So we should be mindful of that.
<!-- triage-serverless v3 PLEASE DO NOT REMOVE THIS SECTION -->
> [!important]
> This page may contain information related to upcoming products, features and functionality.
> It is important to note that the information presented is for informational purposes only, so please do not rely on the information for purchasing or planning purposes.
> Just like with all projects, the items mentioned on the page are subject to change or delay, and the development, release, and timing of any products, features, or functionality remain at the sole discretion of GitLab Inc.
<!-- triage-serverless v3 PLEASE DO NOT REMOVE THIS SECTION -->
epic