Security Azure

mosparo on Ubuntu 24.04 LTS on Azure

| Product: mosparo on Ubuntu 24.04 LTS

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.timer running mosparo's retention cleanup
  • nginx.service, php8.3-fpm.service and mariadb.service as 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";'

The nginx, php8.3-fpm and mariadb services reporting active and enabled, with the installed mosparo version and the first boot sentinel present

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");'

The per VM secrets reported by length only, the app secret confirmed not to be the upstream default, and the credentials file at mode 600 owned by root

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.

The mosparo sign in page, protected by the administrator account generated on this VM's first boot

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 mosparo project overview after creating a project for a website contact form

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.

The mosparo protection box embedded in a website contact form, confirming after its check that the submitted data is valid and the form can be sent

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:

The mosparo rule tester evaluating a genuine customer enquiry and correctly clearing it, showing the spam verdict, rating and score, and that no rule 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##')"

The mosparo health endpoint reporting healthy with the database connected, and both the root and setup paths redirecting to the sign in page

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

The hourly mosparo-cleanup timer scheduled and armed, with unattended-upgrades enabled for security patching

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/tcp to 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.