mosparo on Ubuntu 24.04 LTS on Azure
Overview
mosparo is an open source, self hosted spam protection service for web forms, and a privacy friendly alternative to hosted captcha services. Rather than asking a visitor to solve a puzzle or identify traffic lights, it analyses the content that was actually submitted against configurable rules and spam signals, so a genuine visitor usually sees nothing more than a small confirmation box. Because you host it, form submissions are never sent to a third party and no tracking or fingerprinting script is placed on your visitors.
The cloudimg image serves mosparo 1.5.4, a Symfony application, behind nginx with PHP 8.3 FPM and OPcache on a hardened, fully patched Ubuntu 24.04 LTS base, storing its data in a local MariaDB database.
The image is deliberately shipped uninstalled, with no configuration file and an empty database. mosparo has no command line installer, so on the first boot of every VM a one shot service completes mosparo's own setup process against freshly generated values: a unique database password, a unique encryption key, a unique application secret and a unique administrator password, which it writes to a root only file. Until that finishes, nginx serves mosparo on the loopback interface only and nothing is published on port 80 — so no instance is ever reachable from the network in an unconfigured state where a passer by could claim it, and no secret is ever shared between customers. Backed by 24/7 cloudimg support.
What is included:
- mosparo 1.5.4 served by nginx through the PHP 8.3 front controller, managed by systemd
- The mosparo interface on
:80, requiring an administrator sign in - A per VM database password, encryption key, application secret and administrator password generated on first boot and recorded in a root only file
- A local MariaDB database, created empty in the image so no build data is carried into your instance
- An hourly
mosparo-cleanup.timerrunning mosparo's retention cleanup nginx.service,php8.3-fpm.serviceandmariadb.serviceas enabled systemd units- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a sensible starting point and is comfortable for a handful of protected forms; size up if you protect many high traffic forms. NSG inbound: allow 22/tcp from your management network, and 80/tcp (plus 443/tcp once you terminate TLS on the VM) so both your visitors' browsers and your own backend can reach the mosparo API.
mosparo must be reachable from the browsers that load your forms, so it normally lives on a public host name or subdomain such as mosparo.example.com.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for mosparo by cloudimg, 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 HTTP (80). Then Review + create and Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name mosparo \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
Then open port 80 so browsers and your backend can reach mosparo:
az vm open-port --resource-group <your-rg> --name mosparo --port 80
Step 3 - Confirm the services are running
SSH in as azureuser. First boot completes in well under a minute; once it has, nginx binds port 80 and mosparo is ready.
systemctl is-active nginx php8.3-fpm mariadb
ss -tlnp 2>/dev/null | grep ':80 ' | sed 's/ */ /g'
Confirm which version is installed and that first boot finished:
sudo php -r '$c = require "/opt/mosparo/config/env.mosparo.php"; echo "mosparo " . $c["mosparo_installed_version"] . " installed=" . ($c["mosparo_installed"] ? "yes" : "no") . "\n";'

If port 80 is not bound, first boot has not completed. That is deliberate and fail closed: mosparo is never published before it has been installed and secured. Check the service with systemctl status mosparo-firstboot.service and journalctl -u mosparo-firstboot.service.
Step 4 - Retrieve this VM's administrator credentials
Every VM generates its own administrator password on first boot and writes it, with the sign in address, to a root only credentials file:
sudo cat /root/mosparo-credentials.txt
The same first boot also generated this VM's application secret, encryption key and database password. You never need those values, but you can confirm they are unique to this machine without printing them:
sudo php -r '$c = require "/opt/mosparo/config/env.mosparo.php"; foreach (["secret","encryption_key","database_password"] as $k) { printf("%-20s generated per VM (%d chars)\n", $k, strlen($c[$k])); } printf("%-20s %s\n", "app secret check", $c["secret"] === "SecretMosparoToken" ? "SHIPPED DEFAULT" : "not the upstream default");'

