The article explores how moving GitLab’s control plane into an OpenStack cloud creates greater digital sovereignty, while highlighting the trade-offs between control, complexity, and operational responsibility.

image

The Initial Paradox :

When I started building my platform engineering setup on OpenStack, one detail bothered me. I chose OpenStack specifically for sovereignty: keeping my data and processing within a perimeter I control.

Yet, my CI/CD relied on GitLab.com a SaaS service hosted elsewhere.

The question arose: where does my code actually go? My repositories, pipeline secrets, artifacts, infrastructure state… all of this lived on an external platform. I had a sovereign private cloud, and on top of it, a non-sovereign entry point.

This article shares how I bridged that gap and above all, what I learned along the way about what “sovereign” really means.

Sovereignty Is Not Binary: Execution vs. Control

The first lesson was conceptual. I was already using a self-hosted GitLab Runner within my OpenStack tenant, thinking I had solved the issue. That was true… halfway.

Two planes must be distinguished:

  • The execution plane: where the code runs. With a runner in my tenant, my CI jobs run locally. Code is cloned, built, and tested within my perimeter. That part was sovereign.

  • The control plane: where the data lives. Repositories, Git history, CI variables (passwords, tokens), artifacts, logs, Terraform state… all of this remained on GitLab.com.

In other words, my execution was sovereign, but my control was not. The entry point of my entire system GitLab itself was external. If GitLab.com suffered an outage, a legal constraint, or a leak, my repositories and secrets would be affected. A sovereign runner only protects what happens during the job, not what is stored around it.

Why does this matter for a setup like mine? Because the control plane is where the keys are. My repositories don’t just hold application code — they hold Terraform state describing my entire infrastructure, CI variables containing cloud credentials, and the pipeline definitions that can create or destroy resources in my tenant. An external platform holding all of that means a single ToS change, account suspension, regional outage, or credential leak on their side becomes an incident on mine. I wasn’t trying to build something GitLab.com couldn’t handle; I was trying to remove the assumption that a third party would always be there, always be reachable, and always be under the same jurisdiction as my data.

To take the logic all the way, I needed to bring the control plane back: hosting GitLab entirely within my OpenStack cloud.

The Foundation: Preparing the OpenStack Environment :

Before installing GitLab, the infrastructure foundation must be robust. I provisioned an Ubuntu VM within my OpenStack tenant, ensuring it has the necessary network connectivity to reach the repositories for the Omnibus package installation.

To achieve this, I attached a Floating IP (FIP) to the instance. This allows the VM to communicate with the outside world while maintaining the ability to route traffic to and from my local environment.

I verified the network configuration with a simple ping test to ensure the instance could reach external mirrors, confirming that the egress traffic was correctly routed through the OpenStack network.

For this specific instance, we used these resources in terms of RAM and CPU to handle the entire creation process.

 

With network connectivity confirmed and the instance reachable, the infrastructure was ready for the GitLab installation.

Bringing the Control Plane Home: GitLab in the Tenant

I provisioned an Ubuntu VM via OpenStack (Nova for compute, Neutron for networking, Glance for images), then installed GitLab Omnibus the all-in-one package bundling Nginx, PostgreSQL, Redis, and Sidekiq.

curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash

sudo EXTERNAL_URL="http://10.10.10.127" apt-get install -y gitlab-ee

First access used the generated root password, followed by essential security settings: disabling open registration and forcing private project visibility.

On a sovereign instance, no one should be able to create an account without approval.

Barbican: The Sovereign Brick for TLS

This is where OpenStack integration proves its value. Rather than leaving a TLS certificate lying around in a file, I stored it in Barbican, OpenStack’s secrets management service encrypted, with access control via Keystone.

openstack secret store --name 'gitlab-tls-crt' \
--secret-type 'opaque' --payload-content-type 'text/plain' \
--payload "$(cat gitlab.crt)"

openstack secret store --name 'gitlab-tls-key' \
--secret-type 'opaque' --payload-content-type 'text/plain' \
--payload "$(cat gitlab.key)"

As you can see, the certificate and the key have been created and securely stored in Barbican as well.

The certificate now lives in a vault managed by OpenStack, not in a stray file. I then enabled HTTPS on the GitLab side by pointing Nginx to this certificate and changing external_url to https://

A sudo gitlab-ctl reconfigure later, GitLab responded via HTTPS.

Closing the Loop: A 100% Sovereign Runner and Pipeline

Before registering the runner via the command line, it must be defined within the GitLab instance to generate the necessary authentication token.

  • Navigate to the Runner settings: In your GitLab project (or at the instance level for global access), go to Settings > CI/CD > Runners.
  • Create a new Runner: Click on the “New project runner” button.
  • Configure the platform: Select the operating system (e.g., Linux) and enter optional tags. Using the tag sovereign is highly recommended to ensure your jobs are strictly routed to your local OpenStack infrastructure.
  • Get the Token: After clicking “Create runner,” GitLab will display a unique registration token.

 

This token is the “handshake” secret that allows your local VM to securely authenticate with your sovereign GitLab instance.

Once you have this token, you can proceed to the registration command on your VM and before running the registration command, we prepare the local certificate authority by creating the certificate directory and copying GitLab’s self-signed TLS certificate into it.

Then, the gitlab-runner register command links the runner to our internal instance using the non-interactive mode. Key parameters include the Docker executor (–executor docker), host networking (–docker-network-mode host), the sovereign tag for job routing, and the TLS CA file path (–tls-ca-file) to ensure secure communication with our HTTPS-enabled GitLab server.

To make sure that the GitLab Runner was created successfully, we executed the following commands:

Once registered, a minimal .gitlab-ci.yml validated the loop:

As you can see, the job was executed successfully as expected on our GitLab Runner.

Key Takeaway: Sovereignty Is a Spectrum

The biggest lesson from this project is that there is no “sovereign / non-sovereign” toggle switch. There are levels:

All-SaaS (zero sovereignty).

– Self-hosted runners (sovereign execution, external control).
– Self-managed GitLab (sovereign control and execution).
– Beyond: internal object storage, private CA, high availability…
– Each level has a cost in resources, maintenance, and complexity. Moving from level 2 to level 3, as I did, brings true control plane sovereignty, but also hands me the responsibility for backups, updates, and GitLab        availability. What GitLab.com used to do for me, I now must do myself.

The right question is therefore not “am I sovereign?” but “what level of sovereignty does my context require, and am I willing to bear the cost?”

For my part, running that first pipeline entirely within my OpenStack cloud was worth every hour of debugging. And if you are undertaking a similar journey, I would be curious to exchange thoughts on your own trade-offs.

Thank you for reading. Feel free to share your feedback or questions.