Setting up your private PowerShell module factory

This post describes how to set up a self-hosted GitLab instance and use it for maintaining and distributing your own PowerShell modules within your organization. You can use it to maintain and distribute any other kind of software as well, but PowerShell is my main driver so I will focus on that. My PSConfEU 2026 talk describes the motivation behind the „PSFactory“, as we will call it going forward, in more detail. You can find the talk [here – video coming soon!].

The environment

To illustrate the setup, we are using a typical business environment:

  • An Active Directory forest with a root domain (wtf-it.com) and a child domain (work.wtf-it.com)
  • AD-intgerated DNS hosted on domain controllers
  • An ADCS-based two-tier enterprise PKI
  • Our contributors are not, strictly speaking, developers, but rather enterprise scripters who nevertheless understand the value of source code management

The goal

We want to have the ability to collaborate on PowerShell modules, properly version them and, once an updated version has beeen created, to publish them for consumption for our internal users. The consumers‘ experience in locating, installing and updating modules should be as seamless as possible, similar to the native PSGallery experience of a „non-isolation“ PowerSheller.

Laying the foundations

Add A record to DNS

Our PSFactory appliance will be listening on 10.0.101.71 and reachable via psfactory.wtf-it.com.

Create an SSL certificate

Both our contributors and our consumers will be accessing PSFactory via HTTPS. Let’s request a server certificate from the entreprise PKI from a domain member machine and export it as PFX. The certrificate must have the following characteristics:

  • psfactory.wtf-it.com in the SAN (I have also put it in the Subject for uniformity)
  • Server Authentication EKU

Prepare the PSFactory server

For GitLab, we need a Linux server. I will be using a Rocky Linux 10.1 Server instance because I love Rocky Linux.  Although Rocky is not on the official list of supported distributions, following the instructions for RHEL 10 will absolutely get you there. Although we are supposed to be working in isolation, for the installation part I will assume Internet connectivity, specifically to the package repositories involved.

For PSFactory, I have set

  • DNS servers to be the DCs of root domain (10.0.101.11 + 10.0.102.12),
  • hostname to psfactory and domain suffix to wtf-it.com to match the DNS entry created above and the name in the SSL certificate

These can be configured at install time or updated later by using NetworkManager.

We will be using the „Linux Package“ installation procedure detailed in https://docs.gitlab.com/install/package/ . Did I mention that GitLab has outstanding documentation?

Configure disks (optional, but highly recommended)

We will start with two SCSI disks (e.g. /dev/sdb for gitlab and /dev/sdc for repository, although we may not use the repository one in the end) I recommend creating LVM volumes and hosting the entire GitLab installation on those because this will give you the option to extend the disks without disrupting operations. To achieve this, proceed as follows:

Now you need to make the mounts permanent. Make a note of the partition’s UUID in the last command’s output (yours will be different from the one shown below!). Open fstab with write permissions:

Add the following lines at the end:

Reload and verify:

If you decide to skip this step, all of GitLab will be installed into the root partition. For playing around, this may be a viable option.

Add internal CAs as trusted

Export your PKI chain’s certificates as base64-encoded .cer files and transfer them to the Linux box using SCP. A rudimentary SCP client is preinstalled with Windows, or you can use WinSCP. Assuming the file names root_ca.cer and issuing_ca.cer, add them to the system’s trust store like this:

verify the result (our CAs are called „WTF Root CA“ and „WTF issuing CA“):

Open firewall ports

One of the things I love about Red Hat derivatives is that they come with firewall closed by default for inbound trafic except for ICMP, SSH and Cockpit, as befits a modern operating system. Our users, both consumers and contributors, will be using HTTPS, and we can also open HTTP as GitLab will redirect to HTTPS anyway (if we configure it to do so, which of course we will).

Let’s make sure the necessary prerequisites are installed as per installation instructions, although on Rocky 10 they should all be there already:

After we have verified these packages, we are ready to install GitLab!

Install GitLab

Before you kick off the installation, there is one decision to be made: Whether you will be using the free Community Edition (gitlab-ce) or the paid Enterprise Edition (gitlab-ee). Enterprise has lots of additional functionality; however, the added value specifically for PowerShell module development is limited, so we will stick with Community Edition for this exercise. But if you decide to try out Enterprise, just replace the package name in the commands below!

