<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Software Engineering &#8211; hackanooga</title>
	<atom:link href="/category/software-engineering/feed/" rel="self" type="application/rss+xml" />
	<link>/</link>
	<description>Confessions of a homelab hacker</description>
	<lastBuildDate>Wed, 25 Sep 2024 13:56:04 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.6.2</generator>

<image>
	<url>/wp-content/uploads/2024/03/cropped-cropped-avatar-32x32.png</url>
	<title>Software Engineering &#8211; hackanooga</title>
	<link>/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Standing up a Wireguard VPN</title>
		<link>/standing-up-a-wireguard-vpn/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Wed, 25 Sep 2024 13:56:04 +0000</pubDate>
				<category><![CDATA[Automation]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Self Hosted]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[Blog Post]]></category>
		<guid isPermaLink="false">/?p=619</guid>

					<description><![CDATA[VPN&#8217;s have traditionally been slow, complex and hard to set up and configure. That all changed several years ago when Wireguard was officially merged into the mainline Linux kernel (src). I won&#8217;t go over all the reasons for why you should want to use Wireguard in this article, instead I will be focusing on just [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>VPN&#8217;s have traditionally been slow, complex and hard to set up and configure.  That all changed several years ago when Wireguard was officially merged into the mainline Linux kernel (<a href="https://arstechnica.com/gadgets/2020/03/wireguard-vpn-makes-it-to-1-0-0-and-into-the-next-linux-kernel/" data-type="link" data-id="https://arstechnica.com/gadgets/2020/03/wireguard-vpn-makes-it-to-1-0-0-and-into-the-next-linux-kernel/">src</a>).  I won&#8217;t go over all the reasons for why you should want to use Wireguard in this article, instead I will be focusing on just how easy it is to set up and configure. </p>



<p>For this tutorial we will be using Terraform to stand up a Digital Ocean droplet and then install Wireguard onto that. The Digital Ocean droplet will be acting as our &#8220;server&#8221; in this example and we will be using our own computer as the &#8220;client&#8221;. Of course, you don&#8217;t have to use Terraform, you just need a Linux box to install Wireguard on.  You can find the code for this tutorial on my personal Git server <a href="https://git.hackanooga.com/mikeconrad/wireguard-terraform-digitalocean" data-type="link" data-id="https://git.hackanooga.com/mikeconrad/wireguard-terraform-digitalocean">here</a>.</p>



<h3 class="wp-block-heading">Create Droplet with Terraform</h3>



<p>I have written some very basic Terraform to get us started. The Terraform is very basic and just creates a droplet with a predefined ssh key and a setup script passed as user data.  When the droplet gets created, the script will get copied to the instance and automatically executed.  After a few minutes everything should be ready to go.  If you want to clone the repo above, feel free to, or if you would rather do everything by hand that&#8217;s great too.  I will assume that you are doing everything by hand.  The process of deploying from the repo should be pretty self explainitory.  My reasoning for doing it this way is because I wanted to better understand the process.  </p>



<p>First create our main.tf with the following contents:</p>



<pre class="wp-block-code"><code># main.tf
# Attach an SSH key to our droplet
resource "digitalocean_ssh_key" "default" {
  name       = "Terraform Example"
  public_key = file("./tf-digitalocean.pub")
}

# Create a new Web Droplet in the nyc1 region
resource "digitalocean_droplet" "web" {
  image    = "ubuntu-22-04-x64"
  name     = "wireguard"
  region   = "nyc1"
  size     = "s-2vcpu-4gb"
  ssh_keys = &#91;digitalocean_ssh_key.default.fingerprint]
  user_data = file("setup.sh")
}

output "droplet_output" {
  value = digitalocean_droplet.web.ipv4_address
}</code></pre>



<p>Next create a terraform.tf file in the same directory with the following contents:</p>



<pre class="wp-block-code"><code>terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "2.41.0"
    }
  }
}

provider "digitalocean" {
}
</code></pre>



<p>Now we will need to create the ssh key that we defined in our Terraform code.  </p>



<pre class="wp-block-code"><code>$ ssh-keygen -t rsa -C "WireguardVPN" -f ./tf-digitalocean -q -N ""</code></pre>



<p>Next we need to set an environment variable for our DigitalOcean access token.</p>



<pre class="wp-block-code"><code>$ export DIGITALOCEAN_ACCESS_TOKEN=dop_v1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</code></pre>



<p>Now we are ready to initialize our Terraform and apply it:</p>



