Shared, Private or Self-Hosted?
Choosing the right runner type is not a matter of convenience, it is a security decision. Shared runners are practical, but their security model is often not sufficient for Magento production deployments involving SSH keys and database credentials. This article shows where the limits lie and what self-hosted runners can deliver.
Table of Contents
- 1. The three runner types and their security model
- 2. Shared runner: risks and limits of use
- 3. Private runner: isolation at project and group level
- 4. Self-hosted runner: full control and its cost
- 5. Secrets protection: how variables are handled in each runner type
- 6. Secure self-hosted runner configuration for Magento
- 7. Network isolation and privileged mode
- 8. Direct security comparison
- 9. Common security mistakes in runner configuration
- 10. Summary
- 11. FAQ
1. The three runner types and their security model
GitLab distinguishes runners by visibility and registration level: shared runners are available to all projects on a GitLab instance, operated by GitLab Inc. (on SaaS) or provided by the admin of a self-managed instance. Group runners (often called private runners) are limited to a GitLab group and its subprojects. Project runners are assigned exclusively to a single project. In practice, self-hosted runner describes any runner where your own team operates the hardware, the operating system and the gitlab-runner software itself, regardless of whether it is registered at shared, group or project level.
The security model differs fundamentally: with shared runners on GitLab.com, the same runner process executes jobs from thousands of different projects and organizations. With a self-hosted runner, the runner executes only jobs belonging to your own organization. This does not mean shared runners are insecure, GitLab.com uses Docker isolation with short-lived containers, but it does mean your own influence over isolation, network access and secrets protection on a foreign runner is minimal.
For Magento deployments, where SSH private keys, database passwords, Composer auth tokens and production server addresses are stored in GitLab CI variables, the question of runner security is not an academic discussion. A compromised runner with access to these variables can open SSH connections to production servers. That is why it is worth understanding the security guarantees of the different runner types in concrete terms.
2. Shared runner: risks and limits of use
Shared runners on GitLab.com use the Docker executor with short-lived containers. Every job starts a fresh container and deletes it after completion. This prevents a job from reading data left behind by a previous job in the same container. Isolation between jobs of different projects happens at the container level, provided the container is not started in privileged mode, which is not the default on GitLab.com.
The remaining risk with shared runners lies less in the technical isolation and more in the trust placed in the operator and in the visibility of variables. On GitLab.com, protected variables are only visible in jobs running on protected branches or protected tags. Unprotected variables are visible in every job, including merge request pipelines from forked repositories. If a repository is public and allows pipelines from forks, external users can trigger jobs that gain access to unprotected CI/CD variables. For private repositories without forks this risk is lower, but the basic question remains: why would a Magento team that stores production SSH keys in GitLab expose those keys on someone else's infrastructure?
3. Private runner: isolation at project and group level
A group runner registered for a group with multiple Magento projects brings a substantial improvement over shared runners: it only executes jobs belonging to that group. Other GitLab groups or public projects have no access to this runner. This significantly reduces the attack surface: only members of your own group can trigger pipelines that run on this runner.
A project runner, assigned exclusively to a single project, offers the strongest isolation at GitLab level. Jobs from other projects cannot use this runner, even if they belong to the same group. This model makes sense for production deploy runners that store SSH keys for production servers: only pipelines from that one project can use this runner and, with it, the stored secrets.
4. Self-hosted runner: full control and its cost
A self-hosted runner, a machine your own team operates gitlab-runner on, gives you complete control over the operating system, network access, installed tools, stored credentials and execution environment. No shared-runner operator, no cloud provider and no other organization has access to this machine. This is the strongest isolation model, but it comes with corresponding operational overhead: patch management, monitoring, backups, incident response for outages and access management for the team.
For Magento deployments with sensitive production SSH keys and database credentials, the self-hosted runner is the right choice in most cases. The deploy runner that opens SSH connections to production servers should always be self-operated. The build runner for composer install and setup:di:compile without production secrets, on the other hand, can run on GitLab.com shared runners, here the security guarantees of GitLab.com are usually sufficient, as long as no production credentials are exposed in the build job.
5. Secrets protection: how variables are handled in each runner type
GitLab CI/CD variables are stored encrypted on the GitLab server and injected into the runner environment as environment variables at job runtime. The difference between runner types lies in who controls the environment in which these variables exist. On a self-hosted runner, your own team controls which processes run on the machine, which ports are open and who has SSH access. On a cloud provider's shared runner, this control does not exist.
Masked variables in GitLab prevent the value from being shown in job logs. Protected variables are only visible in jobs on protected branches and tags. These protection mechanisms apply regardless of runner type, but they do not protect against a compromised runner host itself. An attacker with access to the runner process can read environment variables from the process context before they are masked. This is why the rule applies: the more sensitive the variable (production SSH key, database root password), the stronger the runner's isolation needs to be.
6. Secure self-hosted runner configuration for Magento
A secure baseline configuration for a self-hosted runner spans several layers: the runner process itself with minimal privileges, the Docker executor with resource limits and no privileged mode, and network configuration that restricts outbound connections to known destinations. The following configuration shows a production-grade setup for a Magento build runner without production secrets.
# /etc/gitlab-runner/config.toml - Secure self-hosted build runner
concurrent = 4
check_interval = 5
shutdown_timeout = 60
log_level = "warning"
log_format = "json"
[[runners]]
name = "magento-build-secure"
url = "https://gitlab.example.com"
token = "${GITLAB_RUNNER_TOKEN}"
executor = "docker"
limit = 2
maximum_timeout = 1500
# Only accept tagged jobs - no untagged job pickup
run_untagged = false
tag_list = ["build", "php84", "docker"]
[runners.docker]
tls_verify = false
image = "php:8.4-cli"
# Never run in privileged mode for build jobs
privileged = false
# Disable Docker socket access - no container escape via /var/run/docker.sock
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
# Allowed volumes: only cache, no host system paths
volumes = ["/runner-cache:/cache:rw"]
# No extra capabilities
cap_add = []
cap_drop = ["ALL"]
security_opt = ["no-new-privileges:true"]
memory = "3g"
memory_swap = "3g"
cpus = "2.0"
# Isolated network - no host network access
network_mode = "runner-net"
# Pull always to prevent stale image attacks
pull_policy = ["always"]
[runners.cache]
Type = "local"
Shared = false
Three security aspects in this configuration matter most: privileged = false prevents jobs from using container escape techniques. cap_drop = ["ALL"] combined with an empty cap_add removes all Linux capabilities from the container. network_mode = "runner-net" places the container in an isolated Docker network with no access to the host network or internal services. Different rules apply for the deploy runner that needs SSH keys, but that runner should run on its own host with significantly stricter access controls.
7. Network isolation and privileged mode
Privileged mode is the most dangerous switch in the runner configuration. A container in privileged mode has near-complete access to the host kernel and can escape Linux namespaces. GitLab.com disables privileged mode by default. On self-hosted runners, some teams enable it for Docker-in-Docker builds (dind), that is, for jobs that build Docker images themselves. For Magento build jobs, Docker-in-Docker is not necessary and should stay disabled.
Network isolation protects against a compromised job reaching internal services on the runner host's network. Without isolation, a job running on the build runner could open connections to internal services such as Redis, RabbitMQ or database servers located on the same network segment. With an isolated Docker network (network_mode = "runner-net"), the container only has access to explicitly allowed destinations, for example GitLab itself for artifact uploads and Packagist for Composer.
8. Direct security comparison
The following comparison summarizes the security characteristics of the three runner types for a typical Magento deployment context. The ratings refer to usage with production SSH keys and database credentials.
| Property | Shared Runner | Group/Project Runner | Self-Hosted Runner |
|---|---|---|---|
| Job isolation | Container per job | Container per job | Fully controlled |
| Who has access? | All projects on the instance | Only own group / project | Only own pipelines |
| Network control | No own control | No own control | Fully configurable |
| Production SSH keys | Not recommended | Conditionally suitable | Recommended |
| Operational overhead | No overhead of your own | Own runner instance | Host, OS, monitoring, updates |
The recommendation for most Magento teams is this: build jobs (without production secrets) can run on GitLab.com shared runners or on a group runner. Deploy jobs (with a production SSH key) must run on a self-hosted runner operated on its own host with strict access controls. This split combines the convenience of shared runners for non-critical builds with the security of self-hosted runners for critical deployments.
9. Common security mistakes in runner configuration
The most common mistake is storing production SSH keys as unprotected variables (without the protected flag). Unprotected variables are visible in every job, including jobs on feature branches and in merge request pipelines. On a shared runner, this means the key, even if only temporarily, exists in a container environment that is not fully under your own control. Solution: always create SSH keys as protected and masked variables, and only deploy from protected branches.
A second serious mistake is enabling privileged = true on build runners that do not need Docker-in-Docker builds. Teams often enable privileged mode because a job seems to require it, without understanding the consequences. In privileged mode a job can access the host's Docker socket (/var/run/docker.sock), inspect every container on the host and start arbitrary containers with host network access, which is a complete container escape. A third mistake: registering a runner without network limits in a network segment that contains database servers. Even without privileged mode, a job can reach internal services over the network if no network isolation concept has been implemented.
10. Summary
Choosing the right runner type is a security-critical decision for Magento teams, not merely an infrastructure one. Shared runners offer zero overhead but minimal control over isolation and network access. Self-hosted runners offer maximum control but require operational effort of their own. The pragmatic solution is a split: non-critical build jobs on shared or group runners, production deployments exclusively on self-hosted runners with strict network and privileged mode configuration.
Three rules that allow no exceptions: never expose production SSH keys on shared runners. Only enable privileged mode when Docker-in-Docker is genuinely needed. Store all variables with production access as protected and masked, so they are only visible on protected branches. With these basic rules you can build a GitLab CI/CD setup that will also hold up under a security review.
Runner Security: The Key Points at a Glance
Shared Runner
No overhead of your own, but no control over isolation and network. Not suitable for production SSH keys and database credentials.
Self-Hosted Deploy Runner
Own hardware, own network control, privileged = false, cap_drop ALL. Mandatory for jobs with production SSH access.
Protected Variables
Always protect and mask production secrets. Limit visibility to protected branches, regardless of runner type.
Network Isolation
Isolate the Docker network for runner containers. No access to internal services without explicit configuration.
11. FAQ: Runner Security in GitLab
1Are shared runners on GitLab.com secure?
2Protected vs. masked variables?
3When is privileged = true justified?
4Is a group runner secure for production deploys?
5Purpose of run_untagged = false?
6Protecting the Docker socket on runner hosts?
7Why is network isolation important?
8Forks and secrets?
9Limiting Linux capabilities in the container?
10SSH keys in GitLab CI/CD?
Decision guide: which runner type for which job?
For production deployments: a dedicated, locked-down runner with privileged = false. For code quality checks (PHPStan, PHPCS): a shared runner is sufficient. For artifacts with a vendor cache: a group runner with a persistent cache volume. For integration tests against a real database: a self-hosted runner with Docker isolation.
Runner security is not a one-time setup, it has to grow with the infrastructure. New projects, new team members and new deployment environments call for regular review of the runner configuration and access rights.