You can prove the generated password really authenticates, straight from the command line. A successful sign in leaves the login form behind; a wrong password is bounced back to it:
E=$(sudo grep '^mosparo.admin.email=' /root/mosparo-credentials.txt | cut -d= -f2-); P=$(sudo grep '^mosparo.admin.pass=' /root/mosparo-credentials.txt | cut -d= -f2-); CJ=$(mktemp); LF=$(curl -s -c "$CJ" http://127.0.0.1/login); T=$(printf '%s' "$LF" | grep -m1 -oE 'name="_csrf_token"[^>]*value="[^"]+"' | sed -E 's/.*value="([^"]+)".*/\1/'); BODY=$(curl -s -b "$CJ" -c "$CJ" -L --data-urlencode "_csrf_token=$T" --data-urlencode "_username=$E" --data-urlencode "_password=$P" http://127.0.0.1/login); rm -f "$CJ"; case "$BODY" in *'name="_password"'*) echo "login FAILED";; *) echo "login OK";; esac
Step 5 - Sign in to mosparo
Browse to http://<vm-public-ip>/. mosparo opens on its sign in screen — not on a setup wizard, because the installer was already completed and closed on first boot. Enter the mosparo.admin.email and mosparo.admin.pass values from Step 4.

Change the password from My account after your first sign in, and add further administrators under Administration then Users.
Step 6 - Create your first project
A project in mosparo represents one form, or one group of forms, that you want to protect. Each project has its own rules, its own design and its own API keys.
Select Create, give the project a name and a description, and add every host that will load the form under Hosts — that is the domain of your website, not of mosparo, and it is what mosparo uses to answer with the correct cross origin headers. Add each variation you actually use, for example both example.com and www.example.com.
The wizard then walks you through the design of the mosparo box so it matches your site's colours, the security features you want enabled, and finally the connection details.

The connection step shows the four values your website needs: the mosparo host, the project's unique identification number, its public key and its private key. You can return to them at any time under Project settings then General. Treat the private key like a password: it belongs only on your server, never in a page a browser can read.
Step 7 - Add the protection box to your form
Add an empty container to your form where the mosparo box should appear, load the frontend script from your instance, and initialise it with the project's host, identification number and public key:
<div id="mosparo-box"></div>
<script src="https://mosparo.example.com/build/mosparo-frontend.js" defer></script>
<script>
var m;
window.onload = function () {
m = new mosparo('mosparo-box', 'https://mosparo.example.com', '<PROJECT_UUID>', '<PUBLIC_KEY>', { loadCssResource: true });
};
</script>
When the visitor ticks the box, mosparo checks the content that has been typed into the form against the project's rules and, if it passes, writes a validation token back into the form and confirms that the data is valid. That check is the product doing its job — the visitor solves no puzzle and is never profiled.

Step 8 - Verify the submission from your backend
The frontend check alone is not a security boundary, because anything in a browser can be tampered with. When your form is submitted, your server must call mosparo's verification API with the project's private key to confirm the submission really was checked and really passed. Only then should you process the message.
mosparo publishes official API clients that sign the request for you, so you do not have to implement the signature by hand:
- PHP:
composer require mosparo/php-api-client - Python:
pip install mosparo-api-client - Node:
npm install @mosparo/api-client
Full details, including the request signature and the integrations for common content management systems and form builders, are in the mosparo documentation.
Step 9 - Tune the spam rules
Each project carries its own rules. Field rules match on the content of a single field — a word or pattern, an email address, a domain, a website, an IP address or subnet, a provider, a user agent, or a Unicode block, which is an effective way to reject submissions written in a script your audience never uses. Submission rules look at the submission as a whole. Rule packages let you subscribe to a maintained, shared rule set.
The Rule tester under Tools lets you try a value against the project's rules before you rely on them, reporting whether the value is treated as spam, the spam rating it accumulated and exactly which rules matched:

Every checked submission is kept under Submissions, where you can see what was received, how it was rated and why, which is the quickest way to understand a false positive.
Step 10 - Check the health of the instance
mosparo exposes a health endpoint that reports whether the application and its database are healthy. It answers on the loopback interface by default:
curl -s http://127.0.0.1/api/v1/health/check
You can also confirm from the command line that the instance is gated and the installer is closed — every page, including the setup path, lands on the sign in form:
printf 'GET / -> HTTP %s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' -L http://127.0.0.1/)" "$(curl -s -o /dev/null -w '%{url_effective}' -L http://127.0.0.1/ | sed 's#http://127.0.0.1##')"; printf 'GET /setup/ -> HTTP %s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' -L http://127.0.0.1/setup/)" "$(curl -s -o /dev/null -w '%{url_effective}' -L http://127.0.0.1/setup/ | sed 's#http://127.0.0.1##')"

To widen who may call the health endpoint, set MOSPARO_HEALTH_ALLOW_LIST in /opt/mosparo/.env.local and reload PHP-FPM.
Step 11 - Retention and scheduled cleanup
mosparo keeps submit tokens for 24 hours and submissions for 14 days by default, then expires them. The image wires that housekeeping to a systemd timer, so there is no cron job for you to add:
systemctl list-timers mosparo-cleanup.timer --no-pager | head -3
You can also run it on demand:
sudo -u www-data php /opt/mosparo/bin/console mosparo:cleanup-database

Retention is tunable in /opt/mosparo/.env.local with MOSPARO_SUBMIT_TOKEN_RETENTION_PERIOD and MOSPARO_SUBMISSION_RETENTION_PERIOD.
Step 12 - Enable HTTPS with Let's Encrypt
Browsers on your visitors' devices will load the mosparo frontend script, so mosparo should be served over HTTPS on the same scheme as the pages that embed it. Point a DNS record at the VM, then:
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d mosparo.example.com
Certbot rewrites the nginx site and installs a renewal timer. Afterwards, update the host in your embed snippet to the https:// address, and make sure 443/tcp is open in the NSG.
Step 13 - Back up and maintain your VM
Everything that matters lives in the MariaDB database and the configuration file, so a database dump plus that one file is a complete backup:
sudo mariadb-dump --single-transaction mosparo > mosparo-backup.sql
sudo cp /opt/mosparo/config/env.mosparo.php mosparo-config-backup.php
Keep the configuration file with the dump: it holds the encryption key, and encrypted columns cannot be read back without it. Store both somewhere safe, because between them they grant full access to your submissions.
Unattended security upgrades are enabled, so the operating system keeps itself patched. Azure Backup or managed disk snapshots give you whole VM recovery. mosparo can update itself from Administration then Update.
Security notes
- No shared secret ships in the image. The image contains no configuration file and an empty database. The database password, encryption key, application secret and administrator password are all generated on the first boot of your VM.
- Nothing is published before it is secured. In the shipped image nginx listens on loopback only and port 80 is unbound. The public site is enabled only after first boot has completed mosparo's installation, so an unconfigured instance is never exposed to the network.
- Fail closed. If first boot cannot complete, port 80 is never bound at all, rather than exposing an open installer.
- Keep the private key server side. The public key belongs in the page; the private key belongs only in your backend's verification call.
- Put mosparo behind HTTPS before real traffic, and restrict
22/tcpto your management network. - Sign in is required for the whole interface, and mosparo throttles repeated failed sign in attempts by user and by IP address. Optional two factor authentication is available under My account.
Architecture summary
| Component | Detail |
|---|---|
| Application | mosparo 1.5.4 (MIT), at /opt/mosparo |
| Web server | nginx, document root /opt/mosparo/public |
| Runtime | PHP 8.3 FPM with OPcache |
| Database | MariaDB 10.11, local, listening on loopback only |
| Configuration | /opt/mosparo/config/env.mosparo.php, written on first boot, mode 0640 |
| Credentials | /root/mosparo-credentials.txt, mode 0600, root only |
| First boot | mosparo-firstboot.service, sentinel /var/lib/cloudimg/mosparo-firstboot.done |
| Housekeeping | mosparo-cleanup.timer, hourly |
| Ports | 22/tcp SSH, 80/tcp mosparo interface and API, 443/tcp once TLS is configured |
| Base image | Ubuntu 24.04 LTS, fully patched, unattended security upgrades enabled |
Support
cloudimg images come with 24/7 support. Email support@cloudimg.co.uk with your Azure subscription ID and the VM name. For mosparo itself, see the mosparo documentation and the mosparo project on GitHub.