Open-source project
gitlabhq/gitlab-shell avatar
gitlabhq/gitlab-shell

gitlab-shell: the SSH front door for GitLab, and the rules for cutting a release

Replacement for gitolite. This repo is a mirror of https://gitlab.com/gitlab-org/gitlab-shell/

564 stars320 forksGoMIT

At a glance

What is it?
GitLab Shell handles Git SSH sessions for GitLab and maintains the list of authorized keys, and it is not a Unix shell. What is unusual about the repository is the release documentation, which is written around a mirror topology where a security release is tagged first in a separate repository and the version is bumped by a bot rather than by a person.
Who is it for?
gitlab-shell is not a project most engineers will adopt, since it is the SSH half of a GitLab installation, but it is worth understanding for anyone running self-hosted GitLab or working on GitLab's own stack. The part to read before touching a release is the tag management guidance, because the repository is mirrored to security and dev mirrors within minutes and recreating a tag that has already synced causes SHA divergence and breaks the mirror sync.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What it does, and what it explicitly is not

GitLab Shell handles Git SSH sessions for GitLab and modifies the list of authorized keys, and the README removes a likely ambiguity in the first line: GitLab Shell is not a Unix shell nor a replacement for Bash or Zsh.

That is worth pausing on because the name invites the wrong assumption. The component is a server that GitLab runs, and it speaks the SSH protocol to authenticate a user and then hand the session to Git, either to serve a repository over the SSH transport or to run an administrative command. The authorized keys part is the other half: the service is the thing that knows which keys are valid for a given account, and it maintains that list.

GitLab supports Git LFS authentication through SSH, so the same path that serves a repository also carries the LFS handshake.

The project is written in Go, and the module path is gitlab.com/gitlab-org/gitlab-shell/v14, so the major version is carried in the import path, which is the Go convention for a component with a stable API contract. It targets Go 1.25.10, which is worth noting when you are pinning a toolchain for a build.

The repository you are reading is a mirror. The description says it is a replacement for gitolite, and that this repository is a mirror of the canonical project at gitlab.com/gitlab-org/gitlab-shell, and the badges at the top of the readme point at gitlab.com for the pipeline status and the coverage report. The GitHub copy exists for the ecosystem that reaches for it, and the actual development happens on GitLab.

The licence is MIT, with the LICENSE file at the root as the authority for what applies to which files.

The five directories, and what belongs in each

The project structure table is short and is organised around the rule that some code is a public interface and some is not.

cmd/ holds the commands that will ultimately be compiled into binaries, so this is the user-facing entry point. internal/ is internal Go source code that is not intended to be used outside of the project or module, which is the Go convention that lets the compiler enforce the boundary. client/ is HTTP and GitLab client logic used internally and by other modules, with Gitaly named as the example. bin/ is where compiled binaries are created. And support/ holds scripts and tools that assist in development and testing.

That client/ directory is the one that shows the project is a component of something larger rather than a standalone tool. The dependency list confirms it. The module depends on gitlab-org/cells/topology-service, on gitaly/v18 at a release candidate, and on labkit in two major versions, v1.64.11 and v2.61.0, alongside the Prometheus client, the OpenFeature Go SDK, gRPC and protobuf, and golang-jwt.

Two of those dependencies are worth calling out. The gitaly dependency at v18.9.0-rc4 means the SSH service and the Git storage layer are versioned together, which is the main upgrade hazard in this codebase. And the presence of both labkit major versions suggests a migration is under way, since the two cannot both be the current interface.

The Git LFS dependencies are also visible: charmbracelet/git-lfs-transfer and git-lfs/pktline, which are the pieces that implement the LFS batch and transfer protocol over the SSH connection.

The repository top level has the usual GitLab furniture alongside the code, including a .gitlab-ci.yml, a lefthook.yml for git hooks, a Dangerfile for merge request checks, a .golangci.yml for the linter, a config.yml.example for configuration, a docker-compose.yml, an acceptance/ directory, and a PROCESS.md which is the closest thing to a design document.

Building, testing, and a Makefile that pins its own tools

The build entry points are minimal. Run make or make build to build, and make test to run the tests.

The Makefile is more interesting than that, because it shows a project that pins the tools it depends on rather than trusting whatever is on the machine. It uses bash with pipefail so that failures in a pipeline, for example a curl piped to tar, are not masked by the exit status of the last command in the pipe.

The pinned tools are the Go test runner at 1.12.0, the Go linter at 2.12.2, and the labkit validator at v2.61.0, each downloaded into a support/bin directory at a fixed version. The SSH audit tooling is pinned too, at 3.9.0, with the policy file for the audit living at support/ssh-audit/gitlab-sshd.policy.

The FIPS handling is a detail that says a lot about the deployment context. The Makefile sets a FIPS_MODE variable defaulting to 0, and the SSH audit policy can be overridden for the FIPS build. The reason given in the comment is that ED25519 keys are not usable under the FIPS module, since they are rejected at load time, so the FIPS job overrides the host key types to rsa and ecdsa.

The build tags are tracer_static, tracer_static_jaeger and continuous_profiler_stackdriver, so the tracing and profiling integrations are compiled in rather than being separate binaries. The build is reproducible in the modern sense, with SOURCE_DATE_EPOCH honoured so the build timestamp is fixed, and the version string derived from git describe with a fallback to the VERSION file.

The architecture is detected and normalised, with x86_64 becoming amd64 and aarch64 becoming arm64, which is the naming the Go toolchain and the container images expect.

