2FAuth
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 enrollment. Accounts can be grouped, searched and exported, and a documented REST API covers the same operations for automation.
Because 2FAuth runs in a browser on infrastructure you control, your second factors are not stranded on a single handset. They survive a lost or replaced phone, and a small team can share one instance with separate accounts.
This SecureAnyCloud image delivers 2FAuth fully installed and configured on Ubuntu 24.04 as a PHP 8.4 Laravel application behind nginx. A working second-factor vault is running within minutes of launch.
What you get:
- 2FAuth 8.0.1 (AGPL-3.0), served from
/var/www/2fauth - PHP 8.4 (
php8.4-fpm) from the ondrej/php PPA and 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.serviceandphp8.4-fpm.serviceas systemd units, enabled and active- 24/7 SecureAnyCloud support
2FAuth is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). This image repackages the upstream open source release with SecureAnyCloud's provisioning and support. All product and company names are trademarks or registered trademarks of their respective holders.
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.
Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting size and is ample for personal or small-team use.
Configure the network security group (NSG) to allow inbound 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 SecureAnyCloud, and select Create.
On the Basics tab, 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).
Select Review + create, then Create.
Step 2 - 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:2fauth-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name 2fauth \
--image sac1647283583153:2fauth-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku StandardOpen HTTP so you can reach the web UI:
az vm open-port --resource-group <your-rg> --name 2fauth --port 80 --priority 900Step 3 - Connect to your VM
Connect over SSH with the administrator account you created at deployment:
ssh azureuser@<vm-public-ip>Step 4 - Confirm the service is running
nginx serves 2FAuth on port 80 and passes PHP requests to php8.4-fpm over a local Unix socket. Both units are enabled, so they start automatically after a reboot.
systemctl is-active nginx php8.4-fpm 2fauth-firstbootCheck the versions that ship in this image:
php -v | head -1 grep -oE "'version' *=> *'[^']+'" /var/www/2fauth/config/2fauth.php | head -1Confirm that 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. It writes them to a root-only file. Nothing is shared between deployments and nothing is baked into the image.
sudo cat /root/2fauth-credentials.txtOnly root can read this file, the application key or the OAuth signing key. Verify the permissions:
sudo stat -c '%a %U:%G %n' /root/2fauth-credentials.txt /var/www/2fauth/.env /var/www/2fauth/storage/oauth-private.keyBack 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 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. 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.
Select an account to reveal its current one-time password. A countdown shows how long the code remains valid, and the next code appears below it. Copy the code straight into whatever is asking for it.
The appliance generates codes locally. To confirm the generator works end to end without a browser, ask the application for a code from a known test secret. This command 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 under 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/countFetch the current code for account 1:
curl -s -H "Authorization: Bearer <your-personal-access-token>" \
http://<vm-public-ip>/api/v1/twofaccounts/1/otpStep 9 - Security model and user management
The image is hardened so that a freshly launched instance is safe before you touch 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 this on your own instance:
ss -tlnH | awk '{print $1, $4}' | sort -u sudo -u www-data php /var/www/2fauth/2fauth-admin.php reportTo add colleagues, sign in as the administrator and open Settings, then Administration.
Under Users, create accounts directly. This is the recommended approach. Under Authentication, you can reopen public registration if you would rather people sign themselves up. You can optionally restrict it to a list of addresses or a pattern.
Step 10 - The dedicated data disk
The entire application lives on a dedicated data disk mounted at /var/www/2fauth. That includes the code, the .env configuration, the SQLite database holding your 2FA secrets, the OAuth signing keys and the cached icons.
Because the disk is separate from the operating system disk, it is independently resizable and survives instance replacement. A backup is a single directory.
findmnt -no SOURCE,TARGET,FSTYPE,OPTIONS /var/www/2fauth df -h /var/www/2fauth | tail -1To take a backup, stop the web tier briefly so that 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 nginxStore the 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 and 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.exampleCertbot rewrites the nginx site and installs a renewal timer. Next, update APP_URL so the application knows its own address. This also matters for WebAuthn passkeys, which 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 nginxStep 12 - Maintenance
Ubuntu's unattended upgrades are enabled, so operating system security patches arrive automatically. Check the periodic settings that drive them:
cat /etc/apt/apt.conf.d/20auto-upgrades systemctl show -p UnitFileState --value unattended-upgrades.serviceLaravel writes the application logs and rotates them daily. 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.
SecureAnyCloud publishes refreshed images as new upstream versions are released. Redeploying from a newer image and restoring your data disk is usually the simplest approach.
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 does not contain real values yet. 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 -20You have lost the administrator password. Set a new one from the VM. Replace the value in the command 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 you add a domain or enable HTTPS:
grep '^APP_URL=' /var/www/2fauth/.envSupport
SecureAnyCloud provides 24/7 technical support for this image by email at sales@secureanycloud.com and through live chat. Critical issues receive a one-hour average response time.
For billing, subscription changes or refund requests, contact the same address.
Deploy on Azure
Launch 2FAuth 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