Public Facing Ignition

Using DigitalOcean, Ubuntu, LXD and NGINX

Created: June 9, 2019 Updated: October 31, 2021

Here is one way to deploy a secure Inductive Automation Ignition gateway to a cloud platform with encryption and access control.

DigitalOcean

This would work on any cloud platform, like Google Cloud Platform or Amazon Web Services, but I'm demonstrating on Digital Ocean because it has straight forward pricing, a low learning curve, and great documentation. I may add links and instructions to using the other platforms later. Click here to get $50 in credits for the next 30 days! Let's get to it.

Initial Droplet Setup

First, we need a server. So follow the quick start guide to create an Ubuntu 18.04 droplet.

I chose a 2 GB / 2 CPU with a 60 GB SSD disk, which at the time of writing this was $15.00/month. Choose what you want, but make sure there is enough resources to meet the minimum system requirements of Ignition and the application you're going to build with it.

Choose a datacenter region close to you.

You don't need any of the additional options to follow along here, but if you know what you're doing and want an additional option, you do you!

While you don't need to upload an SSH key, Digital Ocean will default to password based security and email you a password for the root account of the new droplet if you don't, I strongly recommend that you generate an SSH key for your SSH client of choice and upload it here. However, if you want to simply access the droplet from the web console, feel free to ignore me.

If you're using the SSH client in Ubuntu you can follow this guide, but here's the brief version. Run

ssh-keygen

follow the prompts. You can enter a passphrase if you want to add security, or if you prefer not to enter a passphrase every time you SSH into the server, you can omit it. Then run

cat ~/.ssh/id_rsa.pub

and copy/paste the output into the window that opens when you click add SSH key while you're creating the droplet on DigitalOcean.

If you're using putty, you can follow this guide to generate and load ssh keys.

Now that we have a server, let's set it up. DigitalOcean has a great guide for initial setup here that walks you through creating your user account and initial configuration of the firewall, but in short, SSH into the server as root (or use the DigitalOcean web console):

ssh root@your_server_ip #replace with your IP address

If you'd like your username to be joyja you'll run the following commands on the server:

adduser joyja 
usermod -aG sudo joyja 
ufw app list #Make sure OpenSSH is in there 
ufw allow OpenSSH 
ufw enable 
ufw status

Then if you're using ssh keys:

rsync --archive --chown=joyja:joyja ~/.ssh /home/joyja</jar-code

and test logging in as your user from your ssh client:

ssh joyja@your_server_ip #replace with your IP address</jar-code

If you're not using ssh keys, it will ask you for the password you set up when you created your user.

LXD

LXD is a linux container management system. If you aren't familiar with containers, but you've worked with virtual machines, containers are similar in that they provide an isolated environment, but they utilize the host's operating system kernel. They are much more resource friendly. We'll be using LXD to give us isolated environments for our Ignition server and our NGINX server.

If your familiar with Docker, LXD is a similar technology that is more focused on isolated environments that you can interact with after launching. Docker is more about automating the creation of application environments. Both technologies are incredible and become even more powerful when used together, but that's a topic for another day.

The first thing we need to do is give our user permissions to run LXD container management commands. Remember to use your username in place of joyja.

sudo usermod --append --groups lxd joyja

Log out and then back in afterward.

The recommended storage backend is ZFS, but to use it you need to install ZFS utilities.

sudo apt-get update
sudo apt-get install zfsutils-linux

Now we're ready to initialize LXD. It will go through a series of questions. You should choose a larger size than the default for the new loop device, I chose 30GB. Other than that the defaults are good as long as you installed zfsutils-linux

lxd init

Would you like to use LXD clustering? (yes/no) [default=no]: no
Do you want to configure a new storage pool? (yes/no) [default=yes]: yes
Name of the storage pool [default=default]: default #or your preference
Name of the storage backend to use (btrfs, dir, lvm, zfs) [default=zfs]: zfs
Create a new ZFS pool? (yes/no) [default=yes]: yes
Would you like to use an existing block device? (yes/no) [default=no]: no
Size in GB of the new loop device (1GB minimum) [default=15GB]: 30GB #default is too small
Would you like to connect to a MAAS server (yes/no): no
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]) [default=no]: no
Would you like to create a new local network bridge? (yes/no) [default=yes]: yes
What should the new bridge be called? [default=lxdbr0]: lxdbr0 #or your preference
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none” [default=auto]: auto
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]: auto
Would you like LXD to be available over the network? (yes/no) [default=no]: no
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]: yes
Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: no

