Self-Hosted TOTP 2FA for the Home

Two Factor Authentication (2FA) is critical for authentication of your self hosted and cloud accounts. In this tutorial, we look at a self hosted option called 2fauth that is a replacement for Google Authenticator.

The Github project for 2fauth is here.

I am installing the 2fauth server in a LXD container which is very fast and efficient. If you are new to LXD containers, watch my LXD 101 video here. I also have 50 plus videos on the channel focusing on LXD and networking.

I start by creating a LXD container for my 2fauth Server which is bridged to my LAN.

lxc launch ubuntu:22.04 TOTP --profile default --profile Bridged-MAIN -c security.nesting=true -c limits.memory=2048MB -c limits.cpu.allowance=20% -c boot.autostart=true

Connect to the container.

lxc exec TOTP bash

Update the repositories and the software components.

apt update && apt upgrade -y

Install Docker and docker-compose:

curl -sSL https://get.docker.com | sh
sudo apt install docker-compose

Add a user account.

adduser scott

Put the user in the sudo and the docker groups:

usermod -aG sudo scott
usermod -aG docker scott

Move over to the account.

su - scott

Create a TOTP folder and move into it.

mkdir TOTP
cd TOTP

Create a folder inside of TOTP and set the required permissions and ownership.

mkdir 2fauth
sudo chown -R 1000:1000 2fauth
sudo chmod 700 2fauth

Create a docker-compose file:

nano docker-compose.yml

Put the following inside of the file.

version: "3"
services:
  2fauth:
    image: 2fauth/2fauth
    container_name: 2fauth
    restart: unless_stopped
    volumes:
      - ./2fauth:/2fauth
    ports:
      - 80:8000/tcp
    environment:
      # You can change the name of the app
      - APP_NAME=2FAuth
      # You can leave this on "local". If you change it to production most console commands will ask for extra confirmation.
      # Never set it to "testing".
      - APP_ENV=local
      # Set to true if you want to see debug information in error screens.
      - APP_DEBUG=false
      # This should be your email address
      - SITE_OWNER=mail@example.com
      # The encryption key for  our database and sessions. Keep this very secure.
      # If you generate a new one all existing data must be considered LOST.
      # Change it to a string of exactly 32 chars or use command `php artisan key:generate` to generate it
      - APP_KEY=SomeRandomStringOf32CharsExactly
      # This variable must match your installation's external address but keep in mind that
      # it's only used on the command line as a fallback value.
      - APP_URL=http://localhost
      # Turn this to true if you want your app to react like a demo.
      # The Demo mode reset the app content every hours and set a generic demo user.
      - IS_DEMO_APP=false
      # The log channel defines where your log entries go to.
      # 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
      # Several other options exist. You can use 'single' for one big fat error log (not recommended).
      # Also available are 'syslog', 'errorlog' and 'stdout' which will log to the system itself.
      - LOG_CHANNEL=daily
      # Log level. You can set this from least severe to most severe:
      # debug, info, notice, warning, error, critical, alert, emergency
      # If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
      # nothing will get logged, ever.
      - LOG_LEVEL=notice
      # Database config (can only be sqlite)
      - DB_DATABASE="/srv/database/database.sqlite"
      # If you're looking for performance improvements, you could install memcached.
      - CACHE_DRIVER=file
      - SESSION_DRIVER=file
      # Mail settings
      # Refer your email provider documentation to configure your mail settings
      # Set a value for every available setting to avoid issue
      - MAIL_DRIVER=log
      - MAIL_HOST=smtp.mailtrap.io
      - MAIL_PORT=2525
      - MAIL_FROM=changeme@example.com
      - MAIL_USERNAME=null
      - MAIL_PASSWORD=null
      - MAIL_ENCRYPTION=null
      - MAIL_FROM_NAME=null
      - MAIL_FROM_ADDRESS=null
      # Authentication settings
      # The default authentication guard
      # Supported:
      #   'web-guard' : The Laravel built-in auth system (default if nulled)
      #   'reverse-proxy-guard' : When 2FAuth is deployed behind a reverse-proxy that handle authentication
      # WARNING
      # When using 'reverse-proxy-guard' 2FAuth only look for the dedicated headers and skip all other built-in
      # authentication checks. That means your proxy is fully responsible of the authentication process, 2FAuth will
      # trust him as long as headers are presents.
      - AUTHENTICATION_GUARD=web-guard
      # Name of the HTTP headers sent by the reverse proxy that identifies the authenticated user at proxy level.
      # Check your proxy documentation to find out how these headers are named (i.e 'REMOTE_USER', 'REMOTE_EMAIL', etc...)
      # (only relevant when AUTHENTICATION_GUARD is set to 'reverse-proxy-guard')
      - AUTH_PROXY_HEADER_FOR_USER=null
      - AUTH_PROXY_HEADER_FOR_EMAIL=null
      # Custom logout URL to open when using an auth proxy.
      - PROXY_LOGOUT_URL=null
      # WebAuthn settings
      # Relying Party name, aka the name of the application. If null, defaults to APP_NAME
      - WEBAUTHN_NAME=2FAuth
      # Relying Party ID. If null, the device will fill it internally.
      # See https://webauthn-doc.spomky-labs.com/pre-requisites/the-relying-party#how-to-determine-the-relying-party-id
      - WEBAUTHN_ID=null
      # Optional image data in BASE64 (128 bytes maximum) or an image url
      # See https://webauthn-doc.spomky-labs.com/pre-requisites/the-relying-party#relying-party-icon
      - WEBAUTHN_ICON=null
      # Use this setting to control how user verification behave during the
      # WebAuthn authentication flow.
      #
      # Most authenticators and smartphones will ask the user to actively verify
      # themselves for log in. For example, through a touch plus pin code,
      # password entry, or biometric recognition (e.g., presenting a fingerprint).
      # The intent is to distinguish one user from any other.
      #
      # Supported:
      #   'required': Will ALWAYS ask for user verification
      #   'preferred' (default) : Will ask for user verification IF POSSIBLE
      #   'discouraged' : Will NOT ask for user verification (for example, to minimize disruption to the user interaction flow)
      - WEBAUTHN_USER_VERIFICATION=preferred
      # Use this setting to declare trusted proxied.
      # Supported:
      #   '*': to trust any proxy
      #   A comma separated IP list: The list of proxies IP to trust
      - TRUSTED_PROXIES=null
      # Leave the following configuration vars as is.
      # Unless you like to tinker and know what you're doing.
      - BROADCAST_DRIVER=log
      - QUEUE_DRIVER=sync
      - SESSION_LIFETIME=120
      - REDIS_HOST=127.0.0.1
      - REDIS_PASSWORD=null
      - REDIS_PORT=6379
      - PUSHER_APP_ID=
      - PUSHER_APP_KEY=
      - PUSHER_APP_SECRET=
      - PUSHER_APP_CLUSTER=mt1
      - MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
      - MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
      - MIX_ENV=local

You will want to change the mail settings in the file and the other settings are well documented. Note that I offered the app on port 80 and you may want to change the port number to the left of the colon if port 80 is used on your server. If you are using a LXD container instance as I have documented here, port 80 will be fine.

When you want to save the file, CTRL O and Enter and then CTRL X to exit the editor.

To launch the application:

docker compose up -d

To access the web page for 2fauth, go into your browser and go to the address of your LXD container instance.

At the login page for 2fauth you can register your first account and begin using the app,

Optionally you may also share this to the public web through NginX Proxy Manager provided you have a registered domain.