Runner Security: Shared Runner vs. Private Runner vs. Self-Hosted Runner
AI generated
CI/CD
.yml
GitLab · Runner Security · CI/CD · Magento
Runner Security:
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.

18 min read Isolation · Secrets · SSH Keys · Privileged Mode · Network GitLab SaaS · Self-Managed · Magento 2.4

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?
For non-critical build jobs without production credentials: yes. For jobs with production SSH keys or database passwords: no. Self-hosted runners are the right choice here.
2Protected vs. masked variables?
Protected: only visible on protected branches. Masked: value hidden in logs. Always combine both options for production secrets.
3When is privileged = true justified?
Only for Docker-in-Docker. For Magento build jobs (Composer, npm), privileged = false is the correct and safer setting.
4Is a group runner secure for production deploys?
Conditionally, only if it runs on a self-operated machine. A group runner on GitLab.com infrastructure is not suitable for production SSH keys.
5Purpose of run_untagged = false?
The runner only picks up jobs with its own tags. Prevents a deploy runner from picking up general build jobs and unnecessarily exposing capacity and secrets.
6Protecting the Docker socket on runner hosts?
Do not mount /var/run/docker.sock as a volume. Set privileged = false. Do not leave the gitlab-runner user in the docker group if dind is not needed.
7Why is network isolation important?
Without isolation, jobs can reach internal services (DB, Redis) on the runner host's network. With an isolated Docker network, only explicitly allowed targets.
8Forks and secrets?
Protected variables are not visible in MR pipelines from forks. For public projects, mark all sensitive variables as protected.
9Limiting Linux capabilities in the container?
cap_drop = ['ALL'] removes all capabilities. Then add back only what is explicitly needed via cap_add. Magento builds generally need no additional capabilities.
10SSH keys in GitLab CI/CD?
Store as protected and masked. Only allow deploy jobs on protected branches. Operate the runner yourself and restrict network access to known target servers.

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.