<pre class="wp-block-code"><code>$ terraform init
$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # digitalocean_droplet.web will be created
  + resource "digitalocean_droplet" "web" {
      + backups              = false
      + created_at           = (known after apply)
      + disk                 = (known after apply)
      + graceful_shutdown    = false
      + id                   = (known after apply)
      + image                = "ubuntu-22-04-x64"
      + ipv4_address         = (known after apply)
      + ipv4_address_private = (known after apply)
      + ipv6                 = false
      + ipv6_address         = (known after apply)
      + locked               = (known after apply)
      + memory               = (known after apply)
      + monitoring           = false
      + name                 = "wireguard"
      + price_hourly         = (known after apply)
      + price_monthly        = (known after apply)
      + private_networking   = (known after apply)
      + region               = "nyc1"
      + resize_disk          = true
      + size                 = "s-2vcpu-4gb"
      + ssh_keys             = (known after apply)
      + status               = (known after apply)
      + urn                  = (known after apply)
      + user_data            = "69d130f386b262b136863be5fcffc32bff055ac0"
      + vcpus                = (known after apply)
      + volume_ids           = (known after apply)
      + vpc_uuid             = (known after apply)
    }

  # digitalocean_ssh_key.default will be created
  + resource "digitalocean_ssh_key" "default" {
      + fingerprint = (known after apply)
      + id          = (known after apply)
      + name        = "Terraform Example"
      + public_key  = &lt;&lt;-EOT
            ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDXOBlFdNqV48oxWobrn2rPt4y1FTqrqscA5bSu2f3CogwbDKDyNglXu8RL4opjfdBHQES+pEqvt21niqes8z2QsBTF3TRQ39SaHM8wnOTeC8d0uSgyrp9b7higHd0SDJVJZT0Bz5AlpYfCO/gpEW51XrKKeud7vImj8nGPDHnENN0Ie0UVYZ5+V1zlr0BBI7LX01MtzUOgSldDX0lif7IZWW4XEv40ojWyYJNQwO/gwyDrdAq+kl+xZu7LmBhngcqd02+X6w4SbdgYg2flu25Td0MME0DEsXKiZYf7kniTrKgCs4kJAmidCDYlYRt43dlM69pB5jVD/u4r3O+erTapH/O1EDhsdA9y0aYpKOv26ssYU+ZXK/nax+Heu0giflm7ENTCblKTPCtpG1DBthhX6Ml0AYjZF1cUaaAvpN8UjElxQ9r+PSwXloSnf25/r9UOBs1uco8VDwbx5cM0SpdYm6ERtLqGRYrG2SDJ8yLgiCE9EK9n3uQExyrTMKWzVAc= WireguardVPN
        EOT
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + droplet_output = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

digitalocean_ssh_key.default: Creating...
digitalocean_ssh_key.default: Creation complete after 1s &#91;id=43499750]
digitalocean_droplet.web: Creating...
digitalocean_droplet.web: Still creating... &#91;10s elapsed]
digitalocean_droplet.web: Still creating... &#91;20s elapsed]
digitalocean_droplet.web: Still creating... &#91;30s elapsed]
digitalocean_droplet.web: Creation complete after 31s &#91;id=447469336]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

droplet_output = "159.223.113.207"
</code></pre>



<p>All pretty standard stuff.  Nice!  It only took about 30 seconds or so on my machine to spin up a droplet and start provisioning it.  It is worth noting that the setup script will take a few minutes to run.  Before we log into our new droplet, let&#8217;s take a quick look at the setup script that we are running.</p>



<pre class="wp-block-code"><code>#!/usr/bin/env sh
set -e
set -u
# Set the listen port used by Wireguard, this is the default so feel free to change it.
LISTENPORT=51820
CONFIG_DIR=/root/wireguard-conf
umask 077
mkdir -p $CONFIG_DIR/client

# Install wireguard
apt update &amp;&amp; apt install -y wireguard

# Generate public/private key for the "server".
wg genkey > $CONFIG_DIR/privatekey
wg pubkey &lt; $CONFIG_DIR/privatekey > $CONFIG_DIR/publickey

# Generate public/private key for the "client"
wg genkey > $CONFIG_DIR/client/privatekey
wg pubkey &lt; $CONFIG_DIR/client/privatekey > $CONFIG_DIR/client/publickey


# Generate server config
echo "&#91;Interface]
Address = 10.66.66.1/24,fd42:42:42::1/64
ListenPort = $LISTENPORT
PrivateKey = $(cat $CONFIG_DIR/privatekey)

### Client config
&#91;Peer]
PublicKey = $(cat $CONFIG_DIR/client/publickey)
AllowedIPs = 10.66.66.2/32,fd42:42:42::2/128
" > /etc/wireguard/do.conf


# Generate client config.  This will need to be copied to your machine.
echo "&#91;Interface]
PrivateKey = $(cat $CONFIG_DIR/client/privatekey)
Address = 10.66.66.2/32,fd42:42:42::2/128
DNS = 1.1.1.1,1.0.0.1

&#91;Peer]
PublicKey = $(cat publickey)
Endpoint = $(curl icanhazip.com):$LISTENPORT
AllowedIPs = 0.0.0.0/0,::/0
" > client-config.conf

wg-quick up do

# Add iptables rules to forward internet traffic through this box
# We are assuming our Wireguard interface is called do and our
# primary public facing interface is called eth0.

iptables -I INPUT -p udp --dport 51820 -j ACCEPT
iptables -I FORWARD -i eth0 -o do -j ACCEPT
iptables -I FORWARD -i do -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
ip6tables -I FORWARD -i do -j ACCEPT
ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# Enable routing on the server
echo "net.ipv4.ip_forward = 1
      net.ipv6.conf.all.forwarding = 1" >/etc/sysctl.d/wg.conf
sysctl --system</code></pre>



<p>As you can see, it is pretty straightforward.  All you really need to do is:</p>



<p>On the &#8220;server&#8221; side:</p>



<ol class="wp-block-list">
<li>Generate a private key and derive a public key from it for both the &#8220;server&#8221; and the &#8220;client&#8221;.</li>



<li>Create a &#8220;server&#8221; config that tells the droplet what address to bind to for the wireguard interface, which private key to use to secure that interface and what port to listen on.</li>



<li>The &#8220;server&#8221; config also needs to know what peers or &#8220;clients&#8221; to accept connections from in the AllowedIPs block.  In this case we are just specifying one.  The &#8220;server&#8221; also needs to know the public key of the &#8220;client&#8221; that will be connecting.</li>
</ol>



<p>On the &#8220;client&#8221; side:</p>



<ol class="wp-block-list">
<li>Create a &#8220;client&#8221; config that tells our machine what address to assign to the wireguard interface (obviously needs to be on the same subnet as the interface on the server side).</li>



<li>The client needs to know which private key to use to secure the interface.</li>



<li>It also needs to know the public key of the server as well as the public IP address/hostname of the &#8220;server&#8221; it is connecting to as well as the port it is listening on.  </li>



<li>Finally it needs to know what traffic to route over the wireguard interface.  In this example we are simply routing all traffic but you could restrict this as you see fit.</li>
</ol>



<p>Now that we have our configs in place, we need to copy the client config to our local machine.  The following command should work as long as you make sure to replace the IP address with the IP address of your newly created droplet:</p>



<pre class="wp-block-code"><code>## Make sure you have Wireguard installed on your local machine as well.
## https://wireguard.com/install

## Copy the client config to our local machine and move it to our wireguard directory.
$ ssh -i tf-digitalocean root@157.230.177.54 -- cat /root/wireguard-conf/client-config.conf| sudo tee /etc/wireguard/do.conf</code></pre>



<p>Before we try to connect, let&#8217;s log into the server and make sure everything is set up correctly:</p>



<pre class="wp-block-code"><code>$ ssh -i tf-digitalocean root@159.223.113.207
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-113-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
 * Management:     https://landscape.canonical.com/
 * Support:        https://ubuntu.com/pro

 System information as of Wed Sep 25 13:19:02 UTC 2024

  System load:  0.03              Processes:             113
  Usage of /:   2.1% of 77.35GB   Users logged in:       0
  Memory usage: 6%                IPv4 address for eth0: 157.230.221.196
  Swap usage:   0%                IPv4 address for eth0: 10.10.0.5

Expanded Security Maintenance for Applications is not enabled.

70 updates can be applied immediately.
40 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

New release '24.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.


Last login: Wed Sep 25 13:16:25 2024 from 74.221.191.214
root@wireguard:~#

</code></pre>



<p>Awesome!  We are connected.  Now let&#8217;s check the wireguard interface using the <code>wg</code> command.  If our config was correct, we should see an interface line and 1 peer line like so.  If the peer line is missing then something is wrong with the configuration.  Most likely a mismatch between public/private key.:</p>



<pre class="wp-block-code"><code>root@wireguard:~# wg
interface: do
  public key: fTvqo/cZVofJ9IZgWHwU6XKcIwM/EcxUsMw4voeS/Hg=
  private key: (hidden)
  listening port: 51820

peer: 5RxMenh1L+rNJobROkUrub4DBUj+nEUPKiNe4DFR8iY=
  allowed ips: 10.66.66.2/32, fd42:42:42::2/128
root@wireguard:~# </code></pre>



<p>So now we should be ready to go!  On your local machine go ahead and try it out:</p>



<pre class="wp-block-code"><code>## Start the interface with wg-quick up &#91;interface_name]
$ sudo wg-quick up do
&#91;sudo] password for mikeconrad: 
&#91;#] ip link add do type wireguard
&#91;#] wg setconf do /dev/fd/63
&#91;#] ip -4 address add 10.66.66.2/32 dev do
&#91;#] ip -6 address add fd42:42:42::2/128 dev do
&#91;#] ip link set mtu 1420 up dev do
&#91;#] resolvconf -a do -m 0 -x
&#91;#] wg set do fwmark 51820
&#91;#] ip -6 route add ::/0 dev do table 51820
&#91;#] ip -6 rule add not fwmark 51820 table 51820
&#91;#] ip -6 rule add table main suppress_prefixlength 0
&#91;#] ip6tables-restore -n
&#91;#] ip -4 route add 0.0.0.0/0 dev do table 51820
&#91;#] ip -4 rule add not fwmark 51820 table 51820
&#91;#] ip -4 rule add table main suppress_prefixlength 0
&#91;#] sysctl -q net.ipv4.conf.all.src_valid_mark=1
&#91;#] iptables-restore -n

## Check our config
$ sudo wg
interface: do
  public key: fJ8mptCR/utCR4K2LmJTKTjn3xc4RDmZ3NNEQGwI7iI=
  private key: (hidden)
  listening port: 34596
  fwmark: 0xca6c

peer: duTHwMhzSZxnRJ2GFCUCHE4HgY5tSeRn9EzQt9XVDx4=
  endpoint: 157.230.177.54:51820
  allowed ips: 0.0.0.0/0, ::/0
  latest handshake: 1 second ago
  transfer: 1.82 KiB received, 2.89 KiB sent

## Make sure we can ping the outside world
mikeconrad@pop-os:~/projects/wireguard-terraform-digitalocean$ ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=56 time=28.0 ms
^C
--- 1.1.1.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 27.991/27.991/27.991/0.000 ms

## Verify our traffic is actually going over the tunnel.
$ curl icanhazip.com
157.230.177.54


</code></pre>



<p>We should also be able to ssh into our instance over the VPN using the <code>10.66.66.1</code> address:</p>



<pre class="wp-block-code"><code>$ ssh -i tf-digitalocean root@10.66.66.1
The authenticity of host '10.66.66.1 (10.66.66.1)' can't be established.
ED25519 key fingerprint is SHA256:E7BKSO3qP+iVVXfb/tLaUfKIc4RvtZ0k248epdE04m8.
This host key is known by the following other names/addresses:
    ~/.ssh/known_hosts:130: &#91;hashed name]
Are you sure you want to continue connecting (yes/no/&#91;fingerprint])? yes
Warning: Permanently added '10.66.66.1' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-113-generic x86_64)

 * Documentation:  https://help.ubuntu.com/
 * Management:     https://landscape.canonical.com/
 * Support:        https://ubuntu.com/pro

 System information as of Wed Sep 25 13:32:12 UTC 2024

  System load:  0.02              Processes:             109
  Usage of /:   2.1% of 77.35GB   Users logged in:       0
  Memory usage: 6%                IPv4 address for eth0: 157.230.177.54
  Swap usage:   0%                IPv4 address for eth0: 10.10.0.5

Expanded Security Maintenance for Applications is not enabled.

73 updates can be applied immediately.
40 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

New release '24.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.


root@wireguard:~# 
</code></pre>



<p>Looks like everything is working!  If you run the script from the repo you will have a fully functioning Wireguard VPN in less than 5 minutes!  Pretty cool stuff!  This article was not meant to be exhaustive but instead a simple primer to get your feet wet.  The setup script I used is heavily inspired by <a href="https://github.com/angristan/wireguard-install">angristan/wireguard-install</a>.  Another great resource is the <a href="https://github.com/pirate/wireguard-docs" data-type="link" data-id="https://github.com/pirate/wireguard-docs">Unofficial docs repo</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Hardening your web server by only allowing traffic from Cloudflare</title>
		<link>/hardening-your-web-server-by-only-allowing-traffic-from-cloudflare/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Thu, 01 Aug 2024 21:02:29 +0000</pubDate>
				<category><![CDATA[Cloudflare]]></category>
		<category><![CDATA[Networking]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Blog Post]]></category>
		<guid isPermaLink="false">/?p=607</guid>

					<description><![CDATA[Harden your origin server by only allowing access from Cloudflare IP addresses.]]></description>
										<content:encoded><![CDATA[
<h4 class="wp-block-heading">TDLR:</h4>



<p>If you just want the code you can find a convenient script on my Gitea server <a href="https://git.hackanooga.com/mikeconrad/random_scripts/src/branch/master/allow_only_cloudflare_traffic.sh" data-type="link" data-id="https://git.hackanooga.com/mikeconrad/random_scripts/src/branch/master/allow_only_cloudflare_traffic.sh">here</a>.  This version has been slightly modified so that it will work on more systems.</p>



<p></p>



<p>I have been using Cloudflare for several years for both personal and professional projects.  The free plan has some various gracious limits and it&#8217;s a great way to clear out some low hanging fruit and improve the security of your application.  If you&#8217;re not familiar with how it works, basically Cloudflare has two modes for DNS records.  <code>DNS Only</code> and <code>Proxied</code>.  The only way to get the advantages of Cloudflare is to use <code>Proxied</code> mode.  Cloudflare has some great documentation on how all of their services work but basically what happens is that you are pointing your domain to Cloudflare and Cloudflare provisions their network of Proxy servers to handle requests for your domain.</p>



<p>These proxy servers allow you to secure your domain by implementing things like WAF and Rate limiting.  You can also enforce HTTPS only mode and modify/add custom request/response headers.  You will notice that once you turn this mode on, your webserver will log requests as coming from Cloudflare IP addresses.  They have great <a href="https://developers.cloudflare.com/support/troubleshooting/restoring-visitor-ips/restoring-original-visitor-ips/" data-type="link" data-id="https://developers.cloudflare.com/support/troubleshooting/restoring-visitor-ips/restoring-original-visitor-ips/">documentation</a> on how to configure your webserver to restore these IP addresses in your log files.  </p>



<p>This is a very easy step to start securing your origin server but it still allows attackers to access your servers directly if they know the IP address.  We can take our security one step forward by only allowing requests from IP addresses originating within Cloudflare meaning that we will only allow requests if they are coming from a Cloudflare proxy server.  The setup is fairly straightforward.  In this example I will be using a Linux server.</p>



<p>We can achieve this pretty easily because Cloudflare provides a sort of API where they regular publish their network blocks.  Here is the basic script we will use:</p>



<pre class="wp-block-code"><code>for ip in $(curl https://www.cloudflare.com/ips-v4/); do iptables -I INPUT -p tcp -m multiport --dports http,https -s $ip -j ACCEPT; done

for ip in $(curl https://www.cloudflare.com/ips-v6/); do ip6tables -I INPUT -p tcp -m multiport --dports http,https -s $ip -j ACCEPT; done

iptables -A INPUT -p tcp -m multiport --dports http,https -j DROP
ip6tables -A INPUT -p tcp -m multiport --dports http,https -j DROP
</code></pre>



<p>This will pull down the latest network addresses from Cloudflare and create <code>iptables</code> rules for us.  These IP addresses do change from time to time so you may want to put this in a script and run it via a <code>cronjob</code> to have it update on a regular basis.</p>



<p>Now with this in place, here is the results:</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="320" src="/wp-content/uploads/2024/09/image-1024x320.png" alt="" class="wp-image-608" srcset="/wp-content/uploads/2024/09/image-1024x320.png 1024w, /wp-content/uploads/2024/09/image-300x94.png 300w, /wp-content/uploads/2024/09/image-768x240.png 768w, /wp-content/uploads/2024/09/image.png 1173w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>This should cut down on some of the noise from attackers and script kiddies trying to find holes in your security.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Traefik 3.0 service discovery in Docker Swarm mode</title>
		<link>/traefik-3-0-service-discovery-in-docker-swarm-mode/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Sat, 11 May 2024 13:44:01 +0000</pubDate>
				<category><![CDATA[Ansible]]></category>
		<category><![CDATA[Automation]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Traefik]]></category>
		<category><![CDATA[Blog Post]]></category>
		<guid isPermaLink="false">/?p=564</guid>

					<description><![CDATA[I recently decided to set up a Docker swarm cluster for a project I was working on. If you aren&#8217;t familiar with Swarm mode, it is similar in some ways to k8s but with much less complexity and it is built into Docker. If you are looking for a fairly straightforward way to deploy containers [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I recently decided to set up a Docker swarm cluster for a project I was working on.  If you aren&#8217;t familiar with Swarm mode, it is similar in some ways to k8s but with much less complexity and it is built into Docker.  If you are looking for a fairly straightforward way to deploy containers across a number of nodes without all the overhead of k8s it can be a good choice, however it isn&#8217;t a very popular or widespread solution these days.  </p>



<p>Anyway, I set up a VM scaling set in Azure with 10 Ubuntu 22.04 vms and wrote some Ansible scripts to automate the process of installing Docker on each machine as well as setting 3 up as swarm managers and the other 7 as worker nodes.  I ssh&#8217;d into the primary manager node and created a docker compose file for launching an observability stack.</p>



<p>Here is what that <code>docker-compose.yml</code> looks like:</p>



<pre class="wp-block-code"><code>---
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.88.0
    volumes:
      - /home/user/repo/common/devops/observability/otel-config.yaml:/etc/otel/config.yaml
      - /home/user/repo/log:/log/otel
    command: --config /etc/otel/config.yaml
    environment:
      JAEGER_ENDPOINT: 'tempo:4317'
      LOKI_ENDPOINT: 'http://loki:3100/loki/api/v1/push'
    ports:
      - '8889:8889' # Prometheus metrics exporter (scrape endpoint)
      - '13133:13133' # health_check extension
      - '55679:55679' # ZPages extension
    deploy:
      placement:
        constraints:
          - node.hostname==dockerswa2V8BY4
    networks:
      - traefik
  prometheus:
    container_name: prometheus
    image: prom/prometheus:v2.42.0
    volumes:
      - /home/user/repo/common/devops/observability/prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - '9090:9090'
    deploy:
      placement:
        constraints:
          - node.hostname==dockerswa2V8BY4
    networks:
      - traefik
  loki:
    container_name: loki
    image: grafana/loki:2.7.4
    ports:
      - '3100:3100'
    networks:
      - traefik
  grafana:
    container_name: grafana
    image: grafana/grafana:9.4.3
    volumes:
      - /home/user/repo/common/devops/observability/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
    environment:
      GF_AUTH_ANONYMOUS_ENABLED: 'false'
      GF_AUTH_ANONYMOUS_ORG_ROLE: 'Admin'
    expose:
      - '3000'
    labels:
      - traefik.constraint-label=traefik
      - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
      - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
      - traefik.http.routers.grafana-http.rule=Host(`swarm-grafana.mydomain.com`)
      - traefik.http.routers.grafana-http.entrypoints=http
      - traefik.http.routers.grafana-http.middlewares=https-redirect
      # traefik-https the actual router using HTTPS
      # Uses the environment variable DOMAIN
      - traefik.http.routers.grafana-https.rule=Host(`swarm-grafana.mydomain.com`)
      - traefik.http.routers.grafana-https.entrypoints=https
      - traefik.http.routers.grafana-https.tls=true
      # Use the special Traefik service api@internal with the web UI/Dashboard
      - traefik.http.routers.grafana-https.service=grafana
      # Use the "le" (Let's Encrypt) resolver created below
      - traefik.http.routers.grafana-https.tls.certresolver=le
      # Enable HTTP Basic auth, using the middleware created above
      - traefik.http.services.grafana.loadbalancer.server.port=3000
    deploy:
      placement:
        constraints:
          - node.hostname==dockerswa2V8BY4
    networks:
      - traefik
  # Tempo runs as user 10001, and docker compose creates the volume as root.
  # As such, we need to chown the volume in order for Tempo to start correctly.
  init:
    image: &amp;tempoImage grafana/tempo:latest
    user: root
    entrypoint:
      - 'chown'
      - '10001:10001'
      - '/var/tempo'
    volumes:
      - /home/user/repo/tempo-data:/var/tempo
    deploy:
      placement:
        constraints:
          - node.hostname==dockerswa2V8BY4

  tempo:
    image: *tempoImage
    container_name: tempo
    command: &#91;'-config.file=/etc/tempo.yaml']
    volumes:
      - /home/user/repo/common/devops/observability/tempo.yaml:/etc/tempo.yaml
      - /home/user/repo/tempo-data:/var/tempo
    deploy:
      placement:
        constraints:
          - node.hostname==dockerswa2V8BY4
    ports:
      - '14268' # jaeger ingest
      - '3200' # tempo
      - '4317' # otlp grpc
      - '4318' # otlp http
      - '9411' # zipkin
    depends_on:
      - init
    networks:
      - traefik
networks:
  traefik:
    external: true
</code></pre>



<p>Pretty straightforward so I proceed to deploy it into the swarm</p>



<pre class="wp-block-code"><code>docker stack deploy -c docker-compose.yml observability</code></pre>



<p>Everything deploys properly but when I view the Traefik logs there is an issue with all the services except for the grafana service.  I get errors like this:</p>



<pre class="wp-block-code"><code>traefik_traefik.1.tm5iqb9x59on@dockerswa2V8BY4    | 2024-05-11T13:14:16Z ERR error="service \"observability-prometheus\" error: port is missing" container=observability-prometheus-37i852h4o36c23lzwuu9pvee9 providerName=swarm
</code></pre>



<p>It drove me crazy for about half a day or so.  I couldn&#8217;t find any reason why the grafana service worked as expected but none of the others did.  Part of my love/hate relationship with Traefik stems from the fact that configuration issues like this can be hard to track and debug.  Ultimately after lots of searching and banging my head against a wall I found the answer in the Traefik docs and thought I would share here for anyone else who might run into this issue.  Again, this solution is specific to Docker Swarm mode.</p>



<p></p>



<p><a href="https://doc.traefik.io/traefik/providers/swarm/#configuration-examples">https://doc.traefik.io/traefik/providers/swarm/#configuration-examples</a></p>



<p>Expand that first section and you will see the solution:</p>



<figure class="wp-block-image size-full is-resized"><img decoding="async" width="634" height="181" src="/wp-content/uploads/2024/05/image.png" alt="" class="wp-image-565" style="width:720px;height:auto" srcset="/wp-content/uploads/2024/05/image.png 634w, /wp-content/uploads/2024/05/image-300x86.png 300w" sizes="(max-width: 634px) 100vw, 634px" /></figure>



<p>It turns out I just needed to update my <code>docker-compose.yml</code> and nest the labels under a deploy section, redeploy and everything was working as expected.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Stop all running containers with Docker</title>
		<link>/stop-all-running-containers-with-docker/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Wed, 03 Apr 2024 13:12:41 +0000</pubDate>
				<category><![CDATA[Automation]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Blog Post]]></category>
		<guid isPermaLink="false">/?p=557</guid>

					<description><![CDATA[These are some handy snippets I use on a regular basis when managing containers. I have one server in particular that can sometimes end up with 50 to 100 orphaned containers for various reasons. The easiest/quickest way to stop all of them is to do something like this: Let me break this down in case [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>These are some handy snippets I use on a regular basis when managing containers.  I have one server in particular that can sometimes end up with 50 to 100 orphaned containers for various reasons.  The easiest/quickest way to stop all of them is to do something like this:</p>



<pre class="wp-block-code"><code>docker container stop $(docker container ps -q)</code></pre>



<p>Let me break this down in case you are not familiar with the syntax.  Basically we are passing the output of <code>docker container ps -q</code> into docker container stop.  This works because the stop command can take a list of container ids which is what we get when passing the <code>-q</code> flag to docker container ps. </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Roll your own authenticator app with KeystoneJS and React &#8211; pt 3</title>
		<link>/roll-your-own-authenticator-app-with-keystonejs-and-react-pt-3/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Wed, 17 Jan 2024 17:11:00 +0000</pubDate>
				<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[Blog Post]]></category>
		<category><![CDATA[React]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">/?p=546</guid>

					<description><![CDATA[In our previous post we got to the point of displaying an OTP in our card component. Now it is time to refactor a bit and implement a countdown functionality to see when this token will expire. For now we will go ahead and add this logic into our Card component. In order to figure [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In our previous post we got to the point of displaying an OTP in our card component.  Now it is time to refactor a bit and implement a countdown functionality to see when this token will expire.  For now we will go ahead and add this logic into our Card component.  In order to figure out how to build this countdown timer we first need to understand how the TOTP counter is calculated.</p>



<p>In other words, we know that at TOTP token is derived from a secret key and the current time.  If we dig into the spec some we can find that time is a reference to Linux epoch time or the number of seconds that have elapsed since January 1st 1970.  For a little more clarification check out this Stackexchange <a href="https://crypto.stackexchange.com/questions/72558/what-time-is-used-in-a-totp-counter">article</a>.</p>



<p>So if we know that the time is based on epoch time, we also need to know that most TOTP tokens have a validity period of either 30 seconds or 60 seconds.  30 seconds is the most common standard so we will use that for our implementation.  If we put all that together then basically all we need is 2 variables:</p>



<ol class="wp-block-list">
<li>Number of seconds since epoch</li>



<li>How many seconds until this token expires</li>
</ol>



<p>The first one is easy:</p>



<pre class="wp-block-code"><code>let secondsSinceEpoch;
secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;

# This gives us a time like so: 1710338609</code></pre>



<p>For the second one we will need to do a little math but it&#8217;s pretty straightforward.  We need to divide <code>secondsSinceEpoch</code> by 30 seconds and then subtract this number from 30.  Here is what that looks like:</p>



<pre class="wp-block-code"><code>let secondsSinceEpoch;
let secondsRemaining;
const period = 30;

secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;
secondsRemaining = period - (secondsSinceEpoch % period);</code></pre>



<p>Now let&#8217;s put all of that together into a function that we can test out to make sure we are getting the results we expect.</p>



<pre class="wp-block-code"><code>const timer = setInterval(() => {
  countdown()
}, 1000)

function countdown() {
  let secondsSinceEpoch;
  let secondsRemaining;

  const period = 30;
  secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;

  secondsRemaining = period - (secondsSinceEpoch % period);
  console.log(secondsSinceEpoch, secondsRemaining)
  if (secondsRemaining == 1) {
    console.log("timer done")
    clearInterval(timer)
  }
}
</code></pre>



<p>Running this function should give you output similar to the following.  In this example we are stopping the timer once it hits 1 second just to show that everything is working as we expect.  In our application we will want this time to keep going forever:</p>



<pre class="wp-block-code"><code>1710339348, 12
1710339349, 11
1710339350, 10
1710339351, 9
1710339352, 8
1710339353, 7
1710339354, 6
1710339355, 5
1710339356, 4
1710339357, 3
1710339358, 2
1710339359, 1
"timer done"</code></pre>



<p>Here is a JSfiddle that shows it in action: https://jsfiddle.net/561vg3k7/</p>



<p>We can go ahead and add this function to our Card component and get it wired up.  I am going to skip ahead a bit and add a progress bar to our card that is synced with our countdown timer and changes colors as it drops below 10 seconds.  For now we will be using a <code>setInterval</code> function to accomplish this. </p>



<p>Here is what my updated <code>src/Components/Card.tsx</code> looks like:</p>



<pre class="wp-block-code"><code>import { useState } from "react";
import { IToken } from "../App"
import { TOTP } from 'totp-generator';


function Card({ token }: { token: IToken }) {
  const { otp } = TOTP.generate(token.token);
  const &#91;timerStyle, setTimerStyle] = useState("");
  const &#91;timerWidth, setTimerWidth] = useState("");


  function countdown() {
    let secondsSinceEpoch: number;
    let secondsRemaining: number = 30;
    const period = 30;
    secondsSinceEpoch = Math.ceil(Date.now() / 1000) - 1;
    secondsRemaining = period - (secondsSinceEpoch % period);
    setTimerWidth(`${100 - (100 / 30 * (30 - secondsRemaining))}%`)
    setTimerStyle(secondsRemaining &lt; 10 ? "salmon" : "lightgreen")
  }
  setInterval(() => {
    countdown();
  }, 250);
  return (
    &lt;>
      &lt;div className='card'>
        &lt;div className='progressBar'style={{ width: timerWidth, backgroundColor: timerStyle}}>&lt;/div>
        &lt;span>{token.issuer}&lt;/span>
        &lt;span>{token.account}&lt;/span>
        &lt;span >{otp}&lt;/span>
      &lt;/div>
    &lt;/>

  )
}
export default Card</code></pre>



<p>Pretty straightforward.  I also updated my <code>src/index.css</code> and added a style for our progress bar:</p>



<pre class="wp-block-code"><code>.progressBar {
  height: 10px;
  position: absolute;
  top: 0;
  left: 0;
  right: inherit;
}
// Also be sure to add position:relative to .card which is the parent of this.</code></pre>



<p>Here is what it all looks like in action:</p>



<figure class="wp-block-video"><video controls src="/wp-content/uploads/2024/03/otp-countdown.mp4"></video></figure>



<p>If you look closely you will notice a few interesting things.  First is that the color of the progress bar changes from green to red.  This is handled by our <code>timerStyle</code> variable.  That part is pretty simple, if the timer is less than 10 seconds we set the background color as salmon otherwise we use light green.  The width of the progress bar is controlled by `${100 &#8211; (100 / 30 * (30 &#8211; secondsRemaining))}%`</p>



<p>The other interesting thing to note is that when the timer runs out it automatically restarts at 30 seconds with a new OTP.  This is due to the fact that this component is re-rendering every 1/4 second and every time it re-renders it is running the entire function body including: <code>const { otp } = TOTP.generate(token.token);</code>.  </p>



<p>This is to be expected since we are using React and we are just using a setInterval.  It may be a little unexpected though if you aren&#8217;t as familiar with React render cycles.  For our purposes this will work just fine for now.  Stay tuned for pt 4 of this series where we wire up the backend API.</p>
]]></content:encoded>
					
		
		<enclosure url="/wp-content/uploads/2024/03/otp-countdown.mp4" length="29730" type="video/mp4" />

			</item>
		<item>
		<title>Roll your own authenticator app with KeystoneJS and React &#8211; pt 2</title>
		<link>/roll-your-own-authenticator-app-with-keystonejs-and-react-pt-2/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Thu, 11 Jan 2024 01:41:00 +0000</pubDate>
				<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Blog Post]]></category>
		<category><![CDATA[GraphQL]]></category>
		<category><![CDATA[KeystoneJS]]></category>
		<category><![CDATA[NodeJS]]></category>
		<category><![CDATA[Prisma]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">/?p=539</guid>

					<description><![CDATA[In part 1 of this series we built out a basic backend using KeystoneJS. In this part we will go ahead and start a new React frontend that will interact with our backend. We will be using Vite. Let&#8217;s get started. Make sure you are in the authenticator folder and run the following: Let&#8217;s go [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In part 1 of this series we built out a basic backend using KeystoneJS.  In this part we will go ahead and start a new React frontend that will interact with our backend.  We will be using Vite.  Let&#8217;s get started.  Make sure you are in the <code>authenticator</code> folder and run the following:</p>



<pre class="wp-block-code"><code>$ yarn create vite@latest
yarn create v1.22.21
&#91;1/4] Resolving packages...
&#91;2/4] Fetching packages...
&#91;3/4] Linking dependencies...
&#91;4/4] Building fresh packages...

success Installed "create-vite@5.2.2" with binaries:
      - create-vite
      - cva
&#x2714; Project name: … frontend
&#x2714; Select a framework: › React
&#x2714; Select a variant: › TypeScript

Scaffolding project in /home/mikeconrad/projects/authenticator/frontend...

Done. Now run:

  cd frontend
  yarn
  yarn dev

Done in 10.20s.</code></pre>



<p>Let&#8217;s go ahead and go into our frontend directory and get started:</p>



<pre class="wp-block-code"><code>$ cd frontend
$ yarn
yarn install v1.22.21
info No lockfile found.
&#91;1/4] Resolving packages...
&#91;2/4] Fetching packages...
&#91;3/4] Linking dependencies...
&#91;4/4] Building fresh packages...
success Saved lockfile.
Done in 10.21s.

$ yarn dev
yarn run v1.22.21
$ vite
Port 5173 is in use, trying another one...

  VITE v5.1.6  ready in 218 ms

  ➜  Local:   http://localhost:5174/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
</code></pre>



<p>Next go ahead and open the project up in your IDE of choice.  I prefer <a href="https://vscodium.com/">VSCodium</a>:</p>



<pre class="wp-block-code"><code>codium frontend</code></pre>



<p>Go ahead and open up <code>src/App.tsx</code> and remove all the boilerplate so it looks like this:</p>



<pre class="wp-block-code"><code>import './App.css'

function App() {

  return (
    &lt;&gt;
    &lt;/&gt;
  )
}

export default App</code></pre>



<p>Let&#8217;s start by building a card component that will display an individual token.  Our goal is something that looks like this:</p>



<figure class="wp-block-image size-full"><img decoding="async" width="928" height="320" src="/wp-content/uploads/2024/03/ente-auth.webp" alt="" class="wp-image-540" srcset="/wp-content/uploads/2024/03/ente-auth.webp 928w, /wp-content/uploads/2024/03/ente-auth-300x103.webp 300w, /wp-content/uploads/2024/03/ente-auth-768x265.webp 768w" sizes="(max-width: 928px) 100vw, 928px" /></figure>



<p>We will start by creating a Components folder with a Card component:</p>



<pre class="wp-block-code"><code>$ mkdir src/Components
$ touch src/Components/Card.tsx</code></pre>



<p>Let&#8217;s go ahead and make a couple updates, we will create this simple card component, add some dummy tokens and some basic styling.</p>



<pre class="wp-block-code"><code># src/App.tsx

import './App.css'
import Card from './Components/Card';
export interface IToken {
  account: string;
  issuer: string;
  token: string;
}
function App() {

  const tokens: IToken&#91;] = &#91;
    {
      account: 'enxoco@github.com',
      issuer: 'Github',
      token: 'AJFDLDAJKFK'
    },
    {
      account: 'mikeconrad@example.com',
      issuer: 'Example.com',
      token: 'KAJLFDJLKAFD'
    }
  ]
  return (
    &lt;&gt;
      &lt;div className='cardWrapper'&gt;
        {tokens.map(token =&gt; &lt;Card token={token} /&gt;)}
      &lt;/div&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code># src/Components/Card.tsx
import { IToken } from "../App"

function Card({ token }: { token: IToken }) {
  return (
    &lt;&gt;
      &lt;div className='card'&gt;
        &lt;span&gt;{token.issuer}&lt;/span&gt;
        &lt;span&gt;{token.account}&lt;/span&gt;
        &lt;span&gt;{token.token}&lt;/span&gt;
      &lt;/div&gt;
    &lt;/&gt;

  )
}
export default Card</code></pre>



<pre class="wp-block-code"><code># src/index.css
:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
  background-color: #2c2c2c;

}

.cardWrapper {
  display: flex;
}

.card {
    padding: 2em;
    min-width: 250px;
    border: 1px solid;
    margin: 10px;
    background-color: #333333;
    display: flex;
    flex-direction: column;
    align-items: baseline;
}</code></pre>



<p>Now you should have something that looks like this:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="928" height="320" src="/wp-content/uploads/2024/03/initial-token-frontend.webp" alt="" class="wp-image-541" srcset="/wp-content/uploads/2024/03/initial-token-frontend.webp 928w, /wp-content/uploads/2024/03/initial-token-frontend-300x103.webp 300w, /wp-content/uploads/2024/03/initial-token-frontend-768x265.webp 768w" sizes="(max-width: 928px) 100vw, 928px" /></figure>



<p>Alright, we have some of the boring stuff out of the way, now let&#8217;s start making some magic.  If you aren&#8217;t familiar with how TOTP tokens work, basically there is an Algorithm that generates them.  I would encourage you to read the <a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC</a> for a detailed explanation.  Basically it is an algorithm that generates a one time password using the current time as a source of uniqueness along with the secret key.</p>



<p>If we really wanted to we could implement this algorithm ourselves but thankfully there are some really simple libraries that do it for us.  For our project we will be using one called <code><a href="https://github.com/bellstrand/totp-generator">totp-generator</a></code>.  Let&#8217;s go ahead and install it and check it out:</p>



<pre class="wp-block-code"><code>$ yarn add totp-generator</code></pre>



<p>Now let&#8217;s add it to our card component and see what happens.  Using it is really simple.  We just need to import it, instantiate a new <code>TokenGenerator</code> and pass it our Secret key:</p>



<pre class="wp-block-code"><code># src/Components/card.tsx
import { TOTP } from 'totp-generator';
---
function Card({ token }: { token: IToken }) {
  const { otp, expires } = TOTP.generate(token.token)
  return (
    &lt;>
      &lt;div className='card'>
        &lt;span>{token.issuer}&lt;/span>
        &lt;span>{token.account}&lt;/span>
        &lt;span>{otp} - {expires}&lt;/span>
      &lt;/div>
    &lt;/>
  )
}</code></pre>



<p>Now save and go back to your browser and you should see that our secret keys are now being displayed as tokens:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="928" height="320" src="/wp-content/uploads/2024/03/token-display-1.png" alt="" class="wp-image-542" srcset="/wp-content/uploads/2024/03/token-display-1.png 928w, /wp-content/uploads/2024/03/token-display-1-300x103.png 300w, /wp-content/uploads/2024/03/token-display-1-768x265.png 768w" sizes="(max-width: 928px) 100vw, 928px" /></figure>



<p>That is pretty cool, the only problem is you need to refresh the page to refresh the token.  We will take care of that in part 3 of this series as well as handling fetching tokens from our backend.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Roll your own authenticator app with KeystoneJS and React</title>
		<link>/roll-your-own-authenticator-app-with-keystonejs-and-react/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Thu, 04 Jan 2024 00:59:49 +0000</pubDate>
				<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Blog Post]]></category>
		<category><![CDATA[GraphQL]]></category>
		<category><![CDATA[KeystoneJS]]></category>
		<category><![CDATA[NodeJS]]></category>
		<category><![CDATA[Prisma]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">/?p=533</guid>

					<description><![CDATA[In this series of articles we are going to be building an authenticator app using KeystoneJS for the backend and React for the frontend. The concept is pretty simple and yes there are a bunch out there already but I recently had a need to learn some of the ins and outs of TOTP tokens [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In this series of articles we are going to be building an authenticator app using KeystoneJS for the backend and React for the frontend.  The concept is pretty simple and yes there are a bunch out there already but I recently had a need to learn some of the ins and outs of TOTP tokens and thought this project would be a fun idea.  Let&#8217;s get started.</p>



<h5 class="wp-block-heading">Step 1: Init keystone app</h5>



<p>Open up a terminal and create a blank keystone project.  We are going to call our app authenticator to keep things simple.</p>



<pre class="wp-block-code"><code>$ yarn create keystone-app
yarn create v1.22.21
&#91;1/4] Resolving packages...
&#91;2/4] Fetching packages...
&#91;3/4] Linking dependencies...
&#91;4/4] Building fresh packages...

success Installed "create-keystone-app@9.0.1" with binaries:
      - create-keystone-app
&#91;###################################################################################################################################################################################] 273/273
&#x2728; You're about to generate a project using Keystone 6 packages.

&#x2714; What directory should create-keystone-app generate your app into? · authenticator

⠸ Installing dependencies with yarn. This may take a few minutes.
&#x26a0; Failed to install with yarn.
&#x2714; Installed dependencies with npm.


&#x1f389;  Keystone created a starter project in: authenticator

  To launch your app, run:

  - cd authenticator
  - npm run dev

  Next steps:

  - Read authenticator/README.md for additional getting started details.
  - Edit authenticator/keystone.ts to customize your app.
  - Open the Admin UI
  - Open the Graphql API
  - Read the docs
  - Star Keystone on GitHub

Done in 84.06s.
</code></pre>



<p>After a few minutes you should be ready to go.  Ignore the error about yarn not being able to install dependencies, it&#8217;s an issue with my setup.  Next go ahead and open up the project folder with your editor of choice.  I use VSCodium:</p>



<pre class="wp-block-code"><code>codium authenticator</code></pre>



<p>Let&#8217;s go ahead and remove all the comments from the <code>schema.ts</code> file and clean it up some:</p>



<pre class="wp-block-code"><code>sed -i '/\/\//d' schema.ts</code></pre>



<p>Also, go ahead and delete the Post and Tag list as we won&#8217;t be using them.  Our cleaned up <code>schema.ts</code> should look like this:</p>



<pre class="wp-block-code"><code>// schema.ts
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';

import {
  text,
  relationship,
  password,
  timestamp,
  select,
} from '@keystone-6/core/fields';


import type { Lists } from '.keystone/types';

export const lists: Lists = {
  User: list({
    access: allowAll,

    fields: {
      name: text({ validation: { isRequired: true } }),
      email: text({
        validation: { isRequired: true },
        isIndexed: 'unique',
      }),
      password: password({ validation: { isRequired: true } }),
      createdAt: timestamp({
        defaultValue: { kind: 'now' },
      }),
    },
  }),

};
</code></pre>



<p>Next we will define the schema for our tokens.  We will need 3 basic things to start with:</p>



<ul class="wp-block-list">
<li>Issuer</li>



<li>Secret Key</li>



<li>Account</li>
</ul>



<p>The only thing that really matters for generating a TOTP is actually the secret key.  The other two fields are mostly for identifying and differentiating tokens.  Go ahead and add the following to our <code>schema.ts</code> underneath the User list:</p>



<pre class="wp-block-code"><code>  Token: list({
    access: allowAll,
    fields: {
      secretKey: text({ validation: { isRequired: true } }),
      issuer: text({ validation: { isRequired: true }}),
      account: text({ validation: { isRequired: true }})
    }
  }),</code></pre>



<p>Now that we have defined our Token, we should probably link it to a user.  KeystoneJS makes this really easily.  We simply need to add a relationship field to our User list.  Add the following field to the user list:</p>



<pre class="wp-block-code"><code>tokens: relationship({ ref:'Token', many: true })</code></pre>



<p>We are defining a tokens field on the User list and tying it to our Token list.  We are also passing <code>many: true</code> saying that a user can have one or more tokens.  Now that we have the basics set up, let&#8217;s go ahead and spin up our app and see what we have:</p>



<pre class="wp-block-code"><code>$ yarn dev
yarn run v1.22.21
$ keystone dev
&#x2728; Starting Keystone
&#x2b50; Server listening on :3000 (http://localhost:3000/)
&#x2b50; GraphQL API available at /api/graphql
&#x2728; Generating GraphQL and Prisma schemas
&#x2728; The database is already in sync with the Prisma schema
&#x2728; Connecting to the database
&#x2728; Creating server
&#x2705; GraphQL API ready
&#x2728; Generating Admin UI code
&#x2728; Preparing Admin UI app
&#x2705; Admin UI ready
</code></pre>



<p>Our server should be running on localhost:3000 so let&#8217;s check it out!  The first time we open it up we will be greeted with the initialization screen.  Go ahead and create an account to login:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1012" height="806" src="/wp-content/uploads/2024/03/keystone-init-min.webp" alt="" class="wp-image-534" srcset="/wp-content/uploads/2024/03/keystone-init-min.webp 1012w, /wp-content/uploads/2024/03/keystone-init-min-300x239.webp 300w, /wp-content/uploads/2024/03/keystone-init-min-768x612.webp 768w" sizes="(max-width: 1012px) 100vw, 1012px" /></figure>



<p>Once you login you should see a dashboard similar to this:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="607" src="/wp-content/uploads/2024/03/keystone-dashboard-min-1024x607.webp" alt="" class="wp-image-535" srcset="/wp-content/uploads/2024/03/keystone-dashboard-min-1024x607.webp 1024w, /wp-content/uploads/2024/03/keystone-dashboard-min-300x178.webp 300w, /wp-content/uploads/2024/03/keystone-dashboard-min-768x455.webp 768w, /wp-content/uploads/2024/03/keystone-dashboard-min.webp 1169w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>You can see we have Users and Tokens that we can manage.  The beauty of KeystoneJS is that you get full CRUD functionality out of the box just by defining our schema!  Go ahead and click on Tokens to add a token:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="/wp-content/uploads/2024/03/create-token-min-1024x572.webp" alt="" class="wp-image-536" srcset="/wp-content/uploads/2024/03/create-token-min-1024x572.webp 1024w, /wp-content/uploads/2024/03/create-token-min-300x167.webp 300w, /wp-content/uploads/2024/03/create-token-min-768x429.webp 768w, /wp-content/uploads/2024/03/create-token-min.webp 1134w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>For this example I just entered some random text as an example.  This is enough to start testing out our TOTP functionality.  Click &#8216;Create Token&#8217; and you should see a list displaying existing tokens:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="275" src="/wp-content/uploads/2024/03/display-token-dashboard-min-1024x275.webp" alt="" class="wp-image-537" srcset="/wp-content/uploads/2024/03/display-token-dashboard-min-1024x275.webp 1024w, /wp-content/uploads/2024/03/display-token-dashboard-min-300x81.webp 300w, /wp-content/uploads/2024/03/display-token-dashboard-min-768x206.webp 768w, /wp-content/uploads/2024/03/display-token-dashboard-min-1536x413.webp 1536w, /wp-content/uploads/2024/03/display-token-dashboard-min.webp 1786w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>We are now ready to jump into the frontend.  Stay tuned for pt 2 of this series.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Hoots Wings</title>
		<link>/hoots-wings/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Wed, 12 Jul 2023 15:32:44 +0000</pubDate>
				<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Portfolio]]></category>
		<guid isPermaLink="false">/?p=495</guid>

					<description><![CDATA[While working for Morrison I had the pleasure of building a website for Hoots Wings. The CMS was Perch and it was mostly HTML, CSS, PHP and JavaScript on the frontend, however I built out a customer store locator using NodeJS and VueJS. I was the sole frontend developer responsible for taking the designs from [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>While working for <a href="https://morrison.agency/">Morrison</a> I had the pleasure of building a website for <a href="https://hootswings.com/">Hoots Wings.</a>  The CMS was <a href="https://grabaperch.com/">Perch</a> and it was mostly HTML, CSS, PHP and JavaScript on the frontend, however I built out a customer store locator using NodeJS and VueJS.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1024" height="514" src="/wp-content/uploads/2024/03/hoots-home.webp" alt="" class="wp-image-498" srcset="/wp-content/uploads/2024/03/hoots-home.webp 1024w, /wp-content/uploads/2024/03/hoots-home-300x151.webp 300w, /wp-content/uploads/2024/03/hoots-home-768x386.webp 768w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>I was the sole frontend developer responsible for taking the designs from SketchUp and translating them to the site you see now.  Most of the blocks and templates are built using a mix of PHP and HTML/SCSS.  There was also some JavaScript for things like getting the users location and rendering popups/modals.</p>



<p>The store locator was a separate piece that was built in Vue2.0  with a NodeJS backend.  For the backend I used <a href="https://keystonejs.com/">KeystoneJS</a> to hold all of the store information.  There was also some custom development that was done in order to sync the stores added via the CMS with <a href="https://www.yext.com/">Yext</a> and vice versa.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="522" src="/wp-content/uploads/2024/03/hoots-locations-min-1024x522.webp" alt="hootswings.com screenshot" class="wp-image-502" srcset="/wp-content/uploads/2024/03/hoots-locations-min-1024x522.webp 1024w, /wp-content/uploads/2024/03/hoots-locations-min-300x153.webp 300w, /wp-content/uploads/2024/03/hoots-locations-min-768x391.webp 768w, /wp-content/uploads/2024/03/hoots-locations-min-1536x783.webp 1536w, /wp-content/uploads/2024/03/hoots-locations-min.webp 1900w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>For that piece I ended up having to write a custom integration in Perch that would connect to the NodeJS backend and pull the stores but also make sure that those were in sync with Yext.  This required diving into the Yext API some and examining a similar integration that we had for another client site.</p>



<p>Unfortunately I don&#8217;t have any screen grabs of the admin side of things since that is proprietary but the system I built allowed a site admin to go in and add/edit store locations that would show up on the site and also show up in Yext with the appropriate information.</p>



<h4 class="wp-block-heading">Screenshots</h4>



<p>Here are some full screenshots of the site.<br></p>



<p>Homepage</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="635" height="1024" src="/wp-content/uploads/2024/03/hootswings-home-full-635x1024.webp" alt="hootswings.com homepage screenshot" class="wp-image-504" srcset="/wp-content/uploads/2024/03/hootswings-home-full-635x1024.webp 635w, /wp-content/uploads/2024/03/hootswings-home-full-186x300.webp 186w, /wp-content/uploads/2024/03/hootswings-home-full-768x1238.webp 768w, /wp-content/uploads/2024/03/hootswings-home-full-953x1536.webp 953w, /wp-content/uploads/2024/03/hootswings-home-full-1270x2048.webp 1270w, /wp-content/uploads/2024/03/hootswings-home-full-scaled.webp 1588w" sizes="(max-width: 635px) 100vw, 635px" /></figure>



<p></p>



<p>Menu Page</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="550" height="1024" src="/wp-content/uploads/2024/03/hootswings-menu-page-550x1024.png" alt="" class="wp-image-505" srcset="/wp-content/uploads/2024/03/hootswings-menu-page-550x1024.png 550w, /wp-content/uploads/2024/03/hootswings-menu-page-161x300.png 161w, /wp-content/uploads/2024/03/hootswings-menu-page-768x1430.png 768w, /wp-content/uploads/2024/03/hootswings-menu-page-825x1536.png 825w, /wp-content/uploads/2024/03/hootswings-menu-page-1100x2048.png 1100w, /wp-content/uploads/2024/03/hootswings-menu-page.png 1920w" sizes="(max-width: 550px) 100vw, 550px" /></figure>



<p></p>



<p>Locations Page</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="432" height="1024" src="/wp-content/uploads/2024/03/hootswings-locations-page-432x1024.png" alt="" class="wp-image-506" srcset="/wp-content/uploads/2024/03/hootswings-locations-page-432x1024.png 432w, /wp-content/uploads/2024/03/hootswings-locations-page-126x300.png 126w, /wp-content/uploads/2024/03/hootswings-locations-page-768x1822.png 768w, /wp-content/uploads/2024/03/hootswings-locations-page-863x2048.png 863w, /wp-content/uploads/2024/03/hootswings-locations-page.png 1920w" sizes="(max-width: 432px) 100vw, 432px" /></figure>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Hilger Grading Portal</title>
		<link>/hilger-grading-portal/</link>
		
		<dc:creator><![CDATA[mikeconrad]]></dc:creator>
		<pubDate>Sun, 21 May 2023 20:07:50 +0000</pubDate>
				<category><![CDATA[Software Engineering]]></category>
		<category><![CDATA[Portfolio]]></category>
		<guid isPermaLink="false">/?p=509</guid>

					<description><![CDATA[Back around 2014 I took on my first freelance development project for a Homeschool Co-op here in Chattanooga called Hilger Higher Learning. The problem that they were trying to solve involved managing grades and report cards for their students. In the past, they had a developer build a rudimentary web application that would allow them [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Back around 2014 I took on my first freelance development project for a Homeschool Co-op here in Chattanooga called <a href="https://www.hhlearning.com/" data-type="link" data-id="https://www.hhlearning.com/">Hilger Higher Learning</a>.  The problem that they were trying to solve involved managing grades and report cards for their students.  In the past, they had a developer build a rudimentary web application that would allow them to enter grades for students, however it lacked any sort of concurrency meaning that if two teachers were making changes to the same student at the same time, teacher b&#8217;s changes would overwrite teacher a&#8217;s changes.  This was obviously a huge headache.</p>



<p>I built out the first version of the app using PHP and HTML, CSS and Datatables with lots of jQuery sprinkled in.  I built in custom functionality that allowed them to easily compile and print all the report cards for all students with the simple click of a button.  It was a game changer or them and streamlined the process significantly.</p>



<p>That system was in production for 5 years or so with minimal updates and maintance.  I recently rebuilt it using React and ChakraUI on the frontend and <a href="https://keystonejs.com/">KeystoneJS</a> on the backend.  I also modernized the deployment by building Docker images for the frontend/backend.  I actually ended up keeping parts of it in PHP due to the fact that I couldn&#8217;t find a JavaScript library that would solve the challenges I had.  Here are some screenshots of it in action:</p>



<p></p>



<p>This is the page listing all teachers in the system and whether or not they have admin privileges.  Any admin user can grant other admin users this privilege.  There is also a button to send the teacher a password reset email (via Postmark API integration) and an option that allows admin users to impersonate other users for troubleshooting and diagnostic purposes.</p>



<p></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="514" src="/wp-content/uploads/2024/03/hilger-portal-all-teachers-1024x514.webp" alt="" class="wp-image-511" srcset="/wp-content/uploads/2024/03/hilger-portal-all-teachers-1024x514.webp 1024w, /wp-content/uploads/2024/03/hilger-portal-all-teachers-300x150.webp 300w, /wp-content/uploads/2024/03/hilger-portal-all-teachers-768x385.webp 768w, /wp-content/uploads/2024/03/hilger-portal-all-teachers-1536x770.webp 1536w, /wp-content/uploads/2024/03/hilger-portal-all-teachers.webp 1920w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>The data is all coming from the KeystoneJS backend GraphQL API.  I am using <a href="https://www.npmjs.com/package/urql">urql</a> for fetching the data and handling mutations.  This is the page that displays students.  It is filterable and searchable.  Teachers also have the ability to mark a student as active or inactive for the semester as well as delete them from the system.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="513" src="/wp-content/uploads/2024/03/hilger-portal-all-students-1-1024x513.webp" alt="" class="wp-image-525" srcset="/wp-content/uploads/2024/03/hilger-portal-all-students-1-1024x513.webp 1024w, /wp-content/uploads/2024/03/hilger-portal-all-students-1-300x150.webp 300w, /wp-content/uploads/2024/03/hilger-portal-all-students-1-768x385.webp 768w, /wp-content/uploads/2024/03/hilger-portal-all-students-1-1536x770.webp 1536w, /wp-content/uploads/2024/03/hilger-portal-all-students-1.webp 1920w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Clicking on a student takes the teacher/admin to an edit course screen where they can add and remove courses for each student.  A teacher can add as many courses as they need.  If multiple teachers have added courses for this student, the user will only see the courses they have entered.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="514" src="/wp-content/uploads/2024/03/hilger-portal-edit-student-1024x514.webp" alt="" class="wp-image-526" srcset="/wp-content/uploads/2024/03/hilger-portal-edit-student-1024x514.webp 1024w, /wp-content/uploads/2024/03/hilger-portal-edit-student-300x150.webp 300w, /wp-content/uploads/2024/03/hilger-portal-edit-student-768x385.webp 768w, /wp-content/uploads/2024/03/hilger-portal-edit-student-1536x770.webp 1536w, /wp-content/uploads/2024/03/hilger-portal-edit-student.webp 1920w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>There is another page that allows admin users to view and manage all of the parents in the system.  It allows them to easily send a password reset email to the parents as well as to view the parent portal.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="513" src="/wp-content/uploads/2024/03/hilger-portal-parents-1024x513.webp" alt="" class="wp-image-527" srcset="/wp-content/uploads/2024/03/hilger-portal-parents-1024x513.webp 1024w, /wp-content/uploads/2024/03/hilger-portal-parents-300x150.webp 300w, /wp-content/uploads/2024/03/hilger-portal-parents-768x385.webp 768w, /wp-content/uploads/2024/03/hilger-portal-parents-1536x770.webp 1536w, /wp-content/uploads/2024/03/hilger-portal-parents.webp 1920w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Technologies used</h2>



<ul class="wp-block-list">
<li>Digital Ocean Droplet (Server) &#8211; Ubuntu Server</li>



<li>Docker (Frontend, Backend, PHP, Postgresql database)</li>



<li>Git</li>



<li>NodeJS</li>



<li>PHP</li>



<li>ChakraUI</li>



<li>KeystoneJS</li>



<li>Postmark</li>



<li>GraphQL</li>



<li>Typescript</li>



<li>React</li>



<li>urql</li>
</ul>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
