OwnTracks recorder in a container on Fedora with Let’s Encrypt and nginx

OwnTracks Recorder is a web application which maps locations over time. Generally, it connects to an MQTT server and subscribes to owntracks/+ topics for any location updates, but it also has a built in function to receive updates over HTTP.

I have been using OwnTracks with MQTT for a while, but found it to be too unreliable on Android (disconnects in the background and doesn’t reconnect nicely). Using HTTP is supposed to be more reliable, so this is how I set it up. The idea is to use OwnTracks on Android to post directly to the OwnTracks recorder over HTTP instead of MQTT and have recorder post the MQTT messages on our behalf using LUA scripts (for Home Assistant).

Friends is an important feature (to let members of the family see where eachother is located) and fortunately it is supported in HTTP mode (but it requires a little bit more configuration).

nginx and base configuration

We will use nginx as a reverse proxy in front of the recorder to provide both TLS and authentication to keep the service private and secure.

sudo dnf install nginx httpd-tools

Configure nginx to proxy to OwnTracks recorder by creating a new config file for the domain you are hosting on. For example, if your domain is owntracks.yourdomain.com then create a file at /etc/nginx/conf.d/owntracks.yourdomain.com. Later certbot will update this to add TLS configuration.

cat << \EOF |sudo tee /etc/nginx/conf.d/owntracks.yourdomain.com.conf
server {
  server_name owntracks.yourdomain.com;
  root /var/www/html;

  auth_basic "OwnTracks";
  auth_basic_user_file /etc/nginx/owntracks.htpasswd;
  proxy_set_header X-Limit-U $remote_user;

  location / {
    proxy_pass http://127.0.0.1:8083/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
  }

  location /ws {
    rewrite ^/(.*) /$1 break;
    proxy_pass http://127.0.0.1:8083;
    proxy_http_version  1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }

  location /view/ {
    proxy_buffering off;
    proxy_pass http://127.0.0.1:8083/view/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
  }
  location /static/ {
    proxy_pass http://127.0.0.1:8083/static/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
  }

  location /pub {
    proxy_pass http://127.0.0.1:8083/pub;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
  }

  error_page 404 /404.html;
  location = /40x.html {
  }

  error_page 500 502 503 504 /50x.html;
  location = /50x.html {
  }
}
EOF

Now that we have a web server let’s open the ports to enable traffic on port 80 and 443.

sudo firewall-cmd --zone=FedoraServer --add-service=http
sudo firewall-cmd --zone=FedoraServer --add-service=https
sudo firewall-cmd --runtime-to-permanent

SELinux will block nginx from acting as a proxy and connecting to our other services, so we need to tell it that it’s OK.

 sudo setsebool -P httpd_can_network_connect 1
sudo setsebool -P httpd_can_network_relay 1

Note that we’ve set a password file for nginx to protect recorder in the config, now we need to create that file.

Let’s pretend that we have three users, Alice, Bob and Charlie. Create the nginx password file when you add the password for Alice, then add the password for the other two.

sudo htpasswd -c /etc/nginx/owntracks.htpasswd alice
sudo htpasswd /etc/nginx/owntracks.htpasswd bob
sudo htpasswd /etc/nginx/owntracks.htpasswd charlie

That’s the core nginx config done, next we will use cerbot to get a certificate and re-configure nginx to use TLS.

Certbot

Install certbot and the nginx plugin, which will let us get signed certificates from Let’s Encrypt. Using the plugin means it will configure nginx to handle the challenge and write the config file automatically. You will need to make sure that port 80 on your nginx server is available over the Internet (and probably also port 443 so that we can connect securely to recorder remotely) as well as a DNS entry pointing to your external IP (I’ll use owntracks.yourdomain.com as an example).

sudo dnf install certbot python3-certbot-nginx

Next, use certbot to get TLS certificates from Let’s Encrypt. Follow the prompts and be sure to enable TLS redirection so that all traffic will be encrypted.

sudo certbot --agree-tos \
--redirect \
--rsa-key-size 4096 \
--nginx \
-d owntracks.yourdomain.com

Now that we have a certificate, let’s enable auto renewals.

sudo systemctl enable --now certbot-renew.timer

OK, nginx should now be configured with TLS and managed by certbot.

Recorder with Docker

Now let’s get the recorder container going! First install and prepare Docker. Note that if you’re running on Fedora 31 or later, you need to revert to cgroup v1 first.

sudo groupadd -r docker
sudo gpasswd -a ${USER} docker
newgrp docker
sudo dnf install -y cockpit-docker docker
sudo systemctl start docker
sudo systemctl enable docker

