2F
Security Azure

2FAuth on Ubuntu 24.04 on Azure User Guide

| Product: 2FAuth on Ubuntu 24.04 LTS on Azure

Overview

2FAuth is a free, open source, self hosted web app for managing your two factor authentication accounts. It stores the shared secrets behind your TOTP, HOTP and Steam one time passwords, shows the live code with its countdown so you can copy it in one action, and reads the QR codes services hand out during enrolment. Accounts can be grouped, searched and exported, and a documented REST API covers the same operations for automation. Because it runs in a browser on infrastructure you control, your second factors are no longer stranded on a single handset: they survive a lost or replaced phone, and a small team can share one instance with separate accounts.

The cloudimg image delivers 2FAuth fully installed and configured on Ubuntu 24.04 as a PHP 8.4 Laravel application behind nginx, so a working second factor vault is running within minutes of launch. Backed by 24/7 cloudimg support.

2FAuth is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). All product and company names are trademarks or registered trademarks of their respective holders. This image repackages the upstream open source release with cloudimg's provisioning and support.

What is included:

  • 2FAuth 8.0.1 (AGPL-3.0), served from /var/www/2fauth
  • PHP 8.4 (php8.4-fpm) from the ondrej/php PPA + nginx, with the web UI on port 80
  • Self contained SQLite storage, so there is no separate database server to run or secure
  • The whole application, the configuration and the SQLite database on a dedicated, independently resizable data disk mounted at /var/www/2fauth
  • A per-VM administrator password, a fresh application encryption key and fresh API token signing keys, all generated on first boot and written to a root-only file, so no default or shared login ships in the image
  • Public self registration closed by default, so nobody can claim your instance before you do
  • Stored 2FA secrets encrypted at rest with the per-VM application key
  • nginx.service and php8.4-fpm.service as systemd units, enabled and active
  • 24/7 cloudimg support

2FAuth sign-in page served on first boot

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 good starting point and is ample for personal or small team use. NSG inbound: allow 22/tcp from your management network and 80/tcp (HTTP) from your users. Add 443/tcp if you enable HTTPS.

Step 1 — Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for 2FAuth 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 2fauth \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Open HTTP to reach the web UI:

az vm open-port --resource-group <your-rg> --name 2fauth --port 80 --priority 900

Step 3 — Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 — Confirm the service is running

2FAuth is served by nginx on port 80, which passes PHP requests to php8.4-fpm over a local Unix socket. Both units are enabled, so they come back automatically after a reboot.

systemctl is-active nginx php8.4-fpm 2fauth-firstboot

Check the versions that shipped in this image:

php -v | head -1
grep -oE "'version' *=> *'[^']+'" /var/www/2fauth/config/2fauth.php | head -1

2FAuth services and versions on a freshly launched VM

Confirm the application answers on the loopback interface. /up is the built in health endpoint:

curl -s -o /dev/null -w 'health %{http_code}\n' http://127.0.0.1/up
curl -s -o /dev/null -w 'app    %{http_code}\n' http://127.0.0.1/

Step 5 — Retrieve your per-VM administrator credentials

On the first boot of each VM, 2fauth-firstboot.service generates a fresh application encryption key, fresh API token signing keys and a unique administrator password, then writes them to a root-only file. Nothing is shared between deployments and nothing is baked into the image.

sudo cat /root/2fauth-credentials.txt

The file is readable only by root, and so are the application key and the OAuth signing key:

sudo stat -c '%a %U:%G %n' /root/2fauth-credentials.txt /var/www/2fauth/.env /var/www/2fauth/storage/oauth-private.key

Per-VM 2FAuth credentials and file permissions

Back up /var/www/2fauth/.env. Your stored 2FA secrets are encrypted at rest with the APP_KEY it contains. Without that key the secrets cannot be decrypted, even from a database backup.

Step 6 — Sign in

Browse to http://<vm-public-ip>/ and sign in with the email and password from the credentials file. That account is the administrator of the installation.

There is deliberately no Register link: public self registration is closed on this image so that nobody who reaches your instance can create an account before you do. Step 9 explains how to reopen it.

Step 7 — Add your first account and read its code

Select New, then either scan or upload the QR code the service gave you, or choose to enter the details by hand and paste the secret. Save, and the account joins your list.

The 2FAuth account list

Selecting an account reveals its current one time password with a countdown showing how long the code remains valid, and the next code below it. Copy the code straight into whatever is asking for it.

A live one time password generated by 2FAuth, with its countdown and the next code

Codes are generated locally by the appliance. You can confirm the generator is working end to end without a browser by asking the application for a code from a known test secret. This uses the shared secret from the TOTP specification (RFC 6238), whose expected six digit code at second 59 is 287082:

sudo -u www-data php -r '
require "/var/www/2fauth/vendor/autoload.php";
$app = require "/var/www/2fauth/bootstrap/app.php";
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$a = new App\Models\TwoFAccount;
$a->fillWithURI("otpauth://totp/demo:rfc6238@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=demo&algorithm=sha1&digits=6&period=30", false, true);
echo "RFC 6238 test vector -> ", $a->getOTP(59)->password, PHP_EOL;
'

