Semaphore UI
Overview
Semaphore UI is the open source web interface and REST API for running automation. It turns Ansible playbooks, OpenTofu and Terraform plans, Terragrunt, PowerShell and shell scripts into named templates that your team can run from a browser.
Semaphore UI supports scheduled and event-triggered runs, live streaming output, and a full task history that shows who ran what and when. It also provides role-based access control across projects, plus an API and webhook surface for wiring runs into an existing pipeline.
The SecureAnyCloud image delivers Semaphore UI 2.19.12 on Ubuntu 24.04, served over HTTPS by Semaphore itself. ansible-core 2.21.4 and OpenTofu 1.12.6 are installed alongside it, the datastore is empty, and every secret is generated on the first boot of your VM. The image is backed by 24/7 SecureAnyCloud support.
What you get:
- Semaphore UI 2.19.12 at
/usr/bin/semaphore, run by systemd as the unprivilegedsemaphoreuser. It is built from the project's community release artifact, so the image contains only MIT-licensed code. - The web interface and REST API served over HTTPS on port 443 with a certificate generated for your VM. Port 80 only redirects browsers to HTTPS and refuses anything that could carry a password.
- An embedded SQLite datastore at
/var/lib/semaphore/semaphore.db, so there is no separate database server to run, patch or back up. - ansible-core 2.21.4 in an isolated virtual environment at
/opt/semaphore-ansible, on Ubuntu's own Python 3.12, and OpenTofu 1.12.6 at/usr/local/bin/tofu. Semaphore registers both automatically. - No default login. The administrator account, the session cookie keys, the TLS certificate and the key that encrypts your stored secrets are all created on your VM's first boot, before the web interface is allowed to start.
- Self-service registration closed, including the runner registration endpoint, and project creation restricted to administrators.
- A
semaphoresystemd service, enabled and active.
Semaphore UI, Ansible, OpenTofu and Terraform are trademarks of their respective owners. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. This image packages the unmodified open source software, which is distributed under the MIT License.
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region.
Standard_B2s (2 vCPU, 4 GiB RAM) runs Semaphore and its engines comfortably for a small team. Choose a larger size if you run many tasks in parallel or against large inventories.
Configure these network security group inbound rules: 22/tcp from your management network, and 443/tcp for the web interface from your own addresses only. Step 11 explains why this matters more for this product than for most.
Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Semaphore UI by SecureAnyCloud, and select Create.
On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTPS (443). Then select Review + create and Create.
When the VM exists, narrow those two rules to your own source addresses (Step 11).
Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:semaphore-ui-ubuntu-24-04:default:latest
az vm create \
--resource-group my-resource-group \
--name my-semaphore \
--image sac1647283583153:semaphore-ui-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard \
--os-disk-delete-option Delete \
--nic-delete-option DeleteThen open the web port to your own address range:
az vm open-port --resource-group my-resource-group --name my-semaphore --port 443 --priority 1001Connect to your VM
ssh azureuser@<vm-ip>The login user is azureuser, or whichever administrator name you chose at deployment. The image carries no build-time account and no password login.
Confirm Semaphore is running
First boot takes under a minute. It generates this VM's TLS certificate, both session cookie keys, the key that encrypts your stored secrets, and the administrator password. It then creates an empty datastore, and only then allows the web interface to start. Until first boot finishes, the interface is deliberately not reachable.
sudo systemctl is-active semaphoreactiveCheck the product and its engines:
semaphore version && ansible --version | head -1 && tofu version | head -12.19.12-012ed06-1788086239 ansible [core 2.21.4] OpenTofu v1.12.6Semaphore discovers its engines on PATH when it starts, so these are exactly the binaries your tasks will run.
Check what the network can reach
sudo /usr/local/sbin/semaphore-port-check.shoff-box TCP exactly [22 80 443] SEMAPHORE_PORTS_OKThis is the complete off-box surface of this image: SSH, the HTTPS front door, and the HTTP redirect. Semaphore terminates TLS itself, so there is no reverse proxy in front of it and no second certificate to rotate. The datastore is a file on disk, so no database server listens at all.
sudo ss -Hltn | awk '{print $4}' | sort -u*:443 *:80 0.0.0.0:22 127.0.0.53%lo:53 127.0.0.54:53 [::]:22Retrieve the first-boot credentials
sudo stat -c '%A %U:%G %n' /root/semaphore-credentials.txt /etc/semaphore/config.json-rw------- root:root /root/semaphore-credentials.txt -rw-r----- root:semaphore /etc/semaphore/config.jsonsudo cat /root/semaphore-credentials.txtThe file records this VM's web address, the administrator login and the password generated for it. None of these values exists in the image. Every value is created on this VM's first boot, before the web interface serves a single request.
The configuration file alongside it holds three more per-VM secrets. The most important is access_key_encryption, which encrypts every SSH key and secret you later store in Semaphore:
sudo python3 -c "import json; d=json.load(open('/etc/semaphore/config.json')); print('\n'.join('%-26s %s' % (k, ('set, %d chars' % len(d[k])) if k in d else 'absent') for k in ('access_key_encryption','cookie_hash','cookie_encryption','runner_registration_token')))"access_key_encryption set, 44 chars cookie_hash set, 44 chars cookie_encryption set, 44 chars runner_registration_token absentThe last line is deliberate. POST /api/internal/runners is unauthenticated by design, and a configured global registration token would let anyone who can reach this server register a runner that executes tasks. Leaving the token unset closes that door. Step 7 proves it is closed.
Confirm the security posture
Before you sign in, confirm from the command line that nobody else can:
sudo /usr/local/sbin/semaphore-verify-login.sh /root/semaphore-credentials.txtfront door: this VM's credential accepted (204), cookie Secure+HttpOnly+SameSite, session usable refused: admin/admin and 6 other weak defaults, blank password, anonymous API, anonymous user creation self-registration: runner registration refused for every token; no global token configured transport: plaintext not served on 443; :80 redirects GET (307) and refuses POST (403) SEMAPHORE_LOGIN_OKEach check is a live request against your own running server, not a claim about the image. You can reproduce any one of them by hand, for example with the credential that every Semaphore tutorial uses:
curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Content-Type: application/json' \
--data '{"auth":"admin","password":"admin"}' \
https://127.0.0.1/api/auth/login401Sign in
Open https://<vm-ip>/ in a browser. The certificate is generated for your VM and is self-signed, so the browser warns on the first visit. Step 12 shows how to install a certificate for a real domain name.
Sign in as admin with the password from Step 6. A fresh instance has no projects yet. Create one from the project selector at the top left, then add a repository, an inventory and a task template.
Run your first task
A task template ties together four things: a repository holding your playbooks, an inventory describing the hosts, a variable group for environment values, and the playbook to run. Create them from the left-hand navigation in that order. Then choose Task Templates, then New Template.
Press the run button on the template. The task opens a live log. Semaphore clones the repository, installs any Galaxy requirements it finds, and streams the engine's real output as it runs.
The same function is available from the API. This command signs in with your own credentials and lists the apps that Semaphore has registered:
PASS=$(sudo grep '^SEMAPHORE_ADMIN_PASSWORD=' /root/semaphore-credentials.txt | cut -d= -f2-)
JAR=$(mktemp) printf '{"auth":"admin","password":"%s"}' "$PASS" | curl -sk -o /dev/null -c "$JAR" \
-X POST -H 'Content-Type: application/json' \
https://127.0.0.1/api/auth/login --data-binary @- curl -sk -b "$JAR" https://127.0.0.1/api/apps \
| python3 -c "import json,sys; d=json.load(sys.stdin); rows=d if isinstance(d,list) else list(d.values()); print('\n'.join(sorted(r.get('id','') for r in rows if isinstance(r,dict))))" rm -f "$JAR"ansible bash tofuThe automation engines
Semaphore runs the same binaries that you can run yourself, so anything you can debug from the shell behaves identically inside a task.
cd /tmp && ansible localhost -m ansible.builtin.pinglocalhost | SUCCESS => { "changed": false, "ping": "pong" }ansible-core is installed from a version-pinned wheel into /opt/semaphore-ansible. It is isolated from the system Python, so it cannot collide with anything the operating system manages. To move to a newer release, run:
sudo /opt/semaphore-ansible/bin/pip install --upgrade 'ansible-core==2.21.*'
sudo systemctl restart semaphoreCollections and roles install into the same environment:
sudo /opt/semaphore-ansible/bin/ansible-galaxy collection install community.generalRestrict network access
This step matters more for Semaphore than for most products. Semaphore exists to execute automation, and it holds the SSH keys and secrets that reach your estate. Anyone who can both reach it and authenticate to it can run code on the hosts it manages. Treat the instance as infrastructure, not as a public web application.
Limit both inbound rules to the addresses your team actually uses:
az network nsg rule update \
--resource-group my-resource-group \
--nsg-name my-semaphoreNSG \
--name allow-https \
--source-address-prefixes 203.0.113.0/24If your team does not have stable source addresses, put the instance on a private subnet. Reach it over a VPN, Azure Bastion or a peered network instead of a public IP. The image does not open any port by itself. Everything reachable is something that the network security group allows.
Use your own domain and certificate
Semaphore reads its certificate from /etc/ssl/semaphore/. Point a DNS record at your VM and obtain a certificate however you normally do. Then install the certificate and tell Semaphore its public URL:
sudo install -o root -g semaphore -m 0644 /path/to/fullchain.pem /etc/ssl/semaphore/semaphore.crt
sudo install -o root -g semaphore -m 0640 /path/to/privkey.pem /etc/ssl/semaphore/semaphore.key
sudo python3 - <<'PY' import json p = "/etc/semaphore/config.json" c = json.load(open(p)) c["web_host"] = "https://semaphore.example.com" json.dump(c, open(p, "w"), indent=2) PY
sudo systemctl restart semaphoreThe web_host value is essential. Semaphore only marks its session cookie Secure when that value is an https URL, and the web interface uses it as the base for every asset it serves. Keep the scheme as https.
Change the administrator password
Change the password from the web interface. Select the avatar at the bottom left, then Edit account. You can also change it from the command line with semaphore. Choose a strong value. This block refuses an empty or trivially weak password rather than accepting it:
NEW='ReplaceThisWithYourOwnStrongValue' if [ ${#NEW} -lt 16 ] || [ "$NEW" = "admin" ] || [ "$NEW" = "password" ]; then echo "REJECTED: choose a password of at least 16 characters" else echo "would run: sudo semaphore user change-by-login --login admin --password ... --config /etc/semaphore/config.json" fiwould run:
sudo semaphore user change-by-login --login admin --password ... --config /etc/semaphore/config.jsonAfter you change the password, update /root/semaphore-credentials.txt so the file and the live password agree.
Back up your instance
Everything that matters lives in two places: the datastore, and the configuration file that holds the key that decrypts it.
sudo install -d -m 0700 /root/semaphore-backup
sudo sqlite3 /var/lib/semaphore/semaphore.db ".backup '/root/semaphore-backup/semaphore.db'"
sudo cp -a /etc/semaphore/config.json /root/semaphore-backup/config.json
sudo ls -l /root/semaphore-backup.backup takes a consistent copy of a live database, so you do not have to stop the service.
Keep both files together. The SSH keys and secrets in the datastore are encrypted with access_key_encryption from the configuration file. A datastore restored without that key cannot be decrypted, and the configuration file without the datastore is useless. Store both files somewhere at least as protected as the credentials they unlock.
Keep the image patched
Ubuntu security updates are applied automatically by unattended-upgrades, which is enabled in this image. Check its status:
systemctl is-enabled unattended-upgradesenabledSemaphore itself is a single binary. To move to a newer release, download the community artifact for the version you want. Verify it against the checksums file published with that release, then install it:
echo "sudo apt-get install -y ./semaphore_community_<version>_linux_amd64.deb && sudo systemctl restart semaphore"sudo apt-get install -y ./semaphore_community_<version>_linux_amd64.deb && sudo systemctl restart semaphoreTake the artifact whose name contains community. Every Semaphore release also publishes a larger build that links the project's separately licensed Pro module. This image ships the community build deliberately, so that it contains only MIT-licensed code.
Troubleshooting
The web interface does not answer. First boot has not finished, or it failed. Check both units:
sudo systemctl is-active semaphore-firstboot semaphoreactive activeIf semaphore-firstboot is not active, read its log:
sudo journalctl -u semaphore-firstboot --no-pager | tail -20The server will not start until first boot has written /var/lib/sac/data/semaphore.bootstrap-ready. This is deliberate: it stops the instance from ever serving without an administrator and an encryption key.
A task fails immediately with a repository problem. Semaphore clones with git as the semaphore user. For a private repository, add an SSH key in Key Store and select it on the repository. For a host it has not seen before, accept the host key for that user:
sudo -u semaphore ssh-keyscan -H github.com 2>/dev/null | sudo -u semaphore tee -a /var/lib/semaphore/.ssh/known_hosts >/dev/null echo donedoneA task fails to reach your hosts. Semaphore runs Ansible as the semaphore user. The key it uses is the one you attached to the inventory, not any key in your own home directory.
Check everything at once. The image ships a self-test that re-runs every security assertion against the running instance. It checks the listening set, the login gates, the transport posture, the engine versions, and that the certificate really names your VM:
sudo /usr/local/sbin/semaphore-selftest.shSEMAPHORE_SELFTEST_OKAdding --customer-vm also asserts the image-level facts: no build account survives, first boot completed, and the image carries no swap. This is what SecureAnyCloud's own release gate runs against a freshly launched instance.
Support
SecureAnyCloud supports this image 24/7. Contact us through the SecureAnyCloud website for help with deployment, TLS certificates and custom domains, connecting inventories and source repositories, runner and pipeline design, upgrades and scaling.
Deploy on Azure
Launch Semaphore UI on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com