Next let’s prepare the configuration and scripts for the container.

sudo mkdir -p /var/lib/owntracks/{config,scripts,logs}

Generally we pass variables into containers, but recorder also supports a config file so we’ll use that instead (OTR_LUASCRIPT is not supported as a variable, anyway). Replace the values for your MQTT server below.

NOTE: OTR_PORT must not be a number not a string, else it will be be ignored.

OTR_HOST="mqtt-broker"
OTR_PORT=mqtt-port
OTR_USER="mqtt-user"
OTR_PASS="mqtt-user-password"

cat << EOF | sudo tee /var/lib/owntracks/config/recorder.conf
OTR_TOPICS = "owntracks/#"
OTR_HTTPHOST = "0.0.0.0"
OTR_STORAGEDIR = "/store"
OTR_HTTPLOGDIR = "/logs"
OTR_LUASCRIPT = "/scripts/hook.lua"
OTR_HOST = "${OTR_HOST}"
OTR_PORT = ${OTR_PORT}
OTR_USER = "${OTR_USER}"
OTR_PASS = "${OTR_PASS}"
OTR_CLIENTID = "owntracks-recorder"
EOF

If you’re using TLS on your MQTT server, then copy over the CA (for example, /etc/pki/tls/certs/ca-bundle.crt) and set the OTR_CAFILE config option to point to the file as it will be inside the container. This will automatically enable TLS connection to your MQTT server.

sudo cp /etc/pki/tls/certs/ca-bundle.crt /var/lib/owntracks/config/ca.crt
echo 'OTR_CAFILE="/config/ca.crt"' | sudo tee -a /var/lib/owntracks/config/recorder.conf

Next get the Lua scripts ready which will allow recorder to forward HTTP events on to MQTT. We will write a file called hook.lua to run the script, which is referenced in the config above. It has a JSON dependency, which we will download from the Internet.

wget http://regex.info/code/JSON.lua
sudo mv JSON.lua /var/lib/owntracks/scripts/JSON.lua
cat << EOF | sudo tee /var/lib/owntracks/scripts/hook.lua
JSON = (loadfile "/scripts/JSON.lua")()

function otr_init()
end

function otr_exit()
end

function otr_hook(topic, _type, data)
    otr.log("DEBUG_PUB:" .. topic .. " " .. JSON:encode(data))
    if(data['_http'] == true) then
        if(data['_repub'] == true) then
           return
        end
        data['_repub'] = true
        local payload = JSON:encode(data)
        otr.publish(topic, payload, 1, 1)
    end
end

function otr_putrec(u, d, s)
        j = JSON:decode(s)
        if (j['_repub'] == true) then
                return 1
        end
end
EOF

Next we can run the container for recorder. We will map in all of the directories we created earlier and the configuration we created should be read in when the program in the container starts. Note that :Z option sets the SELinux context on those config files.

docker run -dit --name recorder \
--restart always \
-p 8083:8083 \
-v /var/lib/owntracks/store:/store:Z \
-v /var/lib/owntracks/config:/config:Z \
-v /var/lib/owntracks/scripts:/scripts:Z \
-v /etc/localtime:/etc/localtime:ro \
owntracks/recorder

OwnTracks should now be listening on port 8083, waiting for connections to come in through nginx!

Friends with OwnTracks

To set up friends in HTTP mode we need to get a shell on the container and load friends data into the database.

docker exec -it recorder /bin/sh

Inside the container we load friends data into the database. Let’s use our three friends as an example, Alice with her phone pixel3xl, Bob with his pixel4 and Charlie with her pixel3a, to set up notifications for everyone.

ocat --load=friends << EOF
alice-pixel3xl [ "bob/pixel4", "charlie/pixel3a" ]
bob-pixel4 [ "alice/pixel3xl, "charlie/pixel3a" ]
charlie-pixel3a [ "alice/pixel3xl, "bob/pixel4" ]
EOF

We can dump the friends data to see what we’ve loaded, then exit the container.

ocat -S /store --dump=friends
exit

Now, whenever Alice, Bob or Charlie update their location, recorder will return JSON data with the location of the other two. OwnTracks will then display that information under the Friends tab. Unfortunately, the one thing thing HTTP mode doesn’t support is Regions notifications to be notified when friends enter or leave defined way points, but I’ve found OwnTracks to be much more reliable with HTTP so I guess that’s a small price to pay…


Leave a Reply

Your email address will not be published. Required fields are marked *