Step 8 — Use the REST API

Every operation in the UI is also available over a documented REST API. Create a personal access token from Settings, then OAuth, and pass it as a bearer token.

Count the accounts stored on this instance:

curl -s -H "Authorization: Bearer <your-personal-access-token>" \
  http://<vm-public-ip>/api/v1/twofaccounts/count

Fetch the current code for account 1:

curl -s -H "Authorization: Bearer <your-personal-access-token>" \
  http://<vm-public-ip>/api/v1/twofaccounts/1/otp

Step 9 — Security model and user management

The image is hardened so that a freshly launched instance is safe before you have touched it.

  • No account ships in the image. The captured image contains an empty database. The administrator is created on first boot with a random password.
  • Self registration is closed. Upstream 2FAuth promotes the first person to register to administrator, so an open registration endpoint on a public instance is a takeover risk. This image ships with registration disabled.
  • Secrets are encrypted at rest with the per-VM application key.
  • Only SSH and HTTP listen. SQLite is a local file, so there is no database port to expose.

Confirm all of that on your own instance:

ss -tlnH | awk '{print $1, $4}' | sort -u
sudo -u www-data php /var/www/2fauth/2fauth-admin.php report

2FAuth security posture: listening ports, a refused registration attempt, and the hardened settings

To add colleagues, sign in as the administrator and open Settings, then Administration. Under Users you can create accounts directly, which is the recommended approach. Under Authentication you can reopen public registration if you would rather people sign themselves up, and optionally restrict it to a list of addresses or a pattern.

The 2FAuth administration settings, including the registration controls

Step 10 — The dedicated data disk

The entire application lives on a dedicated data disk mounted at /var/www/2fauth: the code, the .env configuration, the SQLite database holding your 2FA secrets, the OAuth signing keys and the cached icons. Keeping it off the operating system disk means it is independently resizable, survives instance replacement, and a backup is a single directory.

findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS /var/www/2fauth
df -h /var/www/2fauth | tail -1

The dedicated 2FAuth data disk and the SQLite database

To take a backup, stop the web tier briefly so nothing is mid write, copy the directory, then start it again:

sudo systemctl stop nginx
sudo tar czf /var/backups/2fauth-$(date +%F).tar.gz -C /var/www 2fauth
sudo systemctl start nginx

Store that archive somewhere off the VM. It contains your encryption key and your encrypted secrets, so treat it as sensitive.

Step 11 — Add a custom domain and HTTPS

Point a DNS A record at the VM's public IP, allow 443/tcp in the NSG, then issue a certificate:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d 2fa.your-domain.example

Certbot rewrites the nginx site and installs a renewal timer. Then update APP_URL so the application knows its own address, which also matters for WebAuthn passkeys because they are bound to the origin:

sudo sed -i 's|^APP_URL=.*|APP_URL=https://2fa.your-domain.example|' /var/www/2fauth/.env
sudo -u www-data php /var/www/2fauth/artisan config:cache
sudo systemctl reload nginx

Step 12 — Maintenance

Ubuntu's unattended upgrades are enabled, so security patches for the operating system arrive automatically. The two periodic settings below are what drive them:

cat /etc/apt/apt.conf.d/20auto-upgrades
systemctl show -p UnitFileState --value unattended-upgrades.service

Application logs are written by Laravel and rotated daily, and nginx keeps its own access and error logs:

sudo ls -1 /var/www/2fauth/storage/logs/ | tail -5
sudo tail -n 5 /var/log/nginx/2fauth_error.log 2>/dev/null || echo "no nginx errors logged"

To upgrade 2FAuth itself, back up the data disk first (Step 10), then follow the upstream release notes. cloudimg publishes refreshed images as new upstream versions are released, so redeploying from a newer image and restoring your data disk is usually the simplest path.

Troubleshooting

The site returns HTTP 502. PHP-FPM is not running or not reachable on its socket. Check both units and the nginx error log:

systemctl is-active php8.4-fpm nginx
sudo tail -n 20 /var/log/nginx/2fauth_error.log 2>/dev/null || echo "no nginx error log yet"

The credentials file still shows placeholder text. First boot has not finished. Check the one shot unit, which writes its progress to the journal:

systemctl is-active 2fauth-firstboot
sudo journalctl -u 2fauth-firstboot --no-pager | tail -20

You have lost the administrator password. Set a new one from the VM. Replace the value below with the password you want:

sudo -u www-data php /var/www/2fauth/2fauth-admin.php upsert admin@example.com 'YourNewStrongPassword'

Passkeys will not register. WebAuthn binds to the origin, so APP_URL must match the address in your browser's address bar. Re-check it after adding a domain or enabling HTTPS:

grep '^APP_URL=' /var/www/2fauth/.env

Support

cloudimg provides 24/7 technical support for this image by email at support@cloudimg.co.uk and via live chat. Critical issues receive a one hour average response time. For billing, subscription changes or refund requests, contact the same address.