Paul's Internet Landfill/ 2021/ Locking Down Liteshort

Locking Down Liteshort

The Watcamp newsletter contains a lot of links to Google Calendar entries. These links are long and ugly, so I wanted to shorten them for my newsletters. Thus I needed a link shortener.

First I used Google's built-in goo.gl shortener, but naturally it was too useful to live and was replaced by some inscrutible thing called Firebase. Then I switched to the https://da.gd service, because it was gratis, FLOSS, light on surveillance, and maintained by somebody who seemed to share some of my values. Unfortunately because da.gd is open, it is used by spammers, so a bunch of corporate content filters block it.

I looked around for alternatives. There were a couple of other mostly-open offerings (https://www.is.gd/ does not seem bad, and neither does the venerable https://tinyurl.com) but any public service that would be free for me to use would also be usable by spammers, and thus would be targeted by the corporate content filters.

Going with one of the popular commercial link shorteners (bit.ly, for example) was another possibility, but they are all gross, and their free plans are fairly restrictive.

In the end I decided that trying to self-host something off of kwlug.org would be the way to go. Here were my requirements:

After trawling through the Awesome Self-hosted List I decided to try liteshort. It was written in Python, had relatively few features, and the author emphasized simplicity and configurability over surveillance.

Unfortunately, the author does not prioritize security. As asserted on the Nginx configuration page, "Security changes too often for it to be documented here." That is not a great sign, but I was able to lock down the service enough that I feel reasonably confident in running it on the Internet. This blog post documents how I did that.

Initial setup

I chose to run the link shortener on the subdomain s.kwlug.org .

I installed liteshort system-wide using PyPi, following the installation instructions.

I set up Nginx to be my web server, and used a reverse proxy so liteshort could talk to the webserver.

I encoded my configuration changes using Saltstack, but I will ignore that here.

Enabling SSL

This was relatively easy. As is standard these days, I used Let's Encrypt. I created an HTTP and an HTTPS server configuration in Nginx, putting them in /etc/nginx/sites-enabled. Here is the HTTP configuration, which just redirects to the HTTPS site:

server {
    listen 80;

    server_name s.kwlug.org;
    # Dummy page for http
    root /var/www/html/liteshort;

    index index.html;

    location ^~ /.well-known/acme-challenge/ { 
      default_type "text/plain";
      root /var/www/html/certbot/;
    }

    location / {
      return 301 https://$server_name$request_uri;
    }
}

The index.html was just a dummy page I added for testing. Users should never see it.

The HTTPS configuration looked something like:

server { 
    server_name s.kwlug.org;

    location ^~ /static/ { 
      include /etc/nginx/mime.types;
      root /usr/local/lib/python3.7/dist-packages/liteshort;
    } 

    location / { 
      include uwsgi_params;
      uwsgi_pass unix:///run/uwsgi/liteshort.sock;
    }

    location ^~ /.well-known/acme-challenge/ { 
      default_type "text/plain";
      alias /var/www/html/certbot/;
      allow all;
    }

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/s.kwlug.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/s.kwlug.org/privkey.pem;
    include /usr/lib/python3/dist-packages/certbot_nginx/options-ssl-nginx.conf
}

Most of this is standard configuration for setting up Let's Encrypt. The second and third stanzas are specific to liteshort. Note that I changed the location of the socket fron /run/liteshort.sock to /run/uwsgi/liteshort.sock. I had to make a corresponding change in /etc/liteshort/liteshort.ini:

socket = /run/uwsgi/liteshort.sock
chmod-socket = 660

The reason for chmod-socket will become clearer in the next section.

Running as non-root

By default the liteshort service wants to run as root, which is way more powerful than I thought was necessary. So I created a system user called liteshort with a shell of /usr/sbin/nologin .

Next I made a folder for liteshort to store its database, which was owned by this user. /var/lib/liteshort is used by the default install, so I made a folder /var/lib/liteshort-local. I configured liteshort to store its database in this location by setting the following parameter in /etc/liteshort/config.yml:

# String: Filename of the URL database without extension
# Default: 'urls'
database_name: '/var/lib/liteshort-local/liteshort-urls'

liteshort runs as a service, using uWSGI. This is configured in the file /etc/liteshort/liteshort.ini . To the end of that file I added the lines:

uid = liteshort
gid = liteshort

(I am not sure whether these lines were necessary. I think systemd might override them.)

Speaking of our favorite and in no way troublesome init system, we need to add lines to specify the user and group liteshort will use. My /etc/systemd/system/liteshort.service looks like this:

[Unit]
Description=uWSGI instance to serve liteshort
After=network.target

[Service]
RuntimeDirectory=uwsgi
User=liteshort
Group=www-data
ExecStart=/usr/bin/uwsgi --ini /etc/liteshort/liteshort.ini

[Install]
WantedBy=multi-user.target

The User and Group lines deserve some elaboration. Nginx runs as the www-data user. We want liteshort to run as a different unprivileged user. They communicate via a UNIX socket. Both liteshort and www-data need to be able to read and write to this socket, which is why we needed the chmod-socket = 660 in /etc/liteshort/liteshort.ini above.

Setting up the service this way means the socket will be created with the proper permissions so that the two services can still communicate. This is not as locked down as I would like, but I hope there is some real security improvement here.

The RuntimeDirectory=uwsgi line dynamically creates entries in /run/uwsgi for the liteshort service when it is running, and destroys them when it is not.

Disabling the API

On the surface, this seems easy. In /etc/liteshort/config.yml set the disable_api option:

# Boolean: Disables API. If set to true, admin_password/admin_hashed_password do not need to be set.
# Default: false
disable_api: True

However, as of the current writing this does not work the way you might think. Disabling the API disables operations that require a username and API password, but anonymous users will still be able to create links by making POST requests to the service! As it turns out I only want to create links and expand them, so disabling the API makes sense for me, but there is more work to do.

Limiting who can shorten links

I am running liteshort because I want to shorten links in my newsletters. The scripts that generate these newsletters run from a computer with a fixed IP address (say 1.2.3.4). Thus I would like only this computer to create shortlinks. On the other hand, I would like users from any computer to follow those shortlinks.

Fortunately, liteshort uses POST requests to create shortlinks and GET requests to follow them. We can restrict POST requests in the Nginx config. The relevant snippet for the location block becomes;

    location / { 
      limit_except GET { 
        allow 1.2.3.4;
        allow 2.3.4.5;
        deny all;
      }
      include uwsgi_params;
      uwsgi_pass unix:///run/uwsgi/liteshort.sock;
    }

This would allow computers with IP addresses 1.2.3.4 and 2.3.4.5 to create links. Everybody else would just be able to follow shortened links.

This makes me feel much more confident in running liteshort on the Internet. I do not trust the security setup of liteshort, but I do trust that Nginx has good security, because many eyes make shallow bugs.

One problem with this solution is that it returns a status 403 when a bad person tries to POST and not a status 405. I tried getting Nginx to return the proper status instead of deny all;, but I could not get it to work.