The test story is broader than the unit tests. The declared targets include test, test_fancy, acceptance-test, coverage, ssh-audit-test, ssh-audit-generate-policy, validate, verify, lint, check and install, and there is a separate acceptance/ directory and a docker-compose.yml that starts a Gitaly container for the integration work:

The release process, and why a Renovate bot opens the version bump

The release documentation is the longest part of the README and the most instructive, because it describes a process that is deliberately not a single developer pushing a tag.

Releases are automated by the release:prepare and release:tag CI/CD jobs, which run support/release.sh. The sequence is: in the latest main pipeline you run the manual release:prepare job, and it opens a Release vX.Y.Z merge request that updates the VERSION and CHANGELOG files with the merge requests merged since the latest tag. The minor version is bumped by default, and to bump the patch or major version you set the RELEASE_BUMP variable to patch or major when running the job. You can preview the changelog locally with support/release.sh changelog.

You then review and merge that merge request, editing the CHANGELOG if needed while keeping the first line as vX.Y.Z. When the merge request is merged, the release:tag job creates the vX.Y.Z tag and its release, with the version's changelog entries as release notes.

The step that surprises people is the fourth. Renovate opens a gitlab-org/gitlab merge request to update the GITLAB_SHELL_VERSION file in the main GitLab repository to the new tag, and the instruction is to review that merge request instead of creating a new one. Then a version is announced in the #gitlab-shell channel.

So the version bump in GitLab itself is a bot's merge request. The human releases the component; the bot tells the parent application which version to use.

The manual fallback is documented too. If the jobs cannot be used, follow the same steps manually, create the release merge request, create an annotated vX.Y.Z tag on its merge commit and let Renovate bump GITLAB_SHELL_VERSION. Re-running release:tag for the merge commit afterwards creates the missing release for a tag created by hand.

The mirror topology, and why a version can already be taken

The reason the release jobs are as careful as they are is the mirror topology, and the explanation is the most useful part of the documentation for anyone who might cut a release by accident.

Both jobs refuse a version that is already tagged in gitlab-org/security/gitlab-shell. Security releases are tagged there first, and those tags are not visible here until the security release is synced back. Releasing the same version from this repository would make the repositories diverge. If a job fails for this reason, you wait until the security release is synced back, or you choose the next free version.

The tag management guidelines then spell out the mirror rules. This repository is mirrored to security and dev mirrors, mirror syncs happen within minutes of changes to the canonical repository, and so the guidelines are about how to behave when a tag has to change.

Use caution when deleting and recreating tags, because if a tag has already been synced to the mirrors, recreating it causes SHA divergence and breaks the mirror sync. If a tag needs to be re-created, you create an issue on the Delivery tracker and coordinate with the release managers to delete the tag from all mirrors, both security and dev, before recreating it on canonical. And if a mirror sync fails due to tag SHA divergence, you contact the release managers in the g_release_and_deploy channel to fix the mirrors manually.

The two required CI/CD variables explain the rest. GITLAB_SHELL_RELEASE_TOKEN is a project access token with the api scope and the Maintainer role, and the note is that CI_JOB_TOKEN cannot be used because it cannot create merge requests, and tags created with it do not trigger tag pipelines. SECURITY_REPO_READ_TOKEN is a token with the read_api scope and at least the Reporter role in gitlab-org/security/gitlab-shell, which is what lets the job check whether a version is already taken there.

The last push to this mirror was on 2026-10-22, and the release process has clearly been running for a long time, since the example merge request in the documentation is for version 14.58.0.

Editorial conclusion

gitlab-shell is not a project most engineers will adopt, since it is the SSH half of a GitLab installation, but it is worth understanding for anyone running self-hosted GitLab or working on GitLab's own stack. The part to read before touching a release is the tag management guidance, because the repository is mirrored to security and dev mirrors within minutes and recreating a tag that has already synced causes SHA divergence and breaks the mirror sync. Before you release, check whether a security release has already taken the version number, because both release jobs refuse a version that is already tagged in the security repository, and the version bump in the main GitLab repository is opened by Renovate rather than by hand.

Frequently asked questions

What is GitLab Shell?

It handles Git SSH sessions for GitLab and modifies the list of authorized keys. The README is explicit that it is not a Unix shell nor a replacement for Bash or Zsh, and it also provides Git LFS authentication over SSH.

How do I build and test gitlab-shell?

Run make or make build to build, and make test to run the tests. The Makefile pins its own tooling, with the Go test runner, the Go linter, the labkit validator and the SSH audit tool each downloaded at a fixed version into support/bin.

How is a gitlab-shell release cut?

Run the manual release:prepare job in the main pipeline, which opens a Release vX.Y.Z merge request updating VERSION and CHANGELOG, then merge it and the release:tag job creates the tag and its release. Renovate then opens a separate merge request to update GITLAB_SHELL_VERSION in the main GitLab repository.

Why does the gitlab-shell release job refuse some version numbers?

Because both jobs refuse a version that is already tagged in gitlab-org/security/gitlab-shell, where security releases are tagged first and are not visible here until they are synced back. Releasing the same version from this repository would make the two repositories diverge, so you wait for the sync or pick the next free version.

Official sources

  1. gitlabhq/gitlab-shell on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/gitlabhq-gitlab-shell.svg)](https://hysenlabs.com/projects/gitlabhq-gitlab-shell)

Was this useful?