The last item you have to provide is a password for your administrator user („root“). <strongpassword> must be at least 8 characters and should not contain exclamation marks, or you have to escape them properly for bash!

This package is around 1.5 gigabyte so the installation can take a while.

Enable trusted HTTPS

The GitLab installer has created a self-signed certificate for you. To replace it with your PKI-issued certificate, copy the PFX file to the Linux box and run:

Edit the config:

enable HTTP to HTTPS redirection:

Restart to persist changes:

That’s it! Open your browser, head over to https://psfactory.wtf-it.com and start configuring your PSFactory. Not all configurations can be made in the Web GUI, we will still need to drop into the shell for at least two of them.

Enable enterprise authentication

We don’t want our contributors to maintain an additional set of credentials for PSFactory. Good thing they can use their Active Directory accounts to log on to it. Our forest does not yet have LDAPS deployed (e.g. because GitLab is the first app to require a simple bind, and deploying certificates to domain controllers needs a change approval). To identify our potential contributors, we will set their employeeType attribute to psdev.

Unsurprisingly, the GitLab team has created great documentation about all the different authentication options: https://docs.gitlab.com/administration/auth/ldap/ But for now, we’ll just configure basic LDAP. Create a bind account in AD – it doesn’t need any particular permissions, but of course, a non-expiring password, if you can bring yourself to have such a user in your forest, helps ensure that authentication continues working without interruption.

Edit the config:

Add LDAP servers(s):

GitLab will perform a periodic AD scan for new user matching the configured criteria. If you don’t want them to be able to log on without the administrator knowing, set new users to be blocked so they must be approved by administrator:

Save the file, then apply the updated configuration:

The next important step is to disbale user signup. This setting has been ripped out of configuration files 10 years ago, so that the admin needs to log on to the Web UI:

 

Enable SMTP notifications

You probably want your contributors to get notified about merge requests they have to approve, builds failing and so on. The email configuration is another case where you have to get into the weeds of the GitLab configuration file.

In our case, the mail server will accept submissions without authentication so the configuration is very basic:

We leave the smtp_authentication line commented out here to signal that authentication should not be tried.

GitLab offers a lot of SMTP configuration examples at https://docs.gitlab.com/omnibus/settings/smtp. If, despite all isolationist tendencies, Microsoft Exchange Online is your email provider of choice, GitLab has you covered here as well: https://docs.gitlab.com/omnibus/settings/microsoft_graph_mailer/!

There is no built-in nice „Test Mail“ button in GitLab. If you want to make sure email delivery works before putting your GitLab instance in production, load the GitLab console with

wait for the gitlab(prod) prompt to appear, and then fire off email submission using

and follow the running output.

Remove calls to external services

GitLab, even if you host it yourself, reaches out to several internal services to grab some content or provide additional functionality. Since we are in the business of working in isolation, let’s disable what we can.

Gravatar

Disable Gravatar in Account and limit >> Gravatar enabled. GitLab documentation suggests the following Ruby setting:

At the time of writing, this setting is not part of the file, and adding it does not have the effect of disabling Gravatar.

Diagrams.net

Integration with the default embed.diagrams.net server is enabled by default and should be disabled to prevent attempting to connect to that service (General >> Diagrams.net). You can reconfigure it later if you decide to host your own diagrams.net server in isolation.

Customer experience improvement and third-party offers

Enabled by default. Disable it in isolation.

WebIDE assets and single-origin fallback

GitLab offers a rich WebIDE (VS Code online) which, by default, is configured to grab static assets from an external CDN. However, if that domain becomes unavailable or is blocked by CORS, GitLab will serve the static assets from the main application. This is considered a security risk, but in our scenario it’s perfectly acceptable. Acknowledge the warning so that you don’t get nagged anymore.

Managing the code

Let’s configure some general settings that will make source code management within your PSFactory somewhat stricter, but without turning your bunch of enthuastic enterprise scripters into a compliance-driven DevOps organization.

Groups