Now you can run isolated environments of almost any Linux flavor and version you want, congrats! Let's create some. Run the following to create an Ubuntu 18.04 container named ignition.

lxc launch ubuntu:18.04 ignition

You might be wondering why the command uses lxc instead of lxd. LXC is the original linux container environment and LXD adds additional features to lxc. I know it's confusing, but just take a breath and accept that you'll be typing lxc the majority of the time your working with containers.

LXD will retrieve the Ubuntu 18.04 image from the repository and create your new container. We can see the containers we've created by running the following.

lxc list

You'll get a table like this:

We'll also need another container to run NGINX, so might as well create that now too.

lxc launch ubuntu:18.04 nginx

It should go faster the second time because we've already downloaded the Ubuntu 18.04 image from the repository. Now run lxc list one more time and record the IPV4 addresses for the containers. We'll need them later.

Inductive Automation Ignition

Inductive Automation's Ignition is a modern Human Machine Interface software that includes all the features we need to monitor and control process controls systems. Unlike traditional HMI software, the clients are 100% served over http(s) and therefore capable of being served over the internet. It's also available for linux, mac OSX, and Windows. Ignition includes the Perspective module, which allows a modern web browser to be used as a client. They also have some very nice third party MQTT modules that allow for simple, low bandwidth, secure data transfer accross the internet.

Ignition Setup

Let's access the shell on our brand new ignition container. Each container has a default user called "ubuntu".

lxc exec ignition -- su --login ubuntu

Now we'll download Ignition. The current version is 8.0.2:

curl -L -O -H 'Referer: https://inductiveautomation.com/downloads/ignition' https://files.inductiveautomation.com/release/ia/build8.0.2/20190605-1127/Ignition-8.0.2-linux-x64-installer.run --output "Ignition-8.0.2-linux-x64-installer.run"

If you need a different version, you can find it here and copy the appropriate link.

Add execute permissions to the downloaded file and install:

chmod +x Ignition-8.0.2-linux-x64-installer.run
sudo ./Ignition-8.0.2-linux-x64-installer.run

You'll be asked a series of questions. I selected all the defaults for the purposes of this guide. After the questions, Ignition will be installed and started automatically if you said yes to that question.

NGINX

NGINX is a popular web server, reverse proxy, and load balancer. We'll be using it as a reverse proxy here. It will allow us to control access to specific gateway URLs and give us encryption through HTTPS with Let's Encrypt.

##Port Forwarding

Befor we setup our NGINX container lets make sure http/https requests are forwarded to the nginx container. Run lxc list and record the nginx container IPV4 address if you haven't done that already.

Run the following commands to create iptables rules that will forward port 80 and port 443 requests to our nginx container. your_server_ip is the IP address of your DigitalOcean droplet and your_container_ip is the nginx container IP address:

PORT=80 PUBLIC_IP=your_server_ip CONTAINER_IP=your_container_ip \
sudo -E bash -c 'iptables -t nat -I PREROUTING -i eth0 -p TCP -d $PUBLIC_IP --dport $PORT -j DNAT --to-destination $CONTAINER_IP:$PORT -m comment --comment "forward http to nginx"'
PORT=443 PUBLIC_IP=your_server_ip CONTAINER_IP=your_container_ip \
sudo -E bash -c 'iptables -t nat -I PREROUTING -i eth0 -p TCP -d $PUBLIC_IP --dport $PORT -j DNAT --to-destination $CONTAINER_IP:$PORT -m comment --comment "forward https to nginx"'
sudo iptables -t nat -L PREROUTING --line-numbers

and if you need to delete a rule you can use the following, replacing 1 with the line number you want to delete:

sudo iptables -t nat -D PREROUTING 1

NGINX Setup

Let's access the shell on our NGINX container.

lxc exec nginx -- su --login ubuntu

and install nginx

sudo apt-get update
sudo apt-get install nginx