For reasons that will become apparent in a bit, I very much recommend subdividing the scripting space into groups. Other than in classic authorization systems, groups do not just determine who can access what, but rather create a namespace structure. This is reflected in the paths of your projects‘ repositoritories – in GitLab, you cannot have a „top-level-project“ so that any project that is not in a group will end up in the personal namespace of the user who created the project – not a very manaeagable proposition.

That said, there is nothing inherently wrong with having personal repositories for tryi ng things out. And if a project that had started out as a personal playground ultimately takes flight and has to be made accessible to a wieder audience, GitLab supports moving projects from one namespace to another!

How you subdivide your modules into groups depends on what

Branch naming and protection

We absolutely want branch protection in our PSFactory. Thisis not just to prevent enterprise scripters from overwriting each other’s work, but also to be able to pinpoint – by automation! – those distinct moments in the code’s lifecycle when a branch is merged into the main codebase. And branches will server yet another purpose in our development process!

Branch protection is configured in the Admin Area unser Repository:

The above configuration is just a suggestion that worked well for me. But you can, of course, relax or tighten the merge permissions as suits your purposes.

Projects and templates

Now you have your groups set up, it’s time to kick off your first project. It will probably not be a completely new development but rather a transfer of existing work from the unmanaged, chaotic process to a central GitLab-based repository.

The“Create from template“ option immediately looks very promising if you intend to transfer lots of modules into PSFactory (or plan on starting to turn out modules like sausages). Unfortunately, PowerShell is not a prominent-enough citizen of the GitLab universe to warrant its own template. And managing templates including creating your own and disabling the templating function altogether is a feature that is only available in the paid Enterprise Edition. We will create our own powerful templating facility at the end of this post, but as long as you confine yourself to the Community Edition, there is no way to not have templates offered to contributors.

For the next steps I am assuming that the contributors have Git installed and configured on their workstations. The best way to do it, if there is one, is outside of the scope of this post. PowerShellers are likely to use Visual Studo Code as their Git frontend, but if one of them prefers (or is stuck with) pure CLI git, the defauilt ReadMe created in a new GitLab project (if you leave the box „Initialize repository with a readme“ checked, that is) has detailed instructions on how to get started. And the „Code“ dropdown on the project’s front page offers lots of helpful options:

The creation of the readme.md file in the new project repo already constitutes the first commit, which is why we allowed it in the branch protection settings above). To add your work to the repository, create a new branch, drop your files in the local repo folder, commit, publish and create a merge request. For your own projects, the above branch protection settings do not require any review or approval – as the maintainer and owner, you are allowed to merge (even if you are not, for your own and everyone else’s good, allowed to push to the main branch!)

Building modules

Code management is fine, but we want to take our contributors‘ module production to the next level.

Install a Windows Runner

Although the default setting allow basically anyone who is a member of a group or a project to create a pipeline runner for that scope, I recommend that the runner arsenal be kept under central supervision and management by the instance admins(s). Especially if your contributors decide not to use tags in their repositories, runners have to be set to run untagged jobs, and this can make the runner selection messy.

To restrict runner creation

Log on as an instance admin, switch to „Admin Area“, then go to Settings >> CI/CD >> Runners and disable runner creation by group and project members:

To create a runner

Log on as an instance admin, switch to „Admin Area“, then open the CI/CD >> Runners menu and select „Create instance runner“:

Make sure to check the „Run untagged jobs“ box, unless you want to start using tags from the very beginning!

After clicking „Create Runner“ you are presented with the choice of runner platforms. Select „Winodws“ and copy the registration command:

To install and register  a Windows runner

For PowerShell development, a Windows runner is a good choice. You can run it on a Windows machine of your choosing, even on your development workstation. The machine must

  • have PowerShell 7 installed
  • have git installed
  • be able to resolve and reach the PSFactory GitLab server by FQDN.

Download the current gitlab runner executable from https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe and save it as gitlab-runner.exe

Create a folder on the machine, copy the executable there, then navigate to that frolder from CMD or PowerShell and run

Then use the registration command copied from the previous step. You can accept the suggestions for the URL and the runner name. The final question is about the „executor“ of the new runner. Type in „shell“, and you have registered a runner that defaults to pwsh!

Now you’re ready to create some CI/CD to automate your module creation and publishing! Head over to the [next post] for a sample configuration!

Image by zephylwer0 from Pixabay