Next we're going to configure NGINX as a reverse proxy for our Ignition server. NGINX is configured by setting up server blocks, which means that we write a configuration file for each domain that is served by this NGINX server. The standard convention is to name these server blocks with the name of the domain being served. The following uses my test domain ignition.jarautomation.io so please replace it with your own.

Let's create our server block. I'm using vim, but feel free to use nano or any other command line text editor you prefer.

sudo vim /etc/nginx/sites-available/ignition.jarautomation.io

The configuration below will simply proxy all requests to the Ignition server. If you want to know more about the options, linuxize has a great post. Remember to use the Ignition container IP address we recorded from running lxc list earlier.

server {
        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name ignition.jarautomation.io;

        location / {
          
                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }
}

We can also use the following more complex configuration to limit the IP addresses that can access the root gateway page, designer and vision module while keeping the Perspective module public. It only allows public access to the URLs required for Perspective to function using the mobile app or directly from a web browser.

server {

        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name ignition.jarautomation.io;

        location / {
                allow xx.xx.xx.xx; #IP addresses listed here can access the gateway (and vision module)
                deny all;

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /system/images/ {
                
                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /data/perspective/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /res/perspective/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /system/perspective-download/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }


        location /system/pws/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /system/gwinfo {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }
}

Now we need to enable our server block configuration by creating a symbolic link from these files to the sites-enabled directory:

sudo ln -s /etc/nginx/sites-available/ignition.jarautomation.io /etc/nginx/sites-enabled/

Now let's add some encryption!

Let's Encrypt

Let's Encrypt is making the internet a better place by providing free SSL certificates and tools that makes it easy to renew them. Because they make it easy and free, certificates can be renewed more often.

If you're familiar with Ignition, you may be wondering why we don't just install the SSL certificates on the gateway and use that. We totally could, but I prefer to use SSL with NGINX and Let's Encrypt because we can use auto renewal and we don't have to go through the extra step of adding the SSL certificate to the java keystore whenever we want to renew. The connection from NGINX to Ignition is also completely private through the container bridge network so there is no real benefit to encrypting it.

First, we'll install the Let's Encrypt Certbot utility that will install and manage our certificates.

sudo add-apt-repository ppa:certbot/certbot
sudo apt install python-certbot-nginx

Then we'll run the utility.

sudo certbot --nginx -d ignition.jarautomation.io

Add -d <domain name> for each domain you want to create a certificate for. You'll be asked a series of questions including whether you agree to the terms of service, if you want to join the Electronic Frontier Foundation email list, and if you want to force all traffic to HTTPS. You can answer these how you wish, other than you won't be able to proceed without agreeing to the terms of service. I force all traffic to HTTPS so all Ignition traffic is encrypted.

Once you've answered all the questions Certbot will generate your certificate and automatically configure NGINX for HTTPS. If you open your NGINX configuration again you'll see what Certbot added.

server {

        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name ignition.jarautomation.io;

        location / {
                allow xx.xx.xx.xx; #IP addresses listed here can access the gateway (and vision module)
                deny all;

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /system/images/ {
                
                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /data/perspective/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /res/perspective/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /system/perspective-download/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }


        location /system/pws/ {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

        location /system/gwinfo {

                proxy_http_version 1.1;
                proxy_cache_bypass $http_upgrade;

                proxy_set_header Upgrade                $http_upgrade;
                proxy_set_header Connection             "Upgrade";
                proxy_set_header Host                   $host;
                proxy_set_header X-Real-IP              $remote_addr;
                proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto      $scheme;
                proxy_set_header X-Forwarded-Host       $host;
                proxy_set_header X-Forwarded-Port       $server_port;

                proxy_pass http://xx.xx.xx.xx:8088; #Use ignition container ip address
        }

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/ignition.jarautomation.io/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/ignition.jarautomation.io/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
    if ($host = ignition.jarautomation.io) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


        listen 80;
        listen [::]:80;

        server_name ignition.jarautomation.io;
    return 404; # managed by Certbot
}

You should now be able to go to your url, mine is https://ignition.jarautomation.io and you'll see the Ignition gateway welcome page. Just remember, if you limited which IP addresses can access the root gateway page, you'll need to be in the right location to access it.

There you have it! A public facing Ignition gateway in the cloud with access control and easy to maintain encryption.