<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Avery Tan, Security Professional</title><link>https://averytan.com/</link><atom:link href="https://averytan.com/index.xml" rel="self" type="application/rss+xml"/><description>Avery Tan, Security Professional</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Mon, 24 Oct 2022 00:00:00 +0000</lastBuildDate><image><url>https://averytan.com/media/icon_hu_d3051226da7ea5ed.png</url><title>Avery Tan, Security Professional</title><link>https://averytan.com/</link></image><item><title>Example Talk</title><link>https://averytan.com/event/example/</link><pubDate>Sat, 01 Jun 2030 13:00:00 +0000</pubDate><guid>https://averytan.com/event/example/</guid><description>&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Click on the &lt;strong>Slides&lt;/strong> button above to view the built-in slides feature.&lt;/span>
&lt;/div>
&lt;p>Slides can be added in a few ways:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Create&lt;/strong> slides using Hugo Blox Builder&amp;rsquo;s
feature and link using &lt;code>slides&lt;/code> parameter in the front matter of the talk file&lt;/li>
&lt;li>&lt;strong>Upload&lt;/strong> an existing slide deck to &lt;code>static/&lt;/code> and link using &lt;code>url_slides&lt;/code> parameter in the front matter of the talk file&lt;/li>
&lt;li>&lt;strong>Embed&lt;/strong> your slides (e.g. Google Slides) or presentation video on this page using
.&lt;/li>
&lt;/ul>
&lt;p>Further event details, including
such as image galleries, can be added to the body of this page.&lt;/p></description></item><item><title>Max Redundancy with 3-2-1!</title><link>https://averytan.com/post/max-redundancy-with-3-2-1/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://averytan.com/post/max-redundancy-with-3-2-1/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Backups are important and an often overlooked aspect in the minds of the day-to-day average joes. I definitely was reminded the importance of having a backup strategy during a failure of a routine kernel upgrade on my main Linux workhorse a few years ago. In this particular absolut tragedy, I count myself incredibly lucky at being able to leverage
to successfully recover most of my data.&lt;/p>
&lt;p>Backups are even more doubly important in production enterprise systems holding client and/or otherwise business-critical data. This of course, should be an obvious and needless-to-mention axiom, and a lack of a backup strategy or adequate disaster recovery response plan would be utterly devastating to any meaningfully sized organization, resulting in all sorts of legal and/or compliance risk, not to mention potential financial losses.&lt;/p>
&lt;p>I personally might not be a organization beholden to the risks of non-compliance to such industry and regulatory frameworks and the repurcussions that come with it, but I do have what I consider to be critically important personal data whose loss would be a true tragedy on a personal level in the form of family photos, historical personal budget/financial data, media server configuration setting files, personally curated recipes collection, and more in the form of a variety of self-hosted applications running on individual docker containers on my home server.&lt;/p>
&lt;h2 id="the-3-2-1-strategy">The 3-2-1 Strategy&lt;/h2>
&lt;p>The 3-2-1 backup strategy is one of the most widely recommended approaches for protecting data. It’s simple, flexible, and works across personal and enterprise contexts.&lt;/p>
&lt;p>This strategy comprises of having 3 copies of your data:&lt;/p>
&lt;ul>
&lt;li>the primary &amp;ldquo;prod&amp;rdquo; data; and&lt;/li>
&lt;li>2 backup copies, 1 of which is stored offsite.&lt;/li>
&lt;/ul>
&lt;p>Having a copy of the data located offsite provides geographic separation, which reduces the risk of data loss via fire, theft, floods, or other natural or man-made disasters!&lt;/p>
&lt;p>Offsite can mean cloud storage, a safety deposit box, or a drive you rotate to another location.&lt;/p>
&lt;h2 id="our-implementation">Our Implementation&lt;/h2>
&lt;p>In our specific case, we will implement a version of the 3-2-1 strategy using 3 SSD harddisks:&lt;/p>
&lt;ul>
&lt;li>SSD #1 - our primary drive installed in a Hard Disk enclosure connected to our home application server&lt;/li>
&lt;li>SSD #2 - our backup drive attached directly to our home application server via a PCI-to-usb adapter&lt;/li>
&lt;li>SSD #3 - our offsite backup drive attached to a separate backup server located in geographically separated area from our main application server&lt;/li>
&lt;/ul>
&lt;p>The relevant directory structure on the primary application server is the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">/home/s1na/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── media-stack
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   └── starr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── pinchflat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ └── config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── jellyseer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ └── config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── jellyfin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ ├── cache/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ └── config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│   └── other-media-server-related-docker-containers...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── config/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/mnt/f15/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── ntfy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ └── server.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── cache/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── docker-containers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── firefly
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── volumes/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── immich
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── volumes/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── calibre
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── volumes/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── mealie
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── volumes/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── other-hosted-applications...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── volumes/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In implementing our 3-2-1 backup strategy, we are interested primarily in the docker applications located in /mnt/f15/docker-containers but only the config data in our *arr media stack folders. For the media server, the actual media data lives at a separate location and the sheer volume of media data (movies + tv shows) and the relatively low importance make including full backups of the media server data impractical.&lt;/p>
&lt;p>We will also be using ntfy in order to trigger email notifications on success or failure. The shutdown of all application containers within the docker-containers directory before a backup copy is taken is
. Hence we run an instance of ntfy outside of /mnt/f15/docker-containers to ensure that ntfy is always running to report the status of the backup process, both success and failure.&lt;/p>
&lt;h2 id="nfty">nfty&lt;/h2>
&lt;p>Docker compose file for ntfy&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-docker" data-lang="docker">&lt;span class="line">&lt;span class="cl">services:&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> ntfy:&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> image: binwiederhier/ntfy&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> container_name: ntfy&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> command:&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> - serve&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> volumes:&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> - ./cache:/var/cache/ntfy&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> - ./config:/etc/ntfy&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> ports:&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> - 10700:80&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> healthcheck: &lt;span class="c1"># optional: remember to adapt the host:port to your environment&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> test: &lt;span class="o">[&lt;/span>&lt;span class="s2">&amp;#34;CMD-SHELL&amp;#34;&lt;/span>, &lt;span class="s2">&amp;#34;wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo &amp;#39;\&amp;#34;healthy\&amp;#34;\\s*:\\s*true&amp;#39; || exit 1&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> interval: 60s&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> timeout: 10s&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> retries: &lt;span class="m">3&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> start_period: 40s&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> restart: unless-stopped&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To set up access to Gmail smtp server, we need a server.yml file within the mapped config volume:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># this is the local ip of the application server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ntfy is running out of port 10700&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">base-url: http://192.168.1.76:10700
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">smtp-sender-addr: &lt;span class="s2">&amp;#34;smtp.gmail.com:587&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">smtp-sender-user: &lt;span class="s2">&amp;#34;sender.email.addr@gmail.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">smtp-sender-pass: &lt;span class="s2">&amp;#34;app-password&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">smtp-sender-from: &lt;span class="s2">&amp;#34;sender.email.addr@gmail.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>As of writing, Google now requires the use of something called an
.&lt;/p>
&lt;p>With the ntfy container spun up and running, email notifications can then be triggered via a HTTP POST request:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Email: recipient.email.addr@gmail.com&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Tags: +1,tada&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;Everything went fine with the backup.&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://localhost:10700/docker-backup
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>
&lt;figure id="figure-ntfy-email-notification">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="ntfy email notification" srcset="
/post/max-redundancy-with-3-2-1/ntfy-email_hu_6b7bed71283efe13.webp 400w,
/post/max-redundancy-with-3-2-1/ntfy-email_hu_4991f048344dedd1.webp 760w,
/post/max-redundancy-with-3-2-1/ntfy-email_hu_fd79cfc412d523e2.webp 1200w"
src="https://averytan.com/post/max-redundancy-with-3-2-1/ntfy-email_hu_6b7bed71283efe13.webp"
width="760"
height="202"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
ntfy email notification
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;h2 id="borg--rsync">Borg &amp;amp; Rsync&lt;/h2>
&lt;p>After extensive consultation with the likes of ChatGPT and Claude, Borg will be the primary backup tool we use to implement our 3-2-1 strategy. Borg has considerable strengths when compared to Rsync, primarily which is its ability to perform versioned and deduplicated incremental backups.&lt;/p>
&lt;p>Prior to implementing Borg for this strategy, Rsync was used in a previous iteration of this strategy. Rsync would periodically make a incremental backup copy which overrides the existing backup copy.&lt;/p>
&lt;p>Versioned backups are critically important for the reason that if the current prod data somehow became corrupted and unreadable, making a copy of this corrupted unreadable data and then overwriting the existing readable backup copy would then result in an absolut tragic loss of data!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># initiating a new empty borg backup dir&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">borg init --encryption&lt;span class="o">=&lt;/span>none /mnt/docker/back/up/dir
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># executing the backup with some nifty informative flags&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">borg create --stats --progress --compression zstd,9 /mnt/docker/back/up/dir::docker-backup-&lt;span class="k">$(&lt;/span>date +%F&lt;span class="k">)&lt;/span> /source/dir/to/back/up
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># list all backup copies&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">borg list /mnt/docker/back/up/dir
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># prune to remove old outdated backup copies&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">borg prune -v --keep-daily&lt;span class="o">=&lt;/span>&lt;span class="m">14&lt;/span> --keep-weekly&lt;span class="o">=&lt;/span>&lt;span class="m">4&lt;/span> --keep-monthly&lt;span class="o">=&lt;/span>&lt;span class="m">6&lt;/span> --keep-yearly&lt;span class="o">=&lt;/span>&lt;span class="m">2&lt;/span> /mnt/docker/back/up/dir
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Another strength of Borg is its ability to perform encryption at rest via the &amp;ndash;encryption mode flag. In our specific implementation, I made the calculated trade-off opting not to encrypt data at rest since none of my servers or services are publicly internet-facing.&lt;/p>
&lt;p>Communication channels between the prod application server and the remote offsite backup server is done via
, giving us a reliable connection while at the same time not having either servers exposing naked services directly to the scary dangerous internet.&lt;/p>
&lt;h2 id="the-backup-scripts">The Backup Scripts&lt;/h2>
&lt;p>We will create 2 separate backup scripts:&lt;/p>
&lt;ul>
&lt;li>one for backing up the media server configuration files (in case tragedy ever befalls our *arr stack, we can quickly spin up a fully configured plug-n-play replacement); and&lt;/li>
&lt;li>one for a full backing up of the more &amp;lsquo;critical&amp;rsquo; self-hosted applicationa and their respective data.&lt;/li>
&lt;/ul>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">My *arr stack media server is a basic implementation as described in the
.&lt;/span>
&lt;/div>
&lt;h3 id="the-arr-stack-media-server">The *arr stack media server&lt;/h3>
&lt;p>Containers running as part of the media server are specified in the single docker-compose.yml file located at the home/s1na/media-stack/starr directory.&lt;/p>
&lt;p>The following bash script:&lt;/p>
&lt;ul>
&lt;li>Checks that both the source and destination directories exist.&lt;/li>
&lt;li>If those checks pass, it then stops the containers specified in home/s1na/media-stack/starr/docker-compose.yml.&lt;/li>
&lt;li>The script then calls Borg to perform the incremental backup and prunes old outdated copies.&lt;/li>
&lt;li>The containers are then spun up.&lt;/li>
&lt;li>Email notifications are executed via ntfy if any intermediary step fails or if the backup is successful&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Define variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">BACKUP_DIR_LOCAL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/mnt/bae/media-server-config-backup-dir&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;/home/s1na/media-stack&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LOG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/log/media-server-config-backup.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Function to send email via ntfy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">send_email&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">subject&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">message&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> curl &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Email: recipient.email.addr@gmail.com&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Tags: cd&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$subject&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="nv">$message&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://192.168.1.76:10700/media-server-config-backup
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Log function&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date &lt;span class="s1">&amp;#39;+%Y-%m-%d %H:%M:%S&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2"> - &lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Start script&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Backup process started.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if backup directory exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> ! -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Backup directory &lt;/span>&lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Backup directory &lt;/span>&lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if backup directory exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> ! -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Docker-containers directory &lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Docker-containers directory &lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Stop all containers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Stopping all containers...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> app_dir in &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/*&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">/docker-compose.yml&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Stopping containers for app in: &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Change to the app directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to change directory to &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to change dir while trying to stop containers into &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> during the backup process&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> sudo docker compose down&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> stopped successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to stop containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to stop containers in &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Borg incremental backup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Starting borg backup...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> sudo borg create --stats --progress --compression zstd,9 &lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>::media-server-config-backup-&lt;span class="k">$(&lt;/span>date +%F&lt;span class="k">)&lt;/span> &lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Borg backup completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Borg backup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Borg backup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Borg Clean up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> sudo borg prune -v --keep-daily&lt;span class="o">=&lt;/span>&lt;span class="m">14&lt;/span> --keep-weekly&lt;span class="o">=&lt;/span>&lt;span class="m">4&lt;/span> --keep-monthly&lt;span class="o">=&lt;/span>&lt;span class="m">12&lt;/span> --keep-yearly&lt;span class="o">=&lt;/span>&lt;span class="m">5&lt;/span> &lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Borg cleanup completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Bor cleanup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Cleanup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Borg Cleanup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Restart containers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Restarting containers...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> app_dir in &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/*&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">/docker-compose.yml&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Restarting containers for app in: &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Change to the app directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to change directory to &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to change dir into &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> during the backup process&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> sudo docker compose up -d&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> restarted successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to restart containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to restart containers in &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Backup process completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">send_email &lt;span class="s2">&amp;#34;Media Server Config Backup Successful&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Backup process completed successfully. See log for details: &lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exit&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This script is periodically run via cron:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># runs every first of the month at 6:00 AM MST.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0&lt;/span> &lt;span class="m">6&lt;/span> &lt;span class="m">1&lt;/span> * * /mnt/f15/desktop/backup-scripts/media_server_config_backup_script.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="the-self-hosted-applications">The self-hosted applications&lt;/h3>
&lt;p>The containers in the /mnt/f15/docker-containers directory form the critical assets where we want to implement full backup of everything &amp;ndash; configuration settings as well as full application data and files. The goal is for speedy and full recovery in the event of a full disk failure of the primary harddisk.&lt;/p>
&lt;p>Each self-hosted application lives in its own directory within the docker-containers directory. Typically, each of these application directories contains a docker-compose.yml file and a volumes subdirectory where all application configuration files and data files live.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Typical directory structure&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/mnt/f15/docker-containers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── some-application
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── docker-compose.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── volumes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── config
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── config-files
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └── data-files
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── cache-dir-or-other-app-specific-volume-dirs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── cache-files-or-other-app-specific-files
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The backup bash script targeting these applications is similar to the one for the media config backup, with a small difference in how containers are stopped and started. The following takes place:&lt;/p>
&lt;ul>
&lt;li>Check for existence of both source and destination directories&lt;/li>
&lt;li>Stops all containers by iteratively entering each subdir of /mnt/f15/docker-containers/docker-containers&lt;/li>
&lt;li>Borg performs the incremental backup and prunes old outdated copies.&lt;/li>
&lt;li>The containers are then spun back up.&lt;/li>
&lt;li>Email notifications are executed via ntfy if any intermediary step fails or if the backup is successful.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Define variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">BACKUP_DIR_LOCAL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/mnt/bae/borg-backup-dir&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;/mnt/f15/docker-containers&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LOG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/log/docker-backup.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Function to send email via ntfy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">send_email&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">subject&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">message&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> curl &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Email: recipient.email.addr@gmail.com&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Tags: cd&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$subject&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="nv">$message&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://192.168.1.76:10700/docker-backup
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Log function&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date &lt;span class="s1">&amp;#39;+%Y-%m-%d %H:%M:%S&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2"> - &lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Start script&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Backup process started.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if backup directory exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> ! -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Backup directory &lt;/span>&lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Backup directory &lt;/span>&lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if backup directory exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> ! -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Docker-containers directory &lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Docker-containers directory &lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Stop all containers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Stopping all containers...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> app_dir in &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/*&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">/docker-compose.yml&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Stopping containers for app in: &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Change to the app directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to change directory to &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to change dir while trying to stop containers into &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> during the backup process&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> sudo docker compose down&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> stopped successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to stop containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to stop containers in &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Borg incremental backup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Starting borg backup...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> sudo borg create --stats --progress --compression zstd,9 &lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>::docker-backup-&lt;span class="k">$(&lt;/span>date +%F&lt;span class="k">)&lt;/span> &lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Borg backup completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Borg backup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Borg backup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Borg Clean up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> sudo borg prune -v --keep-daily&lt;span class="o">=&lt;/span>&lt;span class="m">14&lt;/span> --keep-weekly&lt;span class="o">=&lt;/span>&lt;span class="m">4&lt;/span> --keep-monthly&lt;span class="o">=&lt;/span>&lt;span class="m">12&lt;/span> --keep-yearly&lt;span class="o">=&lt;/span>&lt;span class="m">5&lt;/span> &lt;span class="nv">$BACKUP_DIR_LOCAL&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Borg cleanup completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Bor cleanup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Cleanup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Borg Cleanup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Restart containers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Restarting containers...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> app_dir in &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$DOCKER_CONTAINERS_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>/*&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">/docker-compose.yml&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Restarting containers for app in: &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Change to the app directory&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to change directory to &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to change dir into &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> during the backup process&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> sudo docker compose up -d&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2"> restarted successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Failed to restart containers for app &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Docker Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Failed to restart containers in &lt;/span>&lt;span class="nv">$app_dir&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Backup process completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">send_email &lt;span class="s2">&amp;#34;Docker Backup Successful&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Backup process completed successfully. See log for details: &lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exit&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The crontab entry for this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># runs every other day at 4:00 AM MST&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0&lt;/span> &lt;span class="m">4&lt;/span> */2 * * /mnt/f15/desktop/backup-scripts/docker_backup_script.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-resultant-local-backups">The resultant local backups&lt;/h2>
&lt;p>
&lt;figure id="figure-a-recent-ntfy-notification-email">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Recent ntfy email" srcset="
/post/max-redundancy-with-3-2-1/recent-docker_hu_4eb43dd5d4e8a97b.webp 400w,
/post/max-redundancy-with-3-2-1/recent-docker_hu_26a5c38b321b333a.webp 760w,
/post/max-redundancy-with-3-2-1/recent-docker_hu_50de44a26859535.webp 1200w"
src="https://averytan.com/post/max-redundancy-with-3-2-1/recent-docker_hu_4eb43dd5d4e8a97b.webp"
width="760"
height="184"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
A recent ntfy notification email
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">s1na@big-dumpling:~$ borg list /mnt/bae/docker-backup-dir/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-06-19 Wed, 2025-06-18 21:52:46 &lt;span class="o">[&lt;/span>4f175980a4ca28ad26ec8a14e224b6aa5c4c5a0485027e5b6dfee33c81de88dd&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-07-13 Sat, 2025-07-12 23:28:56 &lt;span class="o">[&lt;/span>492cb6d014c6bac827cd21f0f10b5feba652342039af8319328361da8e79aab8&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-07-21 Sun, 2025-07-20 22:01:00 &lt;span class="o">[&lt;/span>23fb593765cf59f8f7c99edf9d98a51609c3826df2c37fceba117a17ec20427e&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-07-27 Sat, 2025-07-26 22:00:59 &lt;span class="o">[&lt;/span>aec28e0224ebcb6a8ae8ba0d9fffb8f84a9fb64d67e9281c5c0097fa40e93496&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-01 Thu, 2025-07-31 22:01:00 &lt;span class="o">[&lt;/span>e32cef441520f19e4b62752fa1c1bd9792baa4f311063db3dc301075b79bef9d&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-03 Sat, 2025-08-02 22:00:58 &lt;span class="o">[&lt;/span>449d8d699abc67f88d7dac8efc4a7a254dd22d4ae75375479b40c88366bd5726&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-09 Fri, 2025-08-08 22:01:03 &lt;span class="o">[&lt;/span>93311a0e5eacc7564e3c0471ecdec5da051949153b018bb2ddceef7db796b5af&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-11 Sun, 2025-08-10 22:01:03 &lt;span class="o">[&lt;/span>2f25c96fc8a919062015f24b150e9289417ab73366feb4ae841bb94d14b9f59d&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-13 Wed, 2025-08-13 04:01:04 &lt;span class="o">[&lt;/span>f8318d0f435d0ecc3f018c42a981696abdea287761d0c059d7e6b4227b581872&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-15 Fri, 2025-08-15 04:01:01 &lt;span class="o">[&lt;/span>7c41a0f970d653bd6bcd3799ed62d0b6ede0f1a3f241283b94418acf0847193c&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-19 Tue, 2025-08-19 04:01:04 &lt;span class="o">[&lt;/span>abd740749118c55068e7dcb87c12ed105986e8691ea076205c8d78ca77bf41d7&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-21 Thu, 2025-08-21 04:01:00 &lt;span class="o">[&lt;/span>da9ddb1a6cc6f72e5cc46b328b321472d710230c7a3f5d64979e51bec53cb247&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-23 Sat, 2025-08-23 04:00:50 &lt;span class="o">[&lt;/span>fb0c86b220a22bb9d7343ddf97a55f4b35a4e32893b2325e37086552195a309d&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-25 Mon, 2025-08-25 04:00:51 &lt;span class="o">[&lt;/span>9f3e9f2c3ddd8fe514608995791254eea6ea820bad38f17c250f2a320fa82a5c&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-27 Wed, 2025-08-27 04:00:50 &lt;span class="o">[&lt;/span>5727271708eefe625c9d1d12cff43c433df07231777af274b4dcda6ab1ce6ce1&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-29 Fri, 2025-08-29 04:00:55 &lt;span class="o">[&lt;/span>1a5d6b36da5a02c29750dccdcbb9cb74de2899e992c0cd90809c9677fd5d9149&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-08-31 Sun, 2025-08-31 04:00:51 &lt;span class="o">[&lt;/span>2cf17793a4ae580007d504518209d45fd8b352b7b41150b8230da5b991671c08&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-09-01 Mon, 2025-09-01 04:00:55 &lt;span class="o">[&lt;/span>87c4d2ad57f27cdded1735baa346e01efbe1cf7f5a588205ea62dded8cff7892&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-09-03 Wed, 2025-09-03 04:00:51 &lt;span class="o">[&lt;/span>71234a94c8836af7c2baaa36956051d15f7c8d34c5474f304252ec2bf9cfc2c3&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker-backup-2025-09-05 Fri, 2025-09-05 04:00:52 &lt;span class="o">[&lt;/span>6db1ecd6da8ac1173366266a41867453ef19d57f84c30611b7cc98af4e77c725&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">s1na@big-dumpling:~$ borg info /mnt/bae/borg-backup-dir/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Repository ID: 2290625c5b22dc05897fe230602d386c376780016a6f393cf9578c9a0b73008f
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Location: /mnt/bae/docker-backup-dir
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Encrypted: No
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Cache: /root/.cache/borg/2290625c5b22dc05897fe230602d386c376780016a6f393cf9578c9a0b73008f
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Security dir: /root/.config/borg/security/2290625c5b22dc05897fe230602d386c376780016a6f393cf9578c9a0b73008f
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">------------------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Original size Compressed size Deduplicated size
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">All archives: 2.63 TB 2.59 TB 126.98 GB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Unique chunks Total chunks
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Chunk index: &lt;span class="m">101445&lt;/span> &lt;span class="m">1931093&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">s1na@big-dumpling:~$ borg list /mnt/bae/media-server-config-backup-dir/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">media-server-config-backup-2025-08-02 Sat, 2025-08-02 05:31:20 &lt;span class="o">[&lt;/span>a058d03472f47d91f51f5d9728ec598a40a8b52d19accc844c6a783cec4aab3e&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">media-server-config-backup-2025-09-01 Mon, 2025-09-01 06:00:13 &lt;span class="o">[&lt;/span>4ba86566575f271f7391cca663d4d7f56efcc5e9b9c8b0598d752f84ef384d39&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">s1na@big-dumpling:~$ borg info /mnt/bae/media-server-config-backup-dir/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Repository ID: f57b38ff467ef220a76fbd056de15e0c906822bbfb0410740b2b4f555692652c
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Location: /mnt/bae/media-server-config-backup-dir
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Encrypted: No
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Cache: /root/.cache/borg/f57b38ff467ef220a76fbd056de15e0c906822bbfb0410740b2b4f555692652c
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Security dir: /root/.config/borg/security/f57b38ff467ef220a76fbd056de15e0c906822bbfb0410740b2b4f555692652c
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">------------------------------------------------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Original size Compressed size Deduplicated size
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">All archives: 27.94 GB 25.81 GB 19.61 GB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Unique chunks Total chunks
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Chunk index: &lt;span class="m">43801&lt;/span> &lt;span class="m">70102&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="sending-a-backup-copy-offsite">Sending a backup copy offsite&lt;/h2>
&lt;p>Unlike for making the versioned incremental local backups where we employ borg, sending a copy of these local borg backup directories to a geographically separate remote server will be done via rsync. We assume that rsync will never run a backup job to the remote server at the same that borg is performing its own incremental backup, and will schedule our cron jobs to ensure this.&lt;/p>
&lt;p>We will also need to perform passwordless ssh login to our remote server. The following generates a new private/public key pair and then copies the public key to our remote server. Our remote server is aptly named lil-dumpling.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">ssh-keygen -t ed25519 -C &lt;span class="s2">&amp;#34;rsync backup key&amp;#34;&lt;/span> -f ~/.ssh/rsync_backup_key
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ssh-copy-id -i ~./ssh/rsync_backup_key.pub s1na@lil-dumpling
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Add a ssh profile to our ~/.ssh/config file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">Host lil-dumpling
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> HostName 10.77.88.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> User s1na
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> IdentityFile ~/.ssh/rsync_backup_key
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>
&lt;figure id="figure-testing-ssh-ing-our-remote-server">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Testing the connection" srcset="
/post/max-redundancy-with-3-2-1/ssh_hu_70d5ad093c17b8a0.webp 400w,
/post/max-redundancy-with-3-2-1/ssh_hu_ee2d7e755291db30.webp 760w,
/post/max-redundancy-with-3-2-1/ssh_hu_477056169191e86d.webp 1200w"
src="https://averytan.com/post/max-redundancy-with-3-2-1/ssh_hu_70d5ad093c17b8a0.webp"
width="760"
height="181"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Testing ssh-ing our remote server
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The following script performs checks similarly to the previous 2 backup scripts. It checks that both locations exist, but also attempts to verify that there is no other borg processes currently executing. The script also checks the health of the output borg repos on the remote server after the job is complete.&lt;/p>
&lt;p>Additionally, the rsync call also uses the -c flag to more securely and accurately detect changed files by calculating checksums to determine change. While slower, it ensures absolute data integrity which is a critical priority of this backup strategy.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Define variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LOCAL_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/mnt/bae&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REMOTE_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/mnt/bak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">PARTIAL_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;.rsync-partial&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LOG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/log/rsync-to-lil-dumpling.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REMOTE_USER&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;s1na&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REMOTE_HOST&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;lil-dumpling&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Function to send email via ntfy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">send_email&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">subject&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">message&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> curl &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Email: recipient.email.addr@gmail.com&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Tags: cd&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$subject&lt;/span>&lt;span class="s2"> &lt;/span>&lt;span class="nv">$message&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://192.168.1.76:10700/rsync-to-lil-dumpling
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Log function&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date &lt;span class="s1">&amp;#39;+%Y-%m-%d %H:%M:%S&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2"> - &lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt; /dev/null
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Start script&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Backup process started.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if backup directory exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> ! -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOCAL_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Backup directory &lt;/span>&lt;span class="nv">$LOCAL_DIR&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Rsync Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Backup directory &lt;/span>&lt;span class="nv">$LOCAL_DIR&lt;/span>&lt;span class="s2"> does not exist.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if backup directory exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> ! ssh -o &lt;span class="nv">BatchMode&lt;/span>&lt;span class="o">=&lt;/span>yes -o &lt;span class="nv">ConnectTimeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">10&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$REMOTE_USER&lt;/span>&lt;span class="s2">@&lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;[ -d \&amp;#34;&lt;/span>&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="s2">\&amp;#34; ]&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Remote directory &lt;/span>&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="s2"> does not exist on &lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Rsync Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Remote directory &lt;/span>&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="s2"> does not exist on &lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if partial directory exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> ! ssh -o &lt;span class="nv">BatchMode&lt;/span>&lt;span class="o">=&lt;/span>yes -o &lt;span class="nv">ConnectTimeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">10&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$REMOTE_USER&lt;/span>&lt;span class="s2">@&lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;[ -d \&amp;#34;&lt;/span>&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="s2">/&lt;/span>&lt;span class="nv">$PARTIAL_DIR&lt;/span>&lt;span class="s2">\&amp;#34; ]&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Remote partial directory &lt;/span>&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="s2">/&lt;/span>&lt;span class="nv">$PARTIAL_DIR&lt;/span>&lt;span class="s2"> does not exist on &lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Rsync Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Remote partial directory &lt;/span>&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="s2">/&lt;/span>&lt;span class="nv">$PARTIAL_DIR&lt;/span>&lt;span class="s2"> does not exist on &lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if any borg process is running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> ps aux &lt;span class="p">|&lt;/span> grep &lt;span class="s2">&amp;#34;[b]org&amp;#34;&lt;/span> &amp;gt; /dev/null&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;A Borg process is currently running. Aborting backup.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Rsync Backup Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Detected running borg process&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Rsync incremental backup&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Starting rsync backup...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> sudo rsync -avcz --partial --partial-dir&lt;span class="o">=&lt;/span>&lt;span class="nv">$PARTIAL_DIR&lt;/span> --progress -e &lt;span class="s2">&amp;#34;ssh -o BatchMode=yes -o ConnectTimeout=10&amp;#34;&lt;/span> &lt;span class="nv">$LOCAL_DIR&lt;/span> &lt;span class="nv">$REMOTE_USER&lt;/span>@&lt;span class="nv">$REMOTE_HOST&lt;/span>:&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Rsync backup completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Rsync backup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Rsync backup to lil-dumpling Failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Rsync backup failed.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verifying integrity of rsynced borg dirs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Checking Borg integrity on &lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Get list of subdirectories on remote&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">borg_dirs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>ssh &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$REMOTE_USER&lt;/span>&lt;span class="s2">@&lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;find &amp;#39;&lt;/span>&lt;span class="nv">$REMOTE_DIR&lt;/span>&lt;span class="s2">&amp;#39; -mindepth 1 -maxdepth 1 -type d&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Loop over each Borg repo dir&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> borg_dir in &lt;span class="nv">$borg_dirs&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Checking: &lt;/span>&lt;span class="nv">$borg_dir&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Run borg check remotely&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ssh &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$REMOTE_USER&lt;/span>&lt;span class="s2">@&lt;/span>&lt;span class="nv">$REMOTE_HOST&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;borg check --verify-data &amp;#39;&lt;/span>&lt;span class="nv">$borg_dir&lt;/span>&lt;span class="s2">&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">RC&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nv">$?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="nv">$RC&lt;/span> -eq &lt;span class="m">0&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Repo &lt;/span>&lt;span class="nv">$borg_dir&lt;/span>&lt;span class="s2"> valid&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="o">[&lt;/span> &lt;span class="nv">$RC&lt;/span> -eq &lt;span class="m">1&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Repo &lt;/span>&lt;span class="nv">$borg_dir&lt;/span>&lt;span class="s2"> valid with warnings&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Borg health checks&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Borg repo &lt;/span>&lt;span class="nv">$borg_dir&lt;/span>&lt;span class="s2"> valid but with warnings???&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> log &lt;span class="s2">&amp;#34;Repo &lt;/span>&lt;span class="nv">$borg_dir&lt;/span>&lt;span class="s2"> unhealthy&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> send_email &lt;span class="s2">&amp;#34;Borg health check failed&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Borg repo &lt;/span>&lt;span class="nv">$borg_dir&lt;/span>&lt;span class="s2"> unhealthy&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">exit&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">log &lt;span class="s2">&amp;#34;Rsync Backup process completed successfully.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">send_email &lt;span class="s2">&amp;#34;Rsync Backup Successful&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;Rsync Backup process completed successfully. See log for details: &lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exit&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># runs every Tuesday and Saturday of the month at 6:00 AM MST.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">0&lt;/span> &lt;span class="m">6&lt;/span> * * 2,5 /mnt/f15/desktop/backup-scripts/rsync_to_lil_dumpling.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="testing-of-backups">Testing of backups&lt;/h2>
&lt;p>Backups require periodic testing to ensure they actually work. Nothing is worse than finding out your backups are corrupted, malformed, improperly copied, or otherwise unuseable when push comes to shove and you actually suffer a tragic data loss incident.&lt;/p>
&lt;p>Simulating such an incident in our implementation is even simpler thanks to the use of docker containers which make spinning up and down applications and cleaning up applications incredibly easy and elegant.&lt;/p>
&lt;p>Testing the resiliency of our backup strategy is as simple as spinning down the self-hosted applications on our main prod application server, unmounting the primary harddisk, extracting the borg backup repos on the local secondary harddisk, and spinning up the containers using docker compose and then verifying the integrity and availability of application data. The following details these steps:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># spins down all running docker containers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker stop &lt;span class="k">$(&lt;/span>docker ps -q&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># unmounts the primary prod harddisk&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sudo umount /mnt/f15
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># extracts a backup copy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir /mnt/bae/docker-containers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">borg extract --progress --verbose /mnt/bae/borg-backup-dir::docker-backup-2025-06-19 /mnt/bae/docker-containers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># spins up the container from the extracted backup. A bash script could be &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># written for this step to iteratively spin up all backed up apps&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /mnt/bae/docker-containers/some-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#clean up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /mnt/bae/docker-containers/some-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose down
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rm -rf /mnt/bae/docker-containers
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Alternatively, similarly testing the remote offsite backups is also possible by simply extracting the borg backups on the remote server locally and spinning them up and comparing with the prod application counterparts without actually having to spin down the prod applications themselves:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># On the remote offsite server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir /mnt/remote/docker-containers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">borg extract --progress --verbose /mnt/bak/borg-backup-dir::docker-backup-2025-06-19 /mnt/remote/docker-containers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># spin up the container from the extracted backup. A bash script could be &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># written for this step to iteratively spin up all backed up apps&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /mnt/remote/docker-containers/some-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#clean up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /mnt/remote/docker-containers/some-app
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose down
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rm -rf /mnt/remote/docker-containers
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Defcon 33 Afterthoughts</title><link>https://averytan.com/post/defcon33-afterthoughts/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><guid>https://averytan.com/post/defcon33-afterthoughts/</guid><description>&lt;p>As August rolled in, Las Vegas once again became the global gathering place for hackers, researchers, and curious minds. Defcon 33 kicked off just as Black Hat wrapped up, bringing together thousands of attendees for a whirlwind of talks, contests, and parties.&lt;/p>
&lt;p>This year marked my fourth Defcon — my first being Defcon 27 in 2019.&lt;/p>
&lt;h3 id="humble-beginnings">Humble Beginnings&lt;/h3>
&lt;p>I can’t quite remember how I first stumbled onto Defcon, but I do know it traces back to Jack Rhysider’s Darknet Diaries podcast sometime in 2019. At the time, I was a fresh graduate returning once again to central British Columbia for what would be my final season of tree planting — my third season of reforestation work.&lt;/p>
&lt;p>
&lt;figure id="figure-a-view-of-the-planting-block-with-my-bags-and-shovel-in-foreground">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="treeplanting" srcset="
/post/defcon33-afterthoughts/treeplanting-2019_hu_6fda801c885b9400.webp 400w,
/post/defcon33-afterthoughts/treeplanting-2019_hu_6caf4e7b5389c8bc.webp 760w,
/post/defcon33-afterthoughts/treeplanting-2019_hu_394a7ed8780d4d8d.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/treeplanting-2019_hu_6fda801c885b9400.webp"
width="760"
height="719"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
A view of the planting &amp;lsquo;block&amp;rsquo; with my bags and shovel in foreground
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>I distinctly remember listening to Darknet Diaries episodes during long drives out to the planting blocks. Tales of espionage and sabotage, physical pentesters infiltrating banks and corporate offices filled these long days — and Defcon kept being mentioned. I had taken CMPUT 333: Security in a Networked World during my undergrad so I had some very crude rudimentary knowledge of hashes, ciphers, and cryptography, but nothing close to preparing me for what I would see at my first Defcon.&lt;/p>
&lt;p>That first Defcon experience was pretty unforgettable. I shared a room at the LINQ with two other attendees. These were the times before corporate sponsorship would cover and reimburse my travel expenses. The time where Walmart baloney sandwiches in ziplock bags made the bulk of my Defcon diet. Circumstances were a lot more no-frills for me during that first Defcon.&lt;/p>
&lt;p>Nonetheless it was an eye-opening affair, one that I do credit being the significant event that marked the historical boundary signaling the start of the &amp;lsquo;cybersecurity era&amp;rsquo; for me.&lt;/p>
&lt;p>
&lt;figure id="figure-when-three-or-more-hack-for-satan-badges-are-in-close-proximity-lights-flicker-in-a-seance-of-satanic-goodness">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="seance" srcset="
/post/defcon33-afterthoughts/seance_hu_6a20079c45000fc5.webp 400w,
/post/defcon33-afterthoughts/seance_hu_1d2487918ce8f8e6.webp 760w,
/post/defcon33-afterthoughts/seance_hu_6622312ca3baa26a.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/seance_hu_6a20079c45000fc5.webp"
width="570"
height="760"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
When three or more hack for satan badges are in close proximity, lights flicker in a seance of satanic goodness
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-sneaking-into-the-florida-man-party-with-fake-badges-in-a-testament-to-our-peak-social-engineering-skills">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="florida man social engineering" srcset="
/post/defcon33-afterthoughts/florida-man-social-engineering_hu_7b3e933404d23fbc.webp 400w,
/post/defcon33-afterthoughts/florida-man-social-engineering_hu_46eb77b442cd75a8.webp 760w,
/post/defcon33-afterthoughts/florida-man-social-engineering_hu_bd6dcef05a38b7b4.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/florida-man-social-engineering_hu_7b3e933404d23fbc.webp"
width="760"
height="570"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Sneaking into the florida man party with fake badges in a testament to our peak social engineering skills
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>In between wandering from village to village and being bombarded with things that looked and sounded like magic, I ended up one evening in some suite at Bally’s for the CONadian party allong with my two attendee roomates. In between drinks at their open bar, a key conversation that night yielded me the start of my personal &amp;lsquo;security roadmap&amp;rsquo;.&lt;/p>
&lt;p>
&lt;figure id="figure-my-original-cyber-roadmap">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="The original plan" srcset="
/post/defcon33-afterthoughts/original-plan_hu_abc7bc7088c875b5.webp 400w,
/post/defcon33-afterthoughts/original-plan_hu_f2487b0ba58846eb.webp 760w,
/post/defcon33-afterthoughts/original-plan_hu_ce1636e0b56a9499.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/original-plan_hu_abc7bc7088c875b5.webp"
width="535"
height="760"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
My original cyber roadmap
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>While I never did end up following this original roadmap I set myself all those years ago, I would say Defcon 27 definitely provided me the insight and a trajectory to penetrate the world of hacking. As a result, I&amp;rsquo;ve always felt a sense of gratitude and high reverance for what Defcon means to me, that lifechanging historical boundary that marked my entrypoint into the world of infosec, and so it only makes sense that I return for Defcon 33!&lt;/p>
&lt;h3 id="defcon-33">DEFCON 33&lt;/h3>
&lt;p>Defcon 33 was held again this year all &amp;ldquo;under one roof&amp;rdquo; at the Las Vegas Convention Centre from August 7 to August 10 2025. I lined up for merch about an hour before things opened and was incredibly pleasantly surprised I was able to essential be one of the first 100 people in line! What a surprise and subversion of expectations, especially seeing how I was in line for 4+ hours for merch the last time I was at Defcon.&lt;/p>
&lt;p>Unsurprisingly, AI was centre-stage this year. The AIxCC contest continued to expand in size and real estate. This was a contest sponsored by DARPA going into its second year where contestants deploy AI systems that can detect, exploit, and patch vulnerabilities in systems mimicking industrial infrastructure. The activity at other villages were still ever-present, but AI and LLM tooling and discussions dominated the narrative.&lt;/p>
&lt;p>
&lt;figure id="figure-openai-execs-on-the-aixcc-stage">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="OpenAI execs on the AIxCC stage" srcset="
/post/defcon33-afterthoughts/aixcc-openai_hu_a41bff2c0b9c7085.webp 400w,
/post/defcon33-afterthoughts/aixcc-openai_hu_c603475967de5a38.webp 760w,
/post/defcon33-afterthoughts/aixcc-openai_hu_b2d07f9462d47a28.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/aixcc-openai_hu_a41bff2c0b9c7085.webp"
width="760"
height="573"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
OpenAI execs on the AIxCC stage
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>A key theme was the use of AI tools, particularly LLMs, and how they democratizing offensive security in ways that outpace their current defensive applications. Hackers demonstrated
, and the AIxCC contest itself was a strong showcase of this particular trend. I don&amp;rsquo;t believe this particular emerging trend is a surprise to really anyone that actually uses LLMs in their day-to-day, even for minor troubleshooting or debugging/coding purposes. An obvious consequence of this democratization is that it lowers the barrier of entry for offensive security, both for aspiring whitehacks as well as for cybercriminals, and this trend is already readily showing up in
!&lt;/p>
&lt;p>
&lt;figure id="figure-tricking-an-llm-into-helping-with-offensive-security-activites">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="LLM helps with hacking" srcset="
/post/defcon33-afterthoughts/LLM-helps-with-hacking_hu_a447db76ebec02ef.webp 400w,
/post/defcon33-afterthoughts/LLM-helps-with-hacking_hu_d025226c41d36510.webp 760w,
/post/defcon33-afterthoughts/LLM-helps-with-hacking_hu_8de2f4e636e1029f.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/LLM-helps-with-hacking_hu_a447db76ebec02ef.webp"
width="573"
height="760"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Tricking an LLM into helping with offensive security activites
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>I also managed to snag a signed copy of &amp;lsquo;Red Teaming AI&amp;rsquo; which ended up selling out on the first day of the convention! Definitely the loot highlight of the con for me.&lt;/p>
&lt;p>
&lt;figure id="figure-prized-loot">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Redteaming AI" srcset="
/post/defcon33-afterthoughts/redteaming-ai_hu_4e87fb70457e475.webp 400w,
/post/defcon33-afterthoughts/redteaming-ai_hu_8f372b36810b6716.webp 760w,
/post/defcon33-afterthoughts/redteaming-ai_hu_b6fa18358503dbd9.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/redteaming-ai_hu_4e87fb70457e475.webp"
width="760"
height="561"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Prized loot
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Another highlight for me was the Deepfake demo held at the AI Village. This demo was running the opensource DeepFaceLab and via a personal consumer-grade laptop sporting a gtx 4090. For the cost of a mid range gaming laptop, anyone can create deep fakes. Deep fakes for all!!&lt;/p>
&lt;p>
&lt;figure id="figure-hardware-running-the-deepfake-demo">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Deepfake hardware" srcset="
/post/defcon33-afterthoughts/deepfake-hardware_hu_c28ef2da80e096b4.webp 400w,
/post/defcon33-afterthoughts/deepfake-hardware_hu_2cade6e5fff19fad.webp 760w,
/post/defcon33-afterthoughts/deepfake-hardware_hu_7969325cecf1051d.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/deepfake-hardware_hu_c28ef2da80e096b4.webp"
width="573"
height="760"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Hardware running the deepfake demo
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-look-mom-im-keanu-reeves">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Deepfake demo"
src="https://averytan.com/post/defcon33-afterthoughts/deepfake.gif"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Look Mom, I&amp;rsquo;m Keanu Reeves!
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Policy discussions were also dominated by AI, particularly its broad and disruptive impact across domains like application security, privacy, and national security. While the tools evolve rapidly, policymakers are still catching up; tale as old as time.&lt;/p>
&lt;p>Other notable highlights were keynotes by the likes of Microsoft, Anthropic, and OpenAI. And stickers, lots of stickers.&lt;/p>
&lt;p>
&lt;figure id="figure-sticker-monster-mode">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="stickers" srcset="
/post/defcon33-afterthoughts/stickers_hu_7fa4d732c2e64fc0.webp 400w,
/post/defcon33-afterthoughts/stickers_hu_d265b4c8312fc9e.webp 760w,
/post/defcon33-afterthoughts/stickers_hu_6f050af2ed5e8328.webp 1200w"
src="https://averytan.com/post/defcon33-afterthoughts/stickers_hu_7fa4d732c2e64fc0.webp"
width="760"
height="573"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
sticker monster mode
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;h3 id="closing-thoughts">Closing Thoughts&lt;/h3>
&lt;p>Defcon creates this aura and feeling of being enveloped and surrounded in the bleeding edge frontier of cybersecurity. The long-running legend now ingrained in Vegas and Defcon myth of having the &amp;lsquo;most hostile wifi network on earth&amp;rsquo; persists and I&amp;rsquo;ve had a few baristas and &amp;lsquo;civies&amp;rsquo;, seeing my attendee badge and then remarking, &amp;lsquo;so, I guess I shouldn&amp;rsquo;t connect to any wifi networks for the rest of the week right?&amp;rsquo;.&lt;/p>
&lt;p>Myths and traditions aside, I had a blast at hacker summer camp this year, and am forever grateful that this event persists year after year, ever evolving with the times, forming a place where the curious and the brightest meet and discuss the latest and greatest.&lt;/p></description></item><item><title>Advantage Blackjack: Analysis with Simulation and Reinforcement Learning Methods</title><link>https://averytan.com/project/blackjack/</link><pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate><guid>https://averytan.com/project/blackjack/</guid><description>&lt;h1 id="a-figure-from-your-pages-folder-or-your-assetsmedia-media-library">A figure from your page’s folder or your assets/media/ media library&lt;/h1>
&lt;p>
&lt;figure id="figure-caption">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img src="blackjack.jpg" alt="screen reader text" loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
caption
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>A work in progress!&lt;/p>
&lt;p>Code generated by Claude Opus&lt;/p></description></item><item><title>Projects</title><link>https://averytan.com/projects/</link><pubDate>Sun, 19 May 2024 00:00:00 +0000</pubDate><guid>https://averytan.com/projects/</guid><description/></item><item><title>🎉 Easily create your own simple yet highly customizable blog</title><link>https://averytan.com/postcopy/get-started/</link><pubDate>Fri, 27 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/postcopy/get-started/</guid><description>&lt;p>Welcome 👋&lt;/p>
&lt;details class="print:hidden xl:hidden" open>
&lt;summary>Table of Contents&lt;/summary>
&lt;div class="text-sm">
&lt;nav id="TableOfContents">
&lt;ul>
&lt;li>&lt;a href="#overview">Overview&lt;/a>
&lt;ul>
&lt;li>&lt;a href="#get-started">Get Started&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a href="#crowd-funded-open-source-software">Crowd-funded open-source software&lt;/a>
&lt;ul>
&lt;li>&lt;a href="#-click-here-to-become-a-sponsor-and-help-support-hugo-blox">&lt;a href="https://hugoblox.com/sponsor/">❤️ Click here to become a sponsor and help support Hugo Blox&amp;rsquo;s future ❤️&lt;/a>&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;a href="#ecosystem">Ecosystem&lt;/a>&lt;/li>
&lt;li>&lt;a href="#inspiration">Inspiration&lt;/a>&lt;/li>
&lt;li>&lt;a href="#features">Features&lt;/a>&lt;/li>
&lt;li>&lt;a href="#themes">Themes&lt;/a>&lt;/li>
&lt;li>&lt;a href="#license">License&lt;/a>&lt;/li>
&lt;/ul>
&lt;/nav>
&lt;/div>
&lt;/details>
&lt;h2 id="overview">Overview&lt;/h2>
&lt;ol>
&lt;li>The Hugo Blox website builder for Hugo, along with its starter templates, is designed for professional creators, educators, and teams/organizations - although it can be used to create any kind of site&lt;/li>
&lt;li>The template can be modified and customised to suit your needs. It&amp;rsquo;s a good platform for anyone looking to take control of their data and online identity whilst having the convenience to start off with a &lt;strong>no-code solution (write in Markdown and customize with YAML parameters)&lt;/strong> and having &lt;strong>flexibility to later add even deeper personalization with HTML and CSS&lt;/strong>&lt;/li>
&lt;li>You can work with all your favourite tools and apps with hundreds of plugins and integrations to speed up your workflows, interact with your readers, and much more&lt;/li>
&lt;/ol>
&lt;h3 id="get-started">Get Started&lt;/h3>
&lt;ul>
&lt;li>👉
&lt;/li>
&lt;li>📚
&lt;/li>
&lt;li>💬
or
&lt;/li>
&lt;li>🐦 Twitter:
#MadeWithHugoBlox&lt;/li>
&lt;li>💡
&lt;/li>
&lt;li>⬆️ &lt;strong>Updating Hugo Blox?&lt;/strong> View the
and
&lt;/li>
&lt;/ul>
&lt;h2 id="crowd-funded-open-source-software">Crowd-funded open-source software&lt;/h2>
&lt;p>To help us develop this template and software sustainably under the MIT license, we ask all individuals and businesses that use it to help support its ongoing maintenance and development via sponsorship.&lt;/p>
&lt;h3 id="-click-here-to-become-a-sponsor-and-help-support-hugo-blox">
&lt;/h3>
&lt;p>As a token of appreciation for sponsoring, you can &lt;strong>unlock
awesome rewards and extra features 🦄✨&lt;/strong>&lt;/p>
&lt;h2 id="ecosystem">Ecosystem&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>
:&lt;/strong> Automatically import publications from BibTeX&lt;/li>
&lt;/ul>
&lt;h2 id="inspiration">Inspiration&lt;/h2>
&lt;p>
are building with this template.&lt;/p>
&lt;h2 id="features">Features&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Page builder&lt;/strong> - Create &lt;em>anything&lt;/em> with no-code
and
&lt;/li>
&lt;li>&lt;strong>Edit any type of content&lt;/strong> - Blog posts, publications, talks, slides, projects, and more!&lt;/li>
&lt;li>&lt;strong>Create content&lt;/strong> in
,
, or
&lt;/li>
&lt;li>&lt;strong>Plugin System&lt;/strong> - Fully customizable
&lt;/li>
&lt;li>&lt;strong>Display Code and Math&lt;/strong> - Code syntax highlighting and LaTeX math supported&lt;/li>
&lt;li>&lt;strong>Integrations&lt;/strong> -
,
, Maps, Contact Forms, and more!&lt;/li>
&lt;li>&lt;strong>Beautiful Site&lt;/strong> - Simple and refreshing one-page design&lt;/li>
&lt;li>&lt;strong>Industry-Leading SEO&lt;/strong> - Help get your website found on search engines and social media&lt;/li>
&lt;li>&lt;strong>Media Galleries&lt;/strong> - Display your images and videos with captions in a customizable gallery&lt;/li>
&lt;li>&lt;strong>Mobile Friendly&lt;/strong> - Look amazing on every screen with a mobile friendly version of your site&lt;/li>
&lt;li>&lt;strong>Multi-language&lt;/strong> - 35+ language packs including English, 中文, and Português&lt;/li>
&lt;li>&lt;strong>Multi-user&lt;/strong> - Each author gets their own profile page&lt;/li>
&lt;li>&lt;strong>Privacy Pack&lt;/strong> - Assists with GDPR&lt;/li>
&lt;li>&lt;strong>Stand Out&lt;/strong> - Bring your site to life with animation, parallax backgrounds, and scroll effects&lt;/li>
&lt;li>&lt;strong>One-Click Deployment&lt;/strong> - No servers. No databases. Only files.&lt;/li>
&lt;/ul>
&lt;h2 id="themes">Themes&lt;/h2>
&lt;p>Hugo Blox and its templates come with &lt;strong>automatic day (light) and night (dark) mode&lt;/strong> built-in. Visitors can choose their preferred mode by clicking the sun/moon icon in the header.&lt;/p>
&lt;p>
for your site. Themes are fully customizable.&lt;/p>
&lt;h2 id="license">License&lt;/h2>
&lt;p>Copyright 2016-present
.&lt;/p>
&lt;p>Released under the
license.&lt;/p></description></item><item><title>🧠 Sharpen your thinking with a second brain</title><link>https://averytan.com/postcopy/second-brain/</link><pubDate>Thu, 26 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/postcopy/second-brain/</guid><description>&lt;p>Create a personal knowledge base and share your knowledge with your peers.&lt;/p>
&lt;p>Hugo Blox web framework empowers you with one of the most flexible note-taking capabilities out there.&lt;/p>
&lt;p>Create a powerful knowledge base that works on top of a local folder of plain text Markdown files.&lt;/p>
&lt;p>Use it as your second brain, either publicly sharing your knowledge with your peers via your website, or via a private GitHub repository and password-protected site just for yourself.&lt;/p>
&lt;h2 id="mindmaps">Mindmaps&lt;/h2>
&lt;p>Hugo Blox supports a Markdown extension for mindmaps.&lt;/p>
&lt;p>With this open format, can even edit your mindmaps in other popular tools such as Obsidian.&lt;/p>
&lt;p>Simply insert a Markdown code block labelled as &lt;code>markmap&lt;/code> and optionally set the height of the mindmap as shown in the example below.&lt;/p>
&lt;p>Mindmaps can be created by simply writing the items as a Markdown list within the &lt;code>markmap&lt;/code> code block, indenting each item to create as many sub-levels as you need:&lt;/p>
&lt;div class="highlight">
&lt;pre class="chroma">
&lt;code>
```markmap {height="200px"}
- Hugo Modules
- Hugo Blox
- blox-plugins-netlify
- blox-plugins-netlify-cms
- blox-plugins-reveal
```
&lt;/code>
&lt;/pre>
&lt;/div>
&lt;p>renders as&lt;/p>
&lt;div class="markmap" style="height: 200px;">
&lt;pre>- Hugo Modules
- Hugo Blox
- blox-plugins-netlify
- blox-plugins-netlify-cms
- blox-plugins-reveal&lt;/pre>
&lt;/div>
&lt;p>Anh here&amp;rsquo;s a more advanced mindmap with formatting, code blocks, and math:&lt;/p>
&lt;div class="highlight">
&lt;pre class="chroma">
&lt;code>
```markmap
- Mindmaps
- Links
- [Hugo Blox Docs](https://docs.hugoblox.com/)
- [Discord Community](https://discord.gg/z8wNYzb)
- [GitHub](https://github.com/HugoBlox/hugo-blox-builder)
- Features
- Markdown formatting
- **inline** ~~text~~ *styles*
- multiline
text
- `inline code`
-
```js
console.log('hello');
console.log('code block');
```
- Math: $x = {-b \pm \sqrt{b^2-4ac} \over 2a}$
```
&lt;/code>
&lt;/pre>
&lt;/div>
&lt;p>renders as&lt;/p>
&lt;div class="markmap" style="height: 500px;">
&lt;pre>- Mindmaps
- Links
- [Hugo Blox Docs](https://docs.hugoblox.com/)
- [Discord Community](https://discord.gg/z8wNYzb)
- [GitHub](https://github.com/HugoBlox/hugo-blox-builder)
- Features
- Markdown formatting
- **inline** ~~text~~ *styles*
- multiline
text
- `inline code`
-
```js
console.log('hello');
console.log('code block');
```
- Math: $x = {-b \pm \sqrt{b^2-4ac} \over 2a}$&lt;/pre>
&lt;/div>
&lt;h2 id="highlighting">Highlighting&lt;/h2>
&lt;p>&lt;mark>Highlight&lt;/mark> important text with &lt;code>mark&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">mark&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Highlighted text&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">mark&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="callouts">Callouts&lt;/h2>
&lt;p>Use
(aka &lt;em>asides&lt;/em>, &lt;em>hints&lt;/em>, or &lt;em>alerts&lt;/em>) to draw attention to notes, tips, and warnings.&lt;/p>
&lt;p>By wrapping a paragraph in &lt;code>{{% callout note %}} ... {{% /callout %}}&lt;/code>, it will render as an aside.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">{{% callout note %}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">A Markdown aside is useful for displaying notices, hints, or definitions to your readers.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{% /callout %}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">A Markdown aside is useful for displaying notices, hints, or definitions to your readers.&lt;/span>
&lt;/div>
&lt;p>Or use the &lt;code>warning&lt;/code> callout type so your readers don&amp;rsquo;t miss critical details:&lt;/p>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-yellow-100 dark:bg-yellow-900">
&lt;span class="pr-3 pt-1 text-red-400">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0zM12 15.75h.007v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">A Markdown aside is useful for displaying notices, hints, or definitions to your readers.&lt;/span>
&lt;/div>
&lt;h2 id="did-you-find-this-page-helpful-consider-sharing-it-">Did you find this page helpful? Consider sharing it 🙌&lt;/h2></description></item><item><title>Communicate your results effectively with the best data visualizations</title><link>https://averytan.com/postcopy/data-visualization/</link><pubDate>Wed, 25 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/postcopy/data-visualization/</guid><description>&lt;p>Hugo Blox is designed to give technical content creators a seamless experience. You can focus on the content and Hugo Blox handles the rest.&lt;/p>
&lt;p>Use popular tools such as Plotly, Mermaid, and data frames.&lt;/p>
&lt;h2 id="charts">Charts&lt;/h2>
&lt;p>Hugo Blox supports the popular
format for interactive data visualizations. With Plotly, you can design almost any kind of visualization you can imagine!&lt;/p>
&lt;p>Save your Plotly JSON in your page folder, for example &lt;code>line-chart.json&lt;/code>, and then add the &lt;code>{{&amp;lt; chart data=&amp;quot;line-chart&amp;quot; &amp;gt;}}&lt;/code> shortcode where you would like the chart to appear.&lt;/p>
&lt;p>Demo:&lt;/p>
&lt;div id="chart-587694312" class="chart">&lt;/div>
&lt;script>
async function fetchChartJSON() {
console.debug('Hugo Blox fetching chart JSON...')
const response = await fetch('.\/line-chart.json');
return await response.json();
}
(function() {
let a = setInterval( function() {
if ( typeof window.Plotly === 'undefined' ) {
console.debug('Plotly not loaded yet...')
return;
}
clearInterval( a );
fetchChartJSON().then(chart => {
console.debug('Plotting chart...')
window.Plotly.newPlot('chart-587694312', chart.data, chart.layout, {responsive: true});
});
}, 500 );
})();
&lt;/script>
&lt;p>You might also find the
useful.&lt;/p>
&lt;h2 id="diagrams">Diagrams&lt;/h2>
&lt;p>Hugo Blox supports the &lt;em>Mermaid&lt;/em> Markdown extension for diagrams.&lt;/p>
&lt;p>An example &lt;strong>flowchart&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>```mermaid
graph TD
A[Hard] --&amp;gt;|Text| B(Round)
B --&amp;gt; C{Decision}
C --&amp;gt;|One| D[Result 1]
C --&amp;gt;|Two| E[Result 2]
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="mermaid">graph TD
A[Hard] -->|Text| B(Round)
B --> C{Decision}
C -->|One| D[Result 1]
C -->|Two| E[Result 2]
&lt;/div>
&lt;p>An example &lt;strong>sequence diagram&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>```mermaid
sequenceDiagram
Alice-&amp;gt;&amp;gt;John: Hello John, how are you?
loop Healthcheck
John-&amp;gt;&amp;gt;John: Fight against hypochondria
end
Note right of John: Rational thoughts!
John--&amp;gt;&amp;gt;Alice: Great!
John-&amp;gt;&amp;gt;Bob: How about you?
Bob--&amp;gt;&amp;gt;John: Jolly good!
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="mermaid">sequenceDiagram
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
&lt;/div>
&lt;p>An example &lt;strong>class diagram&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>```mermaid
classDiagram
Class01 &amp;lt;|-- AveryLongClass : Cool
Class03 *-- Class04
Class05 o-- Class06
Class07 .. Class08
Class09 --&amp;gt; C2 : Where am i?
Class09 --* C3
Class09 --|&amp;gt; Class07
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 &amp;lt;--&amp;gt; C2: Cool label
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="mermaid">classDiagram
Class01 &lt;|-- AveryLongClass : Cool
Class03 *-- Class04
Class05 o-- Class06
Class07 .. Class08
Class09 --> C2 : Where am i?
Class09 --* C3
Class09 --|> Class07
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 &lt;--> C2: Cool label
&lt;/div>
&lt;p>An example &lt;strong>state diagram&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>```mermaid
stateDiagram
[*] --&amp;gt; Still
Still --&amp;gt; [*]
Still --&amp;gt; Moving
Moving --&amp;gt; Still
Moving --&amp;gt; Crash
Crash --&amp;gt; [*]
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="mermaid">stateDiagram
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
&lt;/div>
&lt;h2 id="data-frames">Data Frames&lt;/h2>
&lt;p>Save your spreadsheet as a CSV file in your page&amp;rsquo;s folder and then render it by adding the &lt;em>Table&lt;/em> shortcode to your page:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&amp;lt;&lt;/span> &lt;span class="nx">table&lt;/span> &lt;span class="nx">path&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="s">&amp;#34;results.csv&amp;#34;&lt;/span> &lt;span class="nx">header&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="s">&amp;#34;true&amp;#34;&lt;/span> &lt;span class="nx">caption&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="s">&amp;#34;Table 1: My results&amp;#34;&lt;/span> &lt;span class="p">&amp;gt;}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;table class="table-auto w-full">
&lt;thead>
&lt;tr> &lt;th class="border-b dark:border-slate-600 font-medium p-4 pt-0 pb-3 text-slate-400 dark:text-slate-200 text-left">customer_id&lt;/th> &lt;th class="border-b dark:border-slate-600 font-medium p-4 pt-0 pb-3 text-slate-400 dark:text-slate-200 text-left">score&lt;/th> &lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td data-table-dtype="number" class="border-b border-slate-100 dark:border-slate-700 p-4 text-slate-500 dark:text-slate-400">1&lt;/td>
&lt;td data-table-dtype="number" class="border-b border-slate-100 dark:border-slate-700 p-4 text-slate-500 dark:text-slate-400">0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td data-table-dtype="number" class="border-b border-slate-100 dark:border-slate-700 p-4 text-slate-500 dark:text-slate-400">2&lt;/td>
&lt;td data-table-dtype="text" class="border-b border-slate-100 dark:border-slate-700 p-4 text-slate-500 dark:text-slate-400">0.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td data-table-dtype="number" class="border-b border-slate-100 dark:border-slate-700 p-4 text-slate-500 dark:text-slate-400">3&lt;/td>
&lt;td data-table-dtype="number" class="border-b border-slate-100 dark:border-slate-700 p-4 text-slate-500 dark:text-slate-400">1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;caption class="table-caption">Table 1: My results&lt;/caption>
&lt;/table>
&lt;h2 id="did-you-find-this-page-helpful-consider-sharing-it-">Did you find this page helpful? Consider sharing it 🙌&lt;/h2></description></item><item><title>👩🏼‍🏫 Teach academic courses</title><link>https://averytan.com/postcopy/teach-courses/</link><pubDate>Tue, 24 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/postcopy/teach-courses/</guid><description>&lt;p>
is designed to give technical content creators a seamless experience. You can focus on the content and the Hugo Blox Builder which this template is built upon handles the rest.&lt;/p>
&lt;p>&lt;strong>Embed videos, podcasts, code, LaTeX math, and even test students!&lt;/strong>&lt;/p>
&lt;p>On this page, you&amp;rsquo;ll find some examples of the types of technical content that can be rendered with Hugo Blox.&lt;/p>
&lt;h2 id="video">Video&lt;/h2>
&lt;p>Teach your course by sharing videos with your students. Choose from one of the following approaches:&lt;/p>
&lt;p>&lt;strong>Youtube&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; youtube D2vj0WcvH5c &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/D2vj0WcvH5c?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video">&lt;/iframe>
&lt;/div>
&lt;p>&lt;strong>Bilibili&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; bilibili BV1WV4y1r7DF &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;div class="w-full h-auto aspect-video relative">
&lt;iframe src="//player.bilibili.com/player.html?bvid=BV1WV4y1r7DF&amp;page=1"
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; fullscreen; picture-in-picture;"
class="w-full h-full"
>&lt;/iframe>
&lt;/div>
&lt;p>&lt;strong>Video file&lt;/strong>&lt;/p>
&lt;p>Videos may be added to a page by either placing them in your &lt;code>assets/media/&lt;/code> media library or in your
, and then embedding them with the &lt;em>video&lt;/em> shortcode:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; video src=&amp;quot;my_video.mp4&amp;quot; controls=&amp;quot;yes&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;h2 id="podcast">Podcast&lt;/h2>
&lt;p>You can add a podcast or music to a page by placing the MP3 file in the page&amp;rsquo;s folder or the media library folder and then embedding the audio on your page with the &lt;em>audio&lt;/em> shortcode:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; audio src=&amp;quot;ambient-piano.mp3&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;p>Try it out:&lt;/p>
&lt;audio controls >
&lt;source src="https://averytan.com/postcopy/teach-courses/ambient-piano.mp3" type="audio/mpeg">
&lt;/audio>
&lt;h2 id="test-students">Test students&lt;/h2>
&lt;p>Provide a simple yet fun self-assessment by revealing the solutions to challenges with the &lt;code>spoiler&lt;/code> shortcode:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">{{&lt;span class="p">&amp;lt;&lt;/span> &lt;span class="nt">spoiler&lt;/span> &lt;span class="na">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;👉 Click to view the solution&amp;#34;&lt;/span> &lt;span class="p">&amp;gt;&lt;/span>}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">You found me!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{&lt;span class="p">&amp;lt;&lt;/span> &lt;span class="p">/&lt;/span>&lt;span class="nt">spoiler&lt;/span> &lt;span class="p">&amp;gt;&lt;/span>}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;details class="spoiler " id="spoiler-3">
&lt;summary class="cursor-pointer">👉 Click to view the solution&lt;/summary>
&lt;div class="rounded-lg bg-neutral-50 dark:bg-neutral-800 p-2">
You found me 🎉
&lt;/div>
&lt;/details>
&lt;h2 id="math">Math&lt;/h2>
&lt;p>Hugo Blox Builder supports a Markdown extension for $\LaTeX$ math. Enable math by setting the &lt;code>math: true&lt;/code> option in your page&amp;rsquo;s front matter, or enable math for your entire site by toggling math in your &lt;code>config/_default/params.yaml&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">features&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">math&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To render &lt;em>inline&lt;/em> or &lt;em>block&lt;/em> math, wrap your LaTeX math with &lt;code>$...$&lt;/code> or &lt;code>$$...$$&lt;/code>, respectively.&lt;/p>
&lt;p>Example &lt;strong>math block&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-latex" data-lang="latex">&lt;span class="line">&lt;span class="cl">&lt;span class="sb">$$&lt;/span>&lt;span class="nb">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="nv">\gamma&lt;/span>&lt;span class="nb">_{n} &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\frac&lt;/span>&lt;span class="nb">{ &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> | &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n} &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">} &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb">^T &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> |}{&lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\|\nabla&lt;/span>&lt;span class="nb"> F&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb">{x}_{n}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb">{x}_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\|&lt;/span>&lt;span class="nb">^&lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="nb">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="s">$$&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
$$\gamma_{n} = \frac{ \left | \left (\mathbf x_{n} - \mathbf x_{n-1} \right )^T \left [\nabla F (\mathbf x_{n}) - \nabla F (\mathbf x_{n-1}) \right ] \right |}{\left \|\nabla F(\mathbf{x}_{n}) - \nabla F(\mathbf{x}_{n-1}) \right \|^2}$$&lt;p>Example &lt;strong>inline math&lt;/strong> &lt;code>$\nabla F(\mathbf{x}_{n})$&lt;/code> renders as $\nabla F(\mathbf{x}_{n})$.&lt;/p>
&lt;p>Example &lt;strong>multi-line math&lt;/strong> using the math linebreak (&lt;code>\\&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-latex" data-lang="latex">&lt;span class="line">&lt;span class="cl">&lt;span class="sb">$$&lt;/span>&lt;span class="nb">f&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nb">k;p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\begin&lt;/span>&lt;span class="nb">{cases}p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">} &amp;amp; &lt;/span>&lt;span class="nv">\text&lt;/span>&lt;span class="nb">{if }k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">, &lt;/span>&lt;span class="nv">\\&lt;/span>&lt;span class="nb">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb">p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">} &amp;amp; &lt;/span>&lt;span class="nv">\text&lt;/span>&lt;span class="nb">{if }k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">.&lt;/span>&lt;span class="nv">\end&lt;/span>&lt;span class="nb">{cases}&lt;/span>&lt;span class="s">$$&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
$$
f(k;p_{0}^{*}) = \begin{cases}p_{0}^{*} &amp; \text{if }k=1, \\
1-p_{0}^{*} &amp; \text{if }k=0.\end{cases}
$$&lt;h2 id="code">Code&lt;/h2>
&lt;p>Hugo Blox Builder utilises Hugo&amp;rsquo;s Markdown extension for highlighting code syntax. The code theme can be selected in the &lt;code>config/_default/params.yaml&lt;/code> file.&lt;/p>
&lt;pre>&lt;code>```python
import pandas as pd
data = pd.read_csv(&amp;quot;data.csv&amp;quot;)
data.head()
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pandas&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">pd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;data.csv&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">head&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="inline-images">Inline Images&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&amp;lt;&lt;/span> &lt;span class="nx">icon&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="s">&amp;#34;python&amp;#34;&lt;/span> &lt;span class="p">&amp;gt;}}&lt;/span> &lt;span class="nx">Python&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;p>
&lt;span class="inline-block pr-1">
&lt;svg style="height: 1em; transform: translateY(0.1em);" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512" fill="currentColor">&lt;path d="M439.8 200.5c-7.7-30.9-22.3-54.2-53.4-54.2h-40.1v47.4c0 36.8-31.2 67.8-66.8 67.8H172.7c-29.2 0-53.4 25-53.4 54.3v101.8c0 29 25.2 46 53.4 54.3 33.8 9.9 66.3 11.7 106.8 0 26.9-7.8 53.4-23.5 53.4-54.3v-40.7H226.2v-13.6h160.2c31.1 0 42.6-21.7 53.4-54.2 11.2-33.5 10.7-65.7 0-108.6zM286.2 404c11.1 0 20.1 9.1 20.1 20.3 0 11.3-9 20.4-20.1 20.4-11 0-20.1-9.2-20.1-20.4.1-11.3 9.1-20.3 20.1-20.3zM167.8 248.1h106.8c29.7 0 53.4-24.5 53.4-54.3V91.9c0-29-24.4-50.7-53.4-55.6-35.8-5.9-74.7-5.6-106.8.1-45.2 8-53.4 24.7-53.4 55.6v40.7h106.9v13.6h-147c-31.1 0-58.3 18.7-66.8 54.2-9.8 40.7-10.2 66.1 0 108.6 7.6 31.6 25.7 54.2 56.8 54.2H101v-48.8c0-35.3 30.5-66.4 66.8-66.4zm-6.7-142.6c-11.1 0-20.1-9.1-20.1-20.3.1-11.3 9-20.4 20.1-20.4 11 0 20.1 9.2 20.1 20.4s-9 20.3-20.1 20.3z"/>&lt;/svg>
&lt;/span> Python&lt;/p>
&lt;h2 id="did-you-find-this-page-helpful-consider-sharing-it-">Did you find this page helpful? Consider sharing it 🙌&lt;/h2></description></item><item><title>Experience</title><link>https://averytan.com/education/</link><pubDate>Tue, 24 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/education/</guid><description/></item><item><title>Experience</title><link>https://averytan.com/experience/</link><pubDate>Tue, 24 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/experience/</guid><description/></item><item><title>Learn JavaScript</title><link>https://averytan.com/teaching/js/</link><pubDate>Tue, 24 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/teaching/js/</guid><description>&lt;p>
is designed to give technical content creators a seamless experience. You can focus on the content and the Hugo Blox Builder which this template is built upon handles the rest.&lt;/p>
&lt;p>&lt;strong>Embed videos, podcasts, code, LaTeX math, and even test students!&lt;/strong>&lt;/p>
&lt;p>On this page, you&amp;rsquo;ll find some examples of the types of technical content that can be rendered with Hugo Blox.&lt;/p>
&lt;h2 id="video">Video&lt;/h2>
&lt;p>Teach your course by sharing videos with your students. Choose from one of the following approaches:&lt;/p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/D2vj0WcvH5c?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video">&lt;/iframe>
&lt;/div>
&lt;p>&lt;strong>Youtube&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; youtube w7Ft2ymGmfc &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>Bilibili&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; bilibili id=&amp;quot;BV1WV4y1r7DF&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>Video file&lt;/strong>&lt;/p>
&lt;p>Videos may be added to a page by either placing them in your &lt;code>assets/media/&lt;/code> media library or in your
, and then embedding them with the &lt;em>video&lt;/em> shortcode:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; video src=&amp;quot;my_video.mp4&amp;quot; controls=&amp;quot;yes&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;h2 id="podcast">Podcast&lt;/h2>
&lt;p>You can add a podcast or music to a page by placing the MP3 file in the page&amp;rsquo;s folder or the media library folder and then embedding the audio on your page with the &lt;em>audio&lt;/em> shortcode:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; audio src=&amp;quot;ambient-piano.mp3&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;p>Try it out:&lt;/p>
&lt;audio controls >
&lt;source src="https://averytan.com/teaching/js/ambient-piano.mp3" type="audio/mpeg">
&lt;/audio>
&lt;h2 id="test-students">Test students&lt;/h2>
&lt;p>Provide a simple yet fun self-assessment by revealing the solutions to challenges with the &lt;code>spoiler&lt;/code> shortcode:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">{{&lt;span class="p">&amp;lt;&lt;/span> &lt;span class="nt">spoiler&lt;/span> &lt;span class="na">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;👉 Click to view the solution&amp;#34;&lt;/span> &lt;span class="p">&amp;gt;&lt;/span>}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">You found me!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{&lt;span class="p">&amp;lt;&lt;/span> &lt;span class="p">/&lt;/span>&lt;span class="nt">spoiler&lt;/span> &lt;span class="p">&amp;gt;&lt;/span>}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;details class="spoiler " id="spoiler-2">
&lt;summary class="cursor-pointer">👉 Click to view the solution&lt;/summary>
&lt;div class="rounded-lg bg-neutral-50 dark:bg-neutral-800 p-2">
You found me 🎉
&lt;/div>
&lt;/details>
&lt;h2 id="math">Math&lt;/h2>
&lt;p>Hugo Blox Builder supports a Markdown extension for $\LaTeX$ math. You can enable this feature by toggling the &lt;code>math&lt;/code> option in your &lt;code>config/_default/params.yaml&lt;/code> file.&lt;/p>
&lt;p>To render &lt;em>inline&lt;/em> or &lt;em>block&lt;/em> math, wrap your LaTeX math with &lt;code>{{&amp;lt; math &amp;gt;}}$...${{&amp;lt; /math &amp;gt;}}&lt;/code> or &lt;code>{{&amp;lt; math &amp;gt;}}$$...$${{&amp;lt; /math &amp;gt;}}&lt;/code>, respectively.&lt;/p>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">We wrap the LaTeX math in the Hugo Blox &lt;em>math&lt;/em> shortcode to prevent Hugo rendering our math as Markdown.&lt;/span>
&lt;/div>
&lt;p>Example &lt;strong>math block&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-latex" data-lang="latex">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb">$$&lt;/span>&lt;span class="nb">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="nv">\gamma&lt;/span>&lt;span class="nb">_{n} &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\frac&lt;/span>&lt;span class="nb">{ &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> | &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n} &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">} &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb">^T &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> |}{&lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\|\nabla&lt;/span>&lt;span class="nb"> F&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb">{x}_{n}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb">{x}_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\|&lt;/span>&lt;span class="nb">^&lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="nb">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="s">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; /math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
$$\gamma_{n} = \frac{ \left | \left (\mathbf x_{n} - \mathbf x_{n-1} \right )^T \left [\nabla F (\mathbf x_{n}) - \nabla F (\mathbf x_{n-1}) \right ] \right |}{\left \|\nabla F(\mathbf{x}_{n}) - \nabla F(\mathbf{x}_{n-1}) \right \|^2}$$
&lt;p>Example &lt;strong>inline math&lt;/strong> &lt;code>{{&amp;lt; math &amp;gt;}}$\nabla F(\mathbf{x}_{n})${{&amp;lt; /math &amp;gt;}}&lt;/code> renders as $\nabla F(\mathbf{x}_{n})$
.&lt;/p>
&lt;p>Example &lt;strong>multi-line math&lt;/strong> using the math linebreak (&lt;code>\\&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-latex" data-lang="latex">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb">$$&lt;/span>&lt;span class="nb">f&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nb">k;p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\begin&lt;/span>&lt;span class="nb">{cases}p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">} &amp;amp; &lt;/span>&lt;span class="nv">\text&lt;/span>&lt;span class="nb">{if }k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">, &lt;/span>&lt;span class="nv">\\&lt;/span>&lt;span class="nb">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb">p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">} &amp;amp; &lt;/span>&lt;span class="nv">\text&lt;/span>&lt;span class="nb">{if }k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">.&lt;/span>&lt;span class="nv">\end&lt;/span>&lt;span class="nb">{cases}&lt;/span>&lt;span class="s">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; /math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
$$
f(k;p_{0}^{*}) = \begin{cases}p_{0}^{*} &amp; \text{if }k=1, \\
1-p_{0}^{*} &amp; \text{if }k=0.\end{cases}
$$
&lt;h2 id="code">Code&lt;/h2>
&lt;p>Hugo Blox Builder utilises Hugo&amp;rsquo;s Markdown extension for highlighting code syntax. The code theme can be selected in the &lt;code>config/_default/params.yaml&lt;/code> file.&lt;/p>
&lt;pre>&lt;code>```python
import pandas as pd
data = pd.read_csv(&amp;quot;data.csv&amp;quot;)
data.head()
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pandas&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">pd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;data.csv&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">head&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="inline-images">Inline Images&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&amp;lt;&lt;/span> &lt;span class="nx">icon&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="s">&amp;#34;python&amp;#34;&lt;/span> &lt;span class="p">&amp;gt;}}&lt;/span> &lt;span class="nx">Python&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;p>
&lt;span class="inline-block pr-1">
&lt;svg style="height: 1em; transform: translateY(0.1em);" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512" fill="currentColor">&lt;path d="M439.8 200.5c-7.7-30.9-22.3-54.2-53.4-54.2h-40.1v47.4c0 36.8-31.2 67.8-66.8 67.8H172.7c-29.2 0-53.4 25-53.4 54.3v101.8c0 29 25.2 46 53.4 54.3 33.8 9.9 66.3 11.7 106.8 0 26.9-7.8 53.4-23.5 53.4-54.3v-40.7H226.2v-13.6h160.2c31.1 0 42.6-21.7 53.4-54.2 11.2-33.5 10.7-65.7 0-108.6zM286.2 404c11.1 0 20.1 9.1 20.1 20.3 0 11.3-9 20.4-20.1 20.4-11 0-20.1-9.2-20.1-20.4.1-11.3 9.1-20.3 20.1-20.3zM167.8 248.1h106.8c29.7 0 53.4-24.5 53.4-54.3V91.9c0-29-24.4-50.7-53.4-55.6-35.8-5.9-74.7-5.6-106.8.1-45.2 8-53.4 24.7-53.4 55.6v40.7h106.9v13.6h-147c-31.1 0-58.3 18.7-66.8 54.2-9.8 40.7-10.2 66.1 0 108.6 7.6 31.6 25.7 54.2 56.8 54.2H101v-48.8c0-35.3 30.5-66.4 66.8-66.4zm-6.7-142.6c-11.1 0-20.1-9.1-20.1-20.3.1-11.3 9-20.4 20.1-20.4 11 0 20.1 9.2 20.1 20.4s-9 20.3-20.1 20.3z"/>&lt;/svg>
&lt;/span> Python&lt;/p>
&lt;h2 id="did-you-find-this-page-helpful-consider-sharing-it-">Did you find this page helpful? Consider sharing it 🙌&lt;/h2></description></item><item><title>Learn Python</title><link>https://averytan.com/teaching/python/</link><pubDate>Tue, 24 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/teaching/python/</guid><description>&lt;p>
is designed to give technical content creators a seamless experience. You can focus on the content and the Hugo Blox Builder which this template is built upon handles the rest.&lt;/p>
&lt;p>&lt;strong>Embed videos, podcasts, code, LaTeX math, and even test students!&lt;/strong>&lt;/p>
&lt;p>On this page, you&amp;rsquo;ll find some examples of the types of technical content that can be rendered with Hugo Blox.&lt;/p>
&lt;h2 id="video">Video&lt;/h2>
&lt;p>Teach your course by sharing videos with your students. Choose from one of the following approaches:&lt;/p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/D2vj0WcvH5c?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video">&lt;/iframe>
&lt;/div>
&lt;p>&lt;strong>Youtube&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; youtube w7Ft2ymGmfc &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>Bilibili&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; bilibili id=&amp;quot;BV1WV4y1r7DF&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;p>&lt;strong>Video file&lt;/strong>&lt;/p>
&lt;p>Videos may be added to a page by either placing them in your &lt;code>assets/media/&lt;/code> media library or in your
, and then embedding them with the &lt;em>video&lt;/em> shortcode:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; video src=&amp;quot;my_video.mp4&amp;quot; controls=&amp;quot;yes&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;h2 id="podcast">Podcast&lt;/h2>
&lt;p>You can add a podcast or music to a page by placing the MP3 file in the page&amp;rsquo;s folder or the media library folder and then embedding the audio on your page with the &lt;em>audio&lt;/em> shortcode:&lt;/p>
&lt;pre>&lt;code>{{&amp;lt; audio src=&amp;quot;ambient-piano.mp3&amp;quot; &amp;gt;}}
&lt;/code>&lt;/pre>
&lt;p>Try it out:&lt;/p>
&lt;audio controls >
&lt;source src="https://averytan.com/teaching/python/ambient-piano.mp3" type="audio/mpeg">
&lt;/audio>
&lt;h2 id="test-students">Test students&lt;/h2>
&lt;p>Provide a simple yet fun self-assessment by revealing the solutions to challenges with the &lt;code>spoiler&lt;/code> shortcode:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">{{&lt;span class="p">&amp;lt;&lt;/span> &lt;span class="nt">spoiler&lt;/span> &lt;span class="na">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;👉 Click to view the solution&amp;#34;&lt;/span> &lt;span class="p">&amp;gt;&lt;/span>}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">You found me!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">{{&lt;span class="p">&amp;lt;&lt;/span> &lt;span class="p">/&lt;/span>&lt;span class="nt">spoiler&lt;/span> &lt;span class="p">&amp;gt;&lt;/span>}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;details class="spoiler " id="spoiler-2">
&lt;summary class="cursor-pointer">👉 Click to view the solution&lt;/summary>
&lt;div class="rounded-lg bg-neutral-50 dark:bg-neutral-800 p-2">
You found me 🎉
&lt;/div>
&lt;/details>
&lt;h2 id="math">Math&lt;/h2>
&lt;p>Hugo Blox Builder supports a Markdown extension for $\LaTeX$ math. You can enable this feature by toggling the &lt;code>math&lt;/code> option in your &lt;code>config/_default/params.yaml&lt;/code> file.&lt;/p>
&lt;p>To render &lt;em>inline&lt;/em> or &lt;em>block&lt;/em> math, wrap your LaTeX math with &lt;code>{{&amp;lt; math &amp;gt;}}$...${{&amp;lt; /math &amp;gt;}}&lt;/code> or &lt;code>{{&amp;lt; math &amp;gt;}}$$...$${{&amp;lt; /math &amp;gt;}}&lt;/code>, respectively.&lt;/p>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">We wrap the LaTeX math in the Hugo Blox &lt;em>math&lt;/em> shortcode to prevent Hugo rendering our math as Markdown.&lt;/span>
&lt;/div>
&lt;p>Example &lt;strong>math block&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-latex" data-lang="latex">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb">$$&lt;/span>&lt;span class="nb">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="nv">\gamma&lt;/span>&lt;span class="nb">_{n} &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\frac&lt;/span>&lt;span class="nb">{ &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> | &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n} &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">} &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb">^T &lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb"> x_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> |}{&lt;/span>&lt;span class="nv">\left&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\|\nabla&lt;/span>&lt;span class="nb"> F&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb">{x}_{n}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\nabla&lt;/span>&lt;span class="nb"> F&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">\mathbf&lt;/span>&lt;span class="nb">{x}_{n&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\right&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\|&lt;/span>&lt;span class="nb">^&lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="nb">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="s">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; /math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
$$\gamma_{n} = \frac{ \left | \left (\mathbf x_{n} - \mathbf x_{n-1} \right )^T \left [\nabla F (\mathbf x_{n}) - \nabla F (\mathbf x_{n-1}) \right ] \right |}{\left \|\nabla F(\mathbf{x}_{n}) - \nabla F(\mathbf{x}_{n-1}) \right \|^2}$$
&lt;p>Example &lt;strong>inline math&lt;/strong> &lt;code>{{&amp;lt; math &amp;gt;}}$\nabla F(\mathbf{x}_{n})${{&amp;lt; /math &amp;gt;}}&lt;/code> renders as $\nabla F(\mathbf{x}_{n})$
.&lt;/p>
&lt;p>Example &lt;strong>multi-line math&lt;/strong> using the math linebreak (&lt;code>\\&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-latex" data-lang="latex">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb">$$&lt;/span>&lt;span class="nb">f&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nb">k;p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb"> &lt;/span>&lt;span class="nv">\begin&lt;/span>&lt;span class="nb">{cases}p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">} &amp;amp; &lt;/span>&lt;span class="nv">\text&lt;/span>&lt;span class="nb">{if }k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="nb">, &lt;/span>&lt;span class="nv">\\&lt;/span>&lt;span class="nb">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nb">p_{&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">}^{&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nb">} &amp;amp; &lt;/span>&lt;span class="nv">\text&lt;/span>&lt;span class="nb">{if }k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="nb">.&lt;/span>&lt;span class="nv">\end&lt;/span>&lt;span class="nb">{cases}&lt;/span>&lt;span class="s">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">{{&lt;/span>&amp;lt; /math &amp;gt;&lt;span class="nb">}}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
$$
f(k;p_{0}^{*}) = \begin{cases}p_{0}^{*} &amp; \text{if }k=1, \\
1-p_{0}^{*} &amp; \text{if }k=0.\end{cases}
$$
&lt;h2 id="code">Code&lt;/h2>
&lt;p>Hugo Blox Builder utilises Hugo&amp;rsquo;s Markdown extension for highlighting code syntax. The code theme can be selected in the &lt;code>config/_default/params.yaml&lt;/code> file.&lt;/p>
&lt;pre>&lt;code>```python
import pandas as pd
data = pd.read_csv(&amp;quot;data.csv&amp;quot;)
data.head()
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pandas&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">pd&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pd&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_csv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;data.csv&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">head&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="inline-images">Inline Images&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{{&amp;lt;&lt;/span> &lt;span class="nx">icon&lt;/span> &lt;span class="nx">name&lt;/span>&lt;span class="p">=&lt;/span>&lt;span class="s">&amp;#34;python&amp;#34;&lt;/span> &lt;span class="p">&amp;gt;}}&lt;/span> &lt;span class="nx">Python&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;p>
&lt;span class="inline-block pr-1">
&lt;svg style="height: 1em; transform: translateY(0.1em);" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512" fill="currentColor">&lt;path d="M439.8 200.5c-7.7-30.9-22.3-54.2-53.4-54.2h-40.1v47.4c0 36.8-31.2 67.8-66.8 67.8H172.7c-29.2 0-53.4 25-53.4 54.3v101.8c0 29 25.2 46 53.4 54.3 33.8 9.9 66.3 11.7 106.8 0 26.9-7.8 53.4-23.5 53.4-54.3v-40.7H226.2v-13.6h160.2c31.1 0 42.6-21.7 53.4-54.2 11.2-33.5 10.7-65.7 0-108.6zM286.2 404c11.1 0 20.1 9.1 20.1 20.3 0 11.3-9 20.4-20.1 20.4-11 0-20.1-9.2-20.1-20.4.1-11.3 9.1-20.3 20.1-20.3zM167.8 248.1h106.8c29.7 0 53.4-24.5 53.4-54.3V91.9c0-29-24.4-50.7-53.4-55.6-35.8-5.9-74.7-5.6-106.8.1-45.2 8-53.4 24.7-53.4 55.6v40.7h106.9v13.6h-147c-31.1 0-58.3 18.7-66.8 54.2-9.8 40.7-10.2 66.1 0 108.6 7.6 31.6 25.7 54.2 56.8 54.2H101v-48.8c0-35.3 30.5-66.4 66.8-66.4zm-6.7-142.6c-11.1 0-20.1-9.1-20.1-20.3.1-11.3 9-20.4 20.1-20.4 11 0 20.1 9.2 20.1 20.4s-9 20.3-20.1 20.3z"/>&lt;/svg>
&lt;/span> Python&lt;/p>
&lt;h2 id="did-you-find-this-page-helpful-consider-sharing-it-">Did you find this page helpful? Consider sharing it 🙌&lt;/h2></description></item><item><title>✅ Manage your projects</title><link>https://averytan.com/postcopy/project-management/</link><pubDate>Mon, 23 Oct 2023 00:00:00 +0000</pubDate><guid>https://averytan.com/postcopy/project-management/</guid><description>&lt;p>Easily manage your projects - create ideation mind maps, Gantt charts, todo lists, and more!&lt;/p>
&lt;h2 id="ideation">Ideation&lt;/h2>
&lt;p>Hugo Blox supports a Markdown extension for mindmaps.&lt;/p>
&lt;p>Simply insert a Markdown code block labelled as &lt;code>markmap&lt;/code> and optionally set the height of the mindmap as shown in the example below.&lt;/p>
&lt;p>Mindmaps can be created by simply writing the items as a Markdown list within the &lt;code>markmap&lt;/code> code block, indenting each item to create as many sub-levels as you need:&lt;/p>
&lt;div class="highlight">
&lt;pre class="chroma">
&lt;code>
```markmap {height="200px"}
- Hugo Modules
- Hugo Blox
- blox-plugins-netlify
- blox-plugins-netlify-cms
- blox-plugins-reveal
```
&lt;/code>
&lt;/pre>
&lt;/div>
&lt;p>renders as&lt;/p>
&lt;div class="markmap" style="height: 200px;">
&lt;pre>- Hugo Modules
- Hugo Blox
- blox-plugins-netlify
- blox-plugins-netlify-cms
- blox-plugins-reveal&lt;/pre>
&lt;/div>
&lt;h2 id="diagrams">Diagrams&lt;/h2>
&lt;p>Hugo Blox supports the &lt;em>Mermaid&lt;/em> Markdown extension for diagrams.&lt;/p>
&lt;p>An example &lt;strong>Gantt diagram&lt;/strong>:&lt;/p>
&lt;pre>&lt;code>```mermaid
gantt
section Section
Completed :done, des1, 2014-01-06,2014-01-08
Active :active, des2, 2014-01-07, 3d
Parallel 1 : des3, after des1, 1d
Parallel 2 : des4, after des1, 1d
Parallel 3 : des5, after des3, 1d
Parallel 4 : des6, after des4, 1d
```
&lt;/code>&lt;/pre>
&lt;p>renders as&lt;/p>
&lt;div class="mermaid">gantt
section Section
Completed :done, des1, 2014-01-06,2014-01-08
Active :active, des2, 2014-01-07, 3d
Parallel 1 : des3, after des1, 1d
Parallel 2 : des4, after des1, 1d
Parallel 3 : des5, after des3, 1d
Parallel 4 : des6, after des4, 1d
&lt;/div>
&lt;h2 id="todo-lists">Todo lists&lt;/h2>
&lt;p>You can even write your todo lists in Markdown too:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="k">- [x]&lt;/span> Write math example
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">- [x]&lt;/span> Write diagram example
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">- [ ]&lt;/span> Do something else
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>renders as&lt;/p>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Write math example
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Write diagram example&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Do something else&lt;/li>
&lt;/ul>
&lt;h2 id="did-you-find-this-page-helpful-consider-sharing-it-">Did you find this page helpful? Consider sharing it 🙌&lt;/h2></description></item><item><title>Spawning an Parrot Attack Machine on the Cloud</title><link>https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/</link><pubDate>Tue, 17 Aug 2021 00:00:00 +0000</pubDate><guid>https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/</guid><description>&lt;p>It is sometimes convenient to have a second attack machine somewhere else not connected to the same network. Your local machine at home may experience slow network connection to the internet, or we might also inadvertently blacklist our home ip address by sending too many requests to a target or account. In cases like these, it is handy to have a second attack machine that can get around these minor inconveniences. We also bump into some minor errors and other problems while setting this up that we will need to resolve and troubleshoot as we are setting up and configuring our machine.&lt;/p>
&lt;p>In this post, I&amp;rsquo;m going to spin up a Debian droplet on Digital Ocean, one of the many cloud service providers offering PaaS packages. Digital Ocean allows us to essentially ‘rent’ a computer system from a selection of regions around the world. One of the key differences between Cloud Providers and their earlier &amp;rsquo;legacy&amp;rsquo; Hosting Providers, is that Cloud Providers are more flexible as it is possible to ‘rent’ out their machines on a per hour or a per use basis as opposed to the traditional Hosting Providers which usually only rent out their machines on a per month basis. This has great advantages to the economics of businesses and industry where they might only have certain ‘peak’ usage times and thus are able to only pay for the time that they are using the resource. I&amp;rsquo;ve elected to use Digital Ocean as my Cloud Provider, but any similar Cloud Providers such as AWS, Azure, or Alibaba Cloud would work as well.&lt;/p>
&lt;p>We&amp;rsquo;ll be installing an instance of ParrotSec, a Debian-based Linux Operating System distribution geared towards security professionals and comes with a sleuth of tools tailored towards pentesting, forensics, and other security activities. In a way, it is the slightly lesser known cousin of the widely popular Kali Linux security distribution.&lt;/p>
&lt;p>Signing into my Digital Ocean dashboard, we create a new droplet.&lt;/p>
&lt;p>
&lt;figure id="figure-create-a-new-digital-ocean-droplet">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Create a new Digital Ocean droplet" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/digital-ocean-dashboard_hu_c70896bef8576fed.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/digital-ocean-dashboard_hu_c0b593e910e79630.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/digital-ocean-dashboard_hu_a848e6f647583e47.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/digital-ocean-dashboard_hu_c70896bef8576fed.webp"
width="760"
height="334"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Create a new Digital Ocean droplet
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We will then select the configuration that our spawned droplet will have.&lt;/p>
&lt;p>
&lt;figure id="figure-we-will-spawn-a-debian-10-machine-with-a-general-purpose-dedicated-cpu">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We will spawn a Debian 10 machine with a general purpose dedicated CPU" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/new-droplet_hu_3bf62a2a11215749.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/new-droplet_hu_b581e234e962ecb2.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/new-droplet_hu_75c6ebadba8d8ecf.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/new-droplet_hu_3bf62a2a11215749.webp"
width="760"
height="411"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We will spawn a Debian 10 machine with a general purpose dedicated CPU
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-our-machine-will-be-located-in-singapore">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img src="region-selection" alt="Our machine will be located in Singapore" loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Our machine will be located in Singapore
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-we-will-add-our-public-ssh-key-for-public-key-authentication">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We will add our public ssh key for public key authentication" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-setup_hu_e31dec4e7e6f8316.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-setup_hu_24ceffd26ed6971b.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-setup_hu_79f58c688cdc0fa7.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-setup_hu_e31dec4e7e6f8316.webp"
width="760"
height="262"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We will add our public ssh key for public key authentication
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>After creating out droplet, we are now able to ssh into the machine as root at the ip address provided by Digital ocean&lt;/p>
&lt;p>
&lt;figure id="figure-ssh-ing-into-our-newly-spawned-machine">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="ssh-ing into our newly spawned machine" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-login_hu_abe4b0f67e471e64.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-login_hu_ea17949faf666730.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-login_hu_f0e7ce0964234217.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/ssh-login_hu_abe4b0f67e471e64.webp"
width="760"
height="190"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
ssh-ing into our newly spawned machine
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We inspect the parrot documentation page for the repository locations. These are links and locations that affect what software packages are available for download and what versions, and contain details of who packages the software. In this case, we are interested in the software packaged by Parrot.&lt;/p>
&lt;p>
&lt;figure id="figure-finding-the-parrot-software-repositories">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Finding the Parrot software repositories" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-repo_hu_9324dd2492f9c662.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-repo_hu_2269917368503d70.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-repo_hu_5e3d2d11aebfedfe.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-repo_hu_9324dd2492f9c662.webp"
width="760"
height="281"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Finding the Parrot software repositories
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We need to add these entries to our sources.list file so our machine knows which repositories to look at for software updates and installation.&lt;/p>
&lt;p>
&lt;figure id="figure-the-sourceslist-file-on-our-debian-machine">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="The sources.list file on our Debian machine" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/sources_hu_9a069e88ba5a8ad6.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/sources_hu_7053b525c0b696e5.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/sources_hu_ca1f950bfe61b4ba.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/sources_hu_9a069e88ba5a8ad6.webp"
width="760"
height="303"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
The sources.list file on our Debian machine
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Now that we have the needed Parrot entries in our sources.list file, we should perform an &amp;lsquo;apt update&amp;rsquo; to download package information from our newly added Parrot sources and see which software we need to install.&lt;/p>
&lt;p>However, when I ran the &amp;lsquo;apt update&amp;rsquo;, I got a GPG no public key found error&lt;/p>
&lt;p>
&lt;figure id="figure-no-public-gpg-key-found">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="No public GPG key found" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/gpg-error_hu_98daa6441167e0dc.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/gpg-error_hu_3cf0dad79810f69a.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/gpg-error_hu_7b9ebd74db11d49.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/gpg-error_hu_98daa6441167e0dc.webp"
width="760"
height="187"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
No public GPG key found
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>GPG is a public key crytographic algorithm that is used, in this case, as a digital signature to verify that the repository we are downloading packages from really are authored and belong to the Parrot team. (See digital signatures)&lt;/p>
&lt;p>Doing some Google-fu, I found a solution here and here to add the required public keys and verify the authenticity of the Parrot sources.&lt;/p>
&lt;p>
&lt;figure id="figure-adding-the-missing-gpg-public-key">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Adding the missing GPG public key" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/adding-gpg_hu_52ef3ed146b5f6c1.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/adding-gpg_hu_9c50816491d15662.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/adding-gpg_hu_933774e195408acc.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/adding-gpg_hu_52ef3ed146b5f6c1.webp"
width="760"
height="362"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Adding the missing GPG public key
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Now that we have added Parrot&amp;rsquo;s public GPG key, we rerun our &amp;lsquo;apt update&amp;rsquo;, then search for the correct Parrot package we want to install.&lt;/p>
&lt;p>
&lt;figure id="figure-apt-update-note-that-we-have-274-packages-that-can-be-upgraded-best-practise-is-to-update-these-packages-before-installing-parrot">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="apt update (Note that we have 274 packages that can be upgraded. Best practise is to update these packages before installing Parrot.)" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/update_hu_96dd0e2fd11ad77f.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/update_hu_c1e4cb0f72db597d.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/update_hu_bec2516aa84a7611.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/update_hu_96dd0e2fd11ad77f.webp"
width="760"
height="282"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
apt update (Note that we have 274 packages that can be upgraded. Best practise is to update these packages before installing Parrot.)
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-searching-for-parrot">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Searching for Parrot" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/search-parrot_hu_f4e29898dc621138.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/search-parrot_hu_183aab19a481eca3.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/search-parrot_hu_f2761f6198cbed2d.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/search-parrot_hu_f4e29898dc621138.webp"
width="760"
height="366"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Searching for Parrot
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Scrolling down, we find the Parrot we are looking for.&lt;/p>
&lt;p>
&lt;figure id="figure-the-right-parrot">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="The right parrot" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/right-parrot_hu_66f043a88ded3718.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/right-parrot_hu_8cc6997e85eaacac.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/right-parrot_hu_e25930dee14a8a04.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/right-parrot_hu_66f043a88ded3718.webp"
width="760"
height="440"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
The right parrot
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We then enter &amp;lsquo;apt install parrot-meta-all -y&amp;rsquo; to begin installation&lt;/p>
&lt;p>
&lt;figure id="figure-we-then-enter-apt-install-parrot-meta-all--y-to-begin-installation">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We then enter &amp;lsquo;apt install parrot-meta-all -y&amp;rsquo; to begin installation" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/install_hu_a16dfb6cb436cfe3.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/install_hu_60ef41e5fe97010e.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/install_hu_526a3c7286aec7a7.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/install_hu_a16dfb6cb436cfe3.webp"
width="760"
height="267"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We then enter &amp;lsquo;apt install parrot-meta-all -y&amp;rsquo; to begin installation
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Another stumbling block we find is during the installation, we are returning a number of 404 errors and a failed installation.&lt;/p>
&lt;p>
&lt;figure id="figure-404-errors">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="404 errors" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/404_hu_c6631bc44fb52b17.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/404_hu_55ed5d2e642a7f30.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/404_hu_af43a8101f9e1f47.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/404_hu_c6631bc44fb52b17.webp"
width="760"
height="508"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
404 errors
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>If we ran a whois lookup on that offending IP address, we see that it is registered to the Singaporean branch of OVH , a French webhosting and cloud provider.&lt;/p>
&lt;p>
&lt;figure id="figure-ovh-singapore">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="OVH Singapore" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/whois_hu_1c45d585df577fb9.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/whois_hu_c5a36cf27e8a5d78.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/whois_hu_a00dfabf94f39853.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/whois_hu_1c45d585df577fb9.webp"
width="760"
height="481"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
OVH Singapore
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>My guess of what is happening is that our Parrot entries we added to our sources.list file is being resolved to a server hosting the Parrot packages in Singapore that, as of this writting is down for whatever reason (Software packages are usually hosted on a number of servers around the world for redundancy as well as performance).&lt;/p>
&lt;p>With this in mind, we go back to the Parrot documentation and look for the list of known mirrors (locations of other servers hosting the Parrot packages)&lt;/p>
&lt;p>
&lt;figure id="figure-the-other-mirrors">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="The other mirrors" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/mirrors_hu_404b5f70e4f9dd75.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/mirrors_hu_52799c9dbbfbbc9c.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/mirrors_hu_488131fa971eb75a.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/mirrors_hu_404b5f70e4f9dd75.webp"
width="760"
height="472"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
The other mirrors
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Seeing that our machine is spawned in Singapore, let&amp;rsquo;s select a mirror that is fairly close to that region of the globe. We copy and paste the entry for Taiwan&amp;rsquo;s NCHC&amp;rsquo;s Free Software Lab into our sources.list file.&lt;/p>
&lt;p>
&lt;figure id="figure-adding-the-taiwanese-mirror-to-our-sourceslist-file">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Adding the Taiwanese mirror to our sources.list file" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/taiwan-mirror_hu_71bc6352792afcfd.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/taiwan-mirror_hu_30edb5d2eeaa4b71.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/taiwan-mirror_hu_7d5ea454d37a1f62.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/taiwan-mirror_hu_71bc6352792afcfd.webp"
width="760"
height="282"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Adding the Taiwanese mirror to our sources.list file
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>With this done, let&amp;rsquo;s do another &amp;lsquo;apt update&amp;rsquo; to download package data from our newly added sources.&lt;/p>
&lt;p>
&lt;figure id="figure-updating-our-package-information-from-our-newly-added-sources">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Updating our package information from our newly added sources" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/new-update_hu_a130eaec30c6260c.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/new-update_hu_73febc59293604d5.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/new-update_hu_9b0333b99d34d366.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/new-update_hu_a130eaec30c6260c.webp"
width="760"
height="146"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Updating our package information from our newly added sources
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Then let&amp;rsquo;s try reinstall the parrot-meta-all package again. This time, the installation completes with no failures. We then update the distribution, and remove any unneeded packages before restarting the machine.&lt;/p>
&lt;p>
&lt;figure id="figure-upgrading-the-distribution">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Upgrading the distribution" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/dist-upgrade_hu_4e3793c4a6a377aa.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/dist-upgrade_hu_e481d09b9edd5f7f.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/dist-upgrade_hu_c84e28ddca53038e.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/dist-upgrade_hu_4e3793c4a6a377aa.webp"
width="760"
height="396"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Upgrading the distribution
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-system-restart">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="System restart" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/restart_hu_7c243680222bc420.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/restart_hu_d9f482663803a97.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/restart_hu_6c714b5549436c94.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/restart_hu_7c243680222bc420.webp"
width="760"
height="47"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
System restart
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Give the system a few minutes to reboot, and when we ssh into it again, we see that Parrot OS is installed and we now have access to all our favourite hacker tools!&lt;/p>
&lt;p>
&lt;figure id="figure-parrot-is-online">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Parrot is online" srcset="
/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-on_hu_80a277f52cb4e77c.webp 400w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-on_hu_4880887deb9ec682.webp 760w,
/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-on_hu_65ae7f48e4efb493.webp 1200w"
src="https://averytan.com/post/spawning-an-parrot-attack-machine-on-the-cloud/parrot-on_hu_80a277f52cb4e77c.webp"
width="760"
height="360"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Parrot is online
&lt;/figcaption>&lt;/figure>
&lt;/p></description></item><item><title>Creating an Active Directory Lab Environment</title><link>https://averytan.com/post/creating-an-active-directory-lab-environment/</link><pubDate>Wed, 11 Aug 2021 00:00:00 +0000</pubDate><guid>https://averytan.com/post/creating-an-active-directory-lab-environment/</guid><description>&lt;p>When I bought, set up, and upgraded my desktop PC, the goal was to have a pretty powerful machine that I can use to power a home lab that could handle running multiple virtual machine instances with ease. Since I&amp;rsquo;ve recently upgraded my RAM up to 24GB and had some free time, I thought I would get on that and write a post about it.&lt;/p>
&lt;p>We will be setting up a simple Active Directory lab. The network would consist of probably 3-4 machines with one Domain Controller. We&amp;rsquo;ll set it up, and then we can use this AD network to practise AD hardening concepts, as well as simulate attacks, enumeration and other AD-related security tasks.&lt;/p>
&lt;p>It is typically used in a corporate environment and allows centralized authentication and authorization of all users and computers and allows access to these shared resources and the setting of a common shared settings in an office.&lt;/p>
&lt;p>A Windows Active Directory deployment consists of the following elements
• Objects which represent a single entity (a printer, a computer, a user, a group (of users) ) and its attributes. An object has a unique name.
• Domains are a collection of objects that are all stored on the same Active Directory database. Domains have their own policies (password policy, user accounts, machines, update schedule, etc&amp;hellip;)
• Trees are a collection of domains
• A Forests is a collection of trees&lt;/p>
&lt;p>Domains are managed by a special Windows server known as the Domain Controller. It is the centralized server that other resources rely on for authentication into the network and access to the directory service. Due to the centralized nature of Active Directory, most corporate networks have more than one Domain Controller to provide failover redundancy in case of failure of the primary Domain Controller. Larger networks also have more Domain Controllers to scale up performance. The Domain Controller then can be thought of as the central brain of an Active Directory network. It is the machine that handles authentication and authorizes machines/users onto the network and for resource access.&lt;/p>
&lt;p>The machines:
I downloaded a bunch of Windows Operating Systems from Microsoft&amp;rsquo;s ‘Windows Evaluation Centre’. We&amp;rsquo;ll set up an Active Directory environment using the following systems:
• Windows Server 2019
• 2 Windows 10 Enterprise Systems
• Windows 7 Professional&lt;/p>
&lt;p>I&amp;rsquo;m using virtualbox as my hypervisor of choice to set up these virtual machines. This post will not go in-depth into the steps to install a Windows Virtual Machine (VM). There are many guides online such as this.&lt;/p>
&lt;p>
&lt;figure id="figure-our-4-vms-are-installed-and-online">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Our 4 VMs are installed and online" srcset="
/post/creating-an-active-directory-lab-environment/vms_hu_855bd7040a8b5ad7.webp 400w,
/post/creating-an-active-directory-lab-environment/vms_hu_4748b348454e5ab5.webp 760w,
/post/creating-an-active-directory-lab-environment/vms_hu_c8287e5a1e633920.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/vms_hu_855bd7040a8b5ad7.webp"
width="760"
height="403"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Our 4 VMs are installed and online
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The Windows Server 2019 VM will be our sole domain controller on this AD network. We rename the PC to &amp;lsquo;CYDONIA-DC&amp;rsquo;, and install Active Directory Domain Services through the Server Manager on this server.&lt;/p>
&lt;p>
&lt;figure id="figure-cydonia-dc-our-soon-to-be-domain-controller-click-on-add-roles-and-features-under-the-manage-tab">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="CYDONIA-DC; our soon-to-be Domain Controller. Click on &amp;lsquo;Add Roles and Features&amp;rsquo; under the &amp;lsquo;Manage&amp;rsquo; tab" srcset="
/post/creating-an-active-directory-lab-environment/cydonia_hu_4c9d3759c8c4421f.webp 400w,
/post/creating-an-active-directory-lab-environment/cydonia_hu_7fbf3e47865f6be0.webp 760w,
/post/creating-an-active-directory-lab-environment/cydonia_hu_edbac9b623cc23e9.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/cydonia_hu_4c9d3759c8c4421f.webp"
width="760"
height="407"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
CYDONIA-DC; our soon-to-be Domain Controller. Click on &amp;lsquo;Add Roles and Features&amp;rsquo; under the &amp;lsquo;Manage&amp;rsquo; tab
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-click-next">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Click next" srcset="
/post/creating-an-active-directory-lab-environment/next_hu_1a22c165e3d5eb12.webp 400w,
/post/creating-an-active-directory-lab-environment/next_hu_f43cc0f0e708e3ff.webp 760w,
/post/creating-an-active-directory-lab-environment/next_hu_e3f1b6167c80552b.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/next_hu_1a22c165e3d5eb12.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Click next
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-have-role-based-or-feature-based-installation-selected-and-hit-next">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Have &amp;lsquo;Role-based or feature-based installation&amp;rsquo; selected and hit Next" srcset="
/post/creating-an-active-directory-lab-environment/role-based-install_hu_bb1acda1884bb4e2.webp 400w,
/post/creating-an-active-directory-lab-environment/role-based-install_hu_efdd9b2f7e1e2111.webp 760w,
/post/creating-an-active-directory-lab-environment/role-based-install_hu_92bdfa632e3dab7a.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/role-based-install_hu_bb1acda1884bb4e2.webp"
width="760"
height="405"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Have &amp;lsquo;Role-based or feature-based installation&amp;rsquo; selected and hit Next
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-have-role-based-or-feature-based-installation-selected-and-hit-next">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Have our server, CYDONIA-DC selected and click Next" srcset="
/post/creating-an-active-directory-lab-environment/step3_hu_6533448cef4374b2.webp 400w,
/post/creating-an-active-directory-lab-environment/step3_hu_cf0b449d31d0e5e0.webp 760w,
/post/creating-an-active-directory-lab-environment/step3_hu_3d4d0889c3d52d17.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step3_hu_6533448cef4374b2.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Have &amp;lsquo;Role-based or feature-based installation&amp;rsquo; selected and hit Next
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-have-active-directory-domain-services-selected-and-click-next">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Have &amp;lsquo;Active Directory Domain Services&amp;rsquo; selected and click Next" srcset="
/post/creating-an-active-directory-lab-environment/step4_hu_a0e6ba96669ece46.webp 400w,
/post/creating-an-active-directory-lab-environment/step4_hu_7478a22cd286316d.webp 760w,
/post/creating-an-active-directory-lab-environment/step4_hu_eac579a6c11fe09.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step4_hu_a0e6ba96669ece46.webp"
width="760"
height="405"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Have &amp;lsquo;Active Directory Domain Services&amp;rsquo; selected and click Next
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-keep-the-rest-of-the-options-as-defualts-and-confirm-the-installation-to-install-active-directory-domain-services">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Keep the rest of the options as defualts and confirm the installation to install Active Directory Domain Services." srcset="
/post/creating-an-active-directory-lab-environment/step5_hu_37f845f902b7d238.webp 400w,
/post/creating-an-active-directory-lab-environment/step5_hu_8cd7b3709a60ed46.webp 760w,
/post/creating-an-active-directory-lab-environment/step5_hu_c3a7c03d5ef8eac6.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step5_hu_37f845f902b7d238.webp"
width="760"
height="388"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Keep the rest of the options as defualts and confirm the installation to install Active Directory Domain Services.
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Installation can take a few minutes. Once this is done installing, we need to promote this server to a Domain Controller. We&amp;rsquo;ll need to create a new forest and give our root domain a name.&lt;/p>
&lt;p>
&lt;figure id="figure-select-promote-this-server-to-a-domain-controller">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Select &amp;lsquo;Promote this server to a domain controller&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step6_hu_c56a6c60a69276ed.webp 400w,
/post/creating-an-active-directory-lab-environment/step6_hu_a7a5127326aae8c8.webp 760w,
/post/creating-an-active-directory-lab-environment/step6_hu_846bcd63ace30e99.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step6_hu_c56a6c60a69276ed.webp"
width="760"
height="407"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Select &amp;lsquo;Promote this server to a domain controller&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-well-add-a-new-forest-and-set-our-domain-name-to-sanctuarylocal">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We&amp;rsquo;ll add a new forest and set our domain name to &amp;lsquo;SANCTUARY.local&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step7_hu_6ad55743c9eb039e.webp 400w,
/post/creating-an-active-directory-lab-environment/step7_hu_1f5e9ecfec36374f.webp 760w,
/post/creating-an-active-directory-lab-environment/step7_hu_a7ce2c2c5b6a6c92.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step7_hu_6ad55743c9eb039e.webp"
width="760"
height="389"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We&amp;rsquo;ll add a new forest and set our domain name to &amp;lsquo;SANCTUARY.local&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We&amp;rsquo;ll also need to create a password&lt;/p>
&lt;p>
&lt;figure id="figure-the-netbios-domain-name-is-sanctuary">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="The NetBIOS domain name is &amp;lsquo;SANCTUARY&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step8_hu_4048be6445391afd.webp 400w,
/post/creating-an-active-directory-lab-environment/step8_hu_e2b9f77d6ec3ce80.webp 760w,
/post/creating-an-active-directory-lab-environment/step8_hu_e584f0083c7acfea.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step8_hu_4048be6445391afd.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
The NetBIOS domain name is &amp;lsquo;SANCTUARY&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-well-keep-most-of-the-rest-of-the-settings-as-default">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We&amp;rsquo;ll keep most of the rest of the settings as default." srcset="
/post/creating-an-active-directory-lab-environment/step9_hu_7355d5b7264db695.webp 400w,
/post/creating-an-active-directory-lab-environment/step9_hu_7123cecdee285835.webp 760w,
/post/creating-an-active-directory-lab-environment/step9_hu_5d0f8f7f7f058935.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step9_hu_7355d5b7264db695.webp"
width="760"
height="396"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We&amp;rsquo;ll keep most of the rest of the settings as default.
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-well-keep-most-of-the-rest-of-the-settings-as-default">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We&amp;rsquo;ll keep most of the rest of the settings as default." srcset="
/post/creating-an-active-directory-lab-environment/step10_hu_1764ae3f289f7096.webp 400w,
/post/creating-an-active-directory-lab-environment/step10_hu_e83399d07cbb80a2.webp 760w,
/post/creating-an-active-directory-lab-environment/step10_hu_e3e109f8e39a076a.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step10_hu_1764ae3f289f7096.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We&amp;rsquo;ll keep most of the rest of the settings as default.
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Once the server finishes installation and restarts, we&amp;rsquo;ll see that our login screen now shows that we are logging into the SANCTUARY domain as the domain user Administrator.&lt;/p>
&lt;p>
&lt;figure id="figure-logging-in-as-administrator-on-the-sanctuary-domain">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Logging in as &amp;lsquo;Administrator&amp;rsquo; on the &amp;lsquo;SANCTUARY&amp;rsquo; domain" srcset="
/post/creating-an-active-directory-lab-environment/step11_hu_abc6570e1f229cdc.webp 400w,
/post/creating-an-active-directory-lab-environment/step11_hu_77f88e85d4b07480.webp 760w,
/post/creating-an-active-directory-lab-environment/step11_hu_66d5128089930ba3.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step11_hu_abc6570e1f229cdc.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Logging in as &amp;lsquo;Administrator&amp;rsquo; on the &amp;lsquo;SANCTUARY&amp;rsquo; domain
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The next step is to configure Users, Groups, and Policies objects. We&amp;rsquo;ll add a bunch of new users.&lt;/p>
&lt;p>
&lt;figure id="figure-select-active-directory-users-and-computers-under-the-tools-tab">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Select &amp;lsquo;Active Directory Users and Computers&amp;rsquo; under the &amp;lsquo;Tools&amp;rsquo; tab" srcset="
/post/creating-an-active-directory-lab-environment/step12_hu_c3e572d84b9f7c04.webp 400w,
/post/creating-an-active-directory-lab-environment/step12_hu_f47222592fffd9c1.webp 760w,
/post/creating-an-active-directory-lab-environment/step12_hu_1e02608100487a7f.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step12_hu_c3e572d84b9f7c04.webp"
width="760"
height="384"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Select &amp;lsquo;Active Directory Users and Computers&amp;rsquo; under the &amp;lsquo;Tools&amp;rsquo; tab
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-select-users-in-the-left-hand-expandable-menu-and-right-click-and-select-new---user-to-create-a-new-domain-user">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Select &amp;lsquo;Users&amp;rsquo; in the left hand expandable menu, and right click and select &amp;lsquo;New -&amp;gt; User&amp;rsquo; to create a new Domain User." srcset="
/post/creating-an-active-directory-lab-environment/step13_hu_fc4a8b293a5be4cb.webp 400w,
/post/creating-an-active-directory-lab-environment/step13_hu_b161dfba083f123e.webp 760w,
/post/creating-an-active-directory-lab-environment/step13_hu_52655da25926a736.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step13_hu_fc4a8b293a5be4cb.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Select &amp;lsquo;Users&amp;rsquo; in the left hand expandable menu, and right click and select &amp;lsquo;New -&amp;gt; User&amp;rsquo; to create a new Domain User.
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-enter-the-user-details-and-hit-next">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Enter the user details and hit &amp;lsquo;Next&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step14_hu_d6b04c1b83df494a.webp 400w,
/post/creating-an-active-directory-lab-environment/step14_hu_8aa5ef14a37538b7.webp 760w,
/post/creating-an-active-directory-lab-environment/step14_hu_b13410f9d20f16cc.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step14_hu_d6b04c1b83df494a.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Enter the user details and hit &amp;lsquo;Next&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-create-a-password-for-our-user-and-hit-next-and-then-finish">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Create a password for our user and hit &amp;lsquo;Next&amp;rsquo; and then &amp;lsquo;Finish&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step15_hu_d87c9abd47f07c2f.webp 400w,
/post/creating-an-active-directory-lab-environment/step15_hu_caf002686933fc99.webp 760w,
/post/creating-an-active-directory-lab-environment/step15_hu_b7b47fffa5ae0304.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step15_hu_d87c9abd47f07c2f.webp"
width="760"
height="407"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Create a password for our user and hit &amp;lsquo;Next&amp;rsquo; and then &amp;lsquo;Finish&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-we-should-then-see-our-newly-created-users">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We should then see our newly created users" srcset="
/post/creating-an-active-directory-lab-environment/step16_hu_57b81ece0b4944ba.webp 400w,
/post/creating-an-active-directory-lab-environment/step16_hu_91da1ed9d5933bd0.webp 760w,
/post/creating-an-active-directory-lab-environment/step16_hu_4111b2b7c4fb6fd5.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step16_hu_57b81ece0b4944ba.webp"
width="760"
height="317"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We should then see our newly created users
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We&amp;rsquo;ll also set up a file share on our Domain Controller.&lt;/p>
&lt;p>
&lt;figure id="figure-under-the-file-and-storage-services-on-the-left-most-menu-select-shares-then-under-the-tasks-dropdown-menu-select-new-share">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Under the &amp;lsquo;File and Storage Services&amp;rsquo; on the left most menu, select &amp;lsquo;Shares&amp;rsquo;. Then under the &amp;lsquo;TASKS&amp;rsquo; dropdown menu, select &amp;lsquo;New Share&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step17_hu_78f7f342c2c16624.webp 400w,
/post/creating-an-active-directory-lab-environment/step17_hu_3103ac5e44e37c5d.webp 760w,
/post/creating-an-active-directory-lab-environment/step17_hu_e1bd68447b9c75cb.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step17_hu_78f7f342c2c16624.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Under the &amp;lsquo;File and Storage Services&amp;rsquo; on the left most menu, select &amp;lsquo;Shares&amp;rsquo;. Then under the &amp;lsquo;TASKS&amp;rsquo; dropdown menu, select &amp;lsquo;New Share&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-well-use-the-smb-share---quick-option-and-hit-next">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We&amp;rsquo;ll use the &amp;lsquo;SMB Share - Quick&amp;rsquo; option and hit &amp;lsquo;Next&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step18_hu_f08669be15859578.webp 400w,
/post/creating-an-active-directory-lab-environment/step18_hu_4fceeb7fe4f44e45.webp 760w,
/post/creating-an-active-directory-lab-environment/step18_hu_176cbbdd5e0d2d85.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step18_hu_f08669be15859578.webp"
width="760"
height="406"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We&amp;rsquo;ll use the &amp;lsquo;SMB Share - Quick&amp;rsquo; option and hit &amp;lsquo;Next&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-well-have-this-share-on-our-cydonia-dc-domain-controller">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We&amp;rsquo;ll have this share on our CYDONIA-DC domain controller" srcset="
/post/creating-an-active-directory-lab-environment/step19_hu_1148e519857b11e8.webp 400w,
/post/creating-an-active-directory-lab-environment/step19_hu_d3dfce21f60c460d.webp 760w,
/post/creating-an-active-directory-lab-environment/step19_hu_3ec4deaa28decd78.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step19_hu_1148e519857b11e8.webp"
width="760"
height="406"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We&amp;rsquo;ll have this share on our CYDONIA-DC domain controller
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-well-name-this-share-corporate-click-next-and-finish-the-installation">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We&amp;rsquo;ll name this share &amp;lsquo;corporate&amp;rsquo;, click &amp;lsquo;Next&amp;rsquo; and finish the installation" srcset="
/post/creating-an-active-directory-lab-environment/step20_hu_e2e28802fba7fe38.webp 400w,
/post/creating-an-active-directory-lab-environment/step20_hu_eb33a53f936ebd06.webp 760w,
/post/creating-an-active-directory-lab-environment/step20_hu_cf465439802d833c.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step20_hu_e2e28802fba7fe38.webp"
width="760"
height="406"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We&amp;rsquo;ll name this share &amp;lsquo;corporate&amp;rsquo;, click &amp;lsquo;Next&amp;rsquo; and finish the installation
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We will also set up a SQL service account. We have to set up something called a Service Principal Name (SPN) which is used by clients to identify a service instance, in this case, our SQL Service instance, and allow authentication to associate a service instance with a service logon account. This will be important for some of the attacks we will be conducting in the future on our Active Directory (AD) lab.&lt;/p>
&lt;p>
&lt;figure id="figure-well-create-a-new-user-and-name-it-sql-service">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="We&amp;rsquo;ll create a new User and name it SQL Service" srcset="
/post/creating-an-active-directory-lab-environment/step21_hu_87d4ab9389bf49c.webp 400w,
/post/creating-an-active-directory-lab-environment/step21_hu_1eb9d1abad1c5db3.webp 760w,
/post/creating-an-active-directory-lab-environment/step21_hu_608f65e7413c4d71.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step21_hu_87d4ab9389bf49c.webp"
width="760"
height="406"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
We&amp;rsquo;ll create a new User and name it SQL Service
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-setting-up-spn-via-the-command-line">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Setting up SPN via the command line" srcset="
/post/creating-an-active-directory-lab-environment/step22_hu_e56ca924e3974e91.webp 400w,
/post/creating-an-active-directory-lab-environment/step22_hu_aa11577b639ca4f7.webp 760w,
/post/creating-an-active-directory-lab-environment/step22_hu_d035b7cfeeeb5a7b.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step22_hu_e56ca924e3974e91.webp"
width="760"
height="174"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Setting up SPN via the command line
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-confirming-spn-was-set-via-the-command-line">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Confirming SPN was set via the command line" srcset="
/post/creating-an-active-directory-lab-environment/step23_hu_8e3a86dea2b56485.webp 400w,
/post/creating-an-active-directory-lab-environment/step23_hu_76813dd77648624.webp 760w,
/post/creating-an-active-directory-lab-environment/step23_hu_f69db6f054a3cf53.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step23_hu_8e3a86dea2b56485.webp"
width="760"
height="355"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Confirming SPN was set via the command line
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Our Domain Controller is now up and running. We will return in the future to modify and edit this server but for now, we can move on to setting up the host machines.&lt;/p>
&lt;p>We will boot up our host machines, rename them, and create a new share in our C: drive&lt;/p>
&lt;p>
&lt;figure id="figure-create-a-new-folder-under-the-c-drive-right-click-and-access-its-properties-and-navigate-to-the-sharing-tab-and-click-share">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Create a new folder under the C: Drive, right click and access its properties and navigate to the &amp;lsquo;Sharing&amp;rsquo; tab and click &amp;lsquo;Share&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step24_hu_6365c1269f01e67b.webp 400w,
/post/creating-an-active-directory-lab-environment/step24_hu_7de50d4d84319d99.webp 760w,
/post/creating-an-active-directory-lab-environment/step24_hu_3cb2dd903ccb1a38.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step24_hu_6365c1269f01e67b.webp"
width="760"
height="407"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Create a new folder under the C: Drive, right click and access its properties and navigate to the &amp;lsquo;Sharing&amp;rsquo; tab and click &amp;lsquo;Share&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-click-share">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Click &amp;lsquo;share&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step25_hu_d547013cbb1252a3.webp 400w,
/post/creating-an-active-directory-lab-environment/step25_hu_55885ae9c63977ca.webp 760w,
/post/creating-an-active-directory-lab-environment/step25_hu_a1e857da1d4d7207.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step25_hu_d547013cbb1252a3.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Click &amp;lsquo;share&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We then need to change our Network Adapter settings to perform DNS through our Domain Controller.&lt;/p>
&lt;p>We can note from running &amp;lsquo;ipconfig&amp;rsquo; on a command line on our Domain Controller that its IP address is 192.168.33.111. This is the IP address we need to set as the DNS server on each of our host machines.&lt;/p>
&lt;p>
&lt;figure id="figure-getting-the-ip-address-of-our-domain-controller">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Getting the IP address of our Domain Controller" srcset="
/post/creating-an-active-directory-lab-environment/step26_hu_c804cffeba90bdb6.webp 400w,
/post/creating-an-active-directory-lab-environment/step26_hu_4716e33b55885c04.webp 760w,
/post/creating-an-active-directory-lab-environment/step26_hu_b1f0957da93f3fa9.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step26_hu_c804cffeba90bdb6.webp"
width="760"
height="406"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Getting the IP address of our Domain Controller
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-in-the-network-connections-section-of-the-control-panel-right-click-on-our-ethernet-connection-and-select-properties">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="In the &amp;lsquo;Network Connections&amp;rsquo; section of the control panel, right click on our Ethernet connection and select &amp;lsquo;Properties&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step27_hu_ee9217fcc956feef.webp 400w,
/post/creating-an-active-directory-lab-environment/step27_hu_70945839edd44585.webp 760w,
/post/creating-an-active-directory-lab-environment/step27_hu_50005029d0734723.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step27_hu_ee9217fcc956feef.webp"
width="760"
height="402"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
In the &amp;lsquo;Network Connections&amp;rsquo; section of the control panel, right click on our Ethernet connection and select &amp;lsquo;Properties&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-add-the-ip-address-of-the-domain-controller-as-the-preferred-dns-server-of-our-host-machines">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Add the IP address of the Domain Controller as the Preferred DNS server of our host machines." srcset="
/post/creating-an-active-directory-lab-environment/step28_hu_b29a5d952634a0e.webp 400w,
/post/creating-an-active-directory-lab-environment/step28_hu_e76d3a8385fa37da.webp 760w,
/post/creating-an-active-directory-lab-environment/step28_hu_ba75d97af9ecd45a.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step28_hu_b29a5d952634a0e.webp"
width="760"
height="403"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Add the IP address of the Domain Controller as the Preferred DNS server of our host machines.
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Finally, we will connect our host machine to the domain.&lt;/p>
&lt;p>
&lt;figure id="figure-select-access-work-or-school-from-our-start-menu">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Select &amp;lsquo;Access work or school&amp;rsquo; from our start menu" srcset="
/post/creating-an-active-directory-lab-environment/step29_hu_39124e5c1abbfc1d.webp 400w,
/post/creating-an-active-directory-lab-environment/step29_hu_1541bca0387e9942.webp 760w,
/post/creating-an-active-directory-lab-environment/step29_hu_7cf5ca8340d6858e.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step29_hu_39124e5c1abbfc1d.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Select &amp;lsquo;Access work or school&amp;rsquo; from our start menu
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-on-the-settings-menu-that-pops-up-select-connect">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="On the &amp;lsquo;Settings&amp;rsquo; menu that pops up, select &amp;lsquo;Connect&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step30_hu_5146b38f73f53b07.webp 400w,
/post/creating-an-active-directory-lab-environment/step30_hu_9a851383f951c12a.webp 760w,
/post/creating-an-active-directory-lab-environment/step30_hu_2d6d5a24f089c816.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step30_hu_5146b38f73f53b07.webp"
width="760"
height="404"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
On the &amp;lsquo;Settings&amp;rsquo; menu that pops up, select &amp;lsquo;Connect&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-select-join-this-device-to-a-local-active-directory-domain">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Select &amp;lsquo;Join this device to a local Active Directory domain&amp;rsquo;" srcset="
/post/creating-an-active-directory-lab-environment/step31_hu_c4c3457b7e163530.webp 400w,
/post/creating-an-active-directory-lab-environment/step31_hu_657635c5b1acfc89.webp 760w,
/post/creating-an-active-directory-lab-environment/step31_hu_13a64e3a42c7d765.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step31_hu_c4c3457b7e163530.webp"
width="760"
height="403"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Select &amp;lsquo;Join this device to a local Active Directory domain&amp;rsquo;
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-type-in-our-domain-name-we-want-to-connect-to">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Type in our domain name we want to connect to" srcset="
/post/creating-an-active-directory-lab-environment/step32_hu_e63cf6f0f2396907.webp 400w,
/post/creating-an-active-directory-lab-environment/step32_hu_32718d7ec5b548f6.webp 760w,
/post/creating-an-active-directory-lab-environment/step32_hu_b67a9c1bce25f61d.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step32_hu_e63cf6f0f2396907.webp"
width="760"
height="402"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Type in our domain name we want to connect to
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-authenticate-into-the-domain-as-one-of-our-domain-users">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Authenticate into the domain as one of our domain users" srcset="
/post/creating-an-active-directory-lab-environment/step33_hu_615a3b6f2b01af2b.webp 400w,
/post/creating-an-active-directory-lab-environment/step33_hu_3f7b7e551308f2b3.webp 760w,
/post/creating-an-active-directory-lab-environment/step33_hu_4bebabc4c067aedc.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step33_hu_615a3b6f2b01af2b.webp"
width="760"
height="406"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Authenticate into the domain as one of our domain users
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Our host machine will then prompt us to restart, and when we do, we can note that we are now prompted to log into our domain&lt;/p>
&lt;p>
&lt;figure id="figure-upon-reboot-we-can-now-log-in-as-etonra-on-the-sanctuary-domain">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Upon reboot, we can now log in as e.tonra on the SANCTUARY domain" srcset="
/post/creating-an-active-directory-lab-environment/step34_hu_f7c61002e35fb877.webp 400w,
/post/creating-an-active-directory-lab-environment/step34_hu_a82092ac80b47dcb.webp 760w,
/post/creating-an-active-directory-lab-environment/step34_hu_22d19aa85e799ad6.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step34_hu_f7c61002e35fb877.webp"
width="760"
height="400"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Upon reboot, we can now log in as e.tonra on the SANCTUARY domain
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>We can then perform the same steps to set up the other hosts and connect them to the domain.&lt;/p>
&lt;p>
&lt;figure id="figure-our-fully-operational-ad-lab-with-4-vms">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Our fully operational AD lab with 4 VMs" srcset="
/post/creating-an-active-directory-lab-environment/step35_hu_dd36a7d21ea46847.webp 400w,
/post/creating-an-active-directory-lab-environment/step35_hu_662b763193ab668e.webp 760w,
/post/creating-an-active-directory-lab-environment/step35_hu_4c4c380f785fb4ef.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/step35_hu_dd36a7d21ea46847.webp"
width="760"
height="406"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Our fully operational AD lab with 4 VMs
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>I booted up another 5th VM running an instance of Parrot Security and conducted a quick nmap scan on the network&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Nmap 7.91 scan initiated Tue Aug 10 23:54:34 2021 as: nmap -n -sT -sC -oA nmap-generic 192.168.33.0/24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nmap scan report &lt;span class="k">for&lt;/span> 192.168.33.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host is up &lt;span class="o">(&lt;/span>0.0067s latency&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Not shown: &lt;span class="m">996&lt;/span> closed ports
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PORT STATE SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">22/tcp open ssh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> ssh-hostkey:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> &lt;span class="m">1024&lt;/span> 52:9d:3b:c8:e6:69:02:00:db:52:8f:3d:b2:4b:af:94 &lt;span class="o">(&lt;/span>DSA&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>_ &lt;span class="m">1039&lt;/span> 51:59:7f:80:47:b3:18:f0:73:b8:e5:18:01:3c:ea:3d &lt;span class="o">(&lt;/span>RSA&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">23/tcp open telnet
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">80/tcp open http
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>_http-title: Site doesn&lt;span class="err">&amp;#39;&lt;/span>t have a title &lt;span class="o">(&lt;/span>text/html&lt;span class="p">;&lt;/span> &lt;span class="nv">charset&lt;/span>&lt;span class="o">=&lt;/span>utf-8&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1900/tcp open upnp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MAC Address: 7C:8B:CA:C3:D0:94 &lt;span class="o">(&lt;/span>Tp-link Technologies&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nmap scan report &lt;span class="k">for&lt;/span> 192.168.33.111
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host is up &lt;span class="o">(&lt;/span>0.0022s latency&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Not shown: &lt;span class="m">989&lt;/span> filtered ports
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PORT STATE SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">53/tcp open domain
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">88/tcp open kerberos-sec
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">135/tcp open msrpc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">139/tcp open netbios-ssn
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">389/tcp open ldap
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">445/tcp open microsoft-ds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">464/tcp open kpasswd5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">593/tcp open http-rpc-epmap
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">636/tcp open ldapssl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3268/tcp open globalcatLDAP
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3269/tcp open globalcatLDAPssl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MAC Address: 08:00:27:A6:C1:72 &lt;span class="o">(&lt;/span>Oracle VirtualBox virtual NIC&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host script results:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>_nbstat: NetBIOS name: CYDONIA-DC, NetBIOS user: &amp;lt;unknown&amp;gt;, NetBIOS MAC: 08:00:27:a6:c1:72 &lt;span class="o">(&lt;/span>Oracle VirtualBox virtual NIC&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> smb2-security-mode:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> 2.02:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>_ Message signing enabled and required
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> smb2-time:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> date: 2021-08-11T03:55:23
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>_ start_date: N/A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nmap scan report &lt;span class="k">for&lt;/span> 192.168.33.112
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host is up &lt;span class="o">(&lt;/span>0.0014s latency&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Not shown: &lt;span class="m">999&lt;/span> filtered ports
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PORT STATE SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">135/tcp open msrpc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MAC Address: 08:00:27:B5:A5:BA &lt;span class="o">(&lt;/span>Oracle VirtualBox virtual NIC&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nmap scan report &lt;span class="k">for&lt;/span> 192.168.33.113
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host is up &lt;span class="o">(&lt;/span>0.0015s latency&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Not shown: &lt;span class="m">999&lt;/span> filtered ports
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PORT STATE SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">135/tcp open msrpc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MAC Address: 08:00:27:9F:F0:4B &lt;span class="o">(&lt;/span>Oracle VirtualBox virtual NIC&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nmap scan report &lt;span class="k">for&lt;/span> 192.168.33.115
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host is up &lt;span class="o">(&lt;/span>0.0016s latency&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Not shown: &lt;span class="m">999&lt;/span> filtered ports
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PORT STATE SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">135/tcp open msrpc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MAC Address: 08:00:27:D0:AC:7D &lt;span class="o">(&lt;/span>Oracle VirtualBox virtual NIC&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nmap scan report &lt;span class="k">for&lt;/span> 192.168.33.137
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host is up &lt;span class="o">(&lt;/span>0.051s latency&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Not shown: &lt;span class="m">999&lt;/span> closed ports
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PORT STATE SERVICE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">902/tcp open iss-realsecure
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">MAC Address: 44:33:4C:49:F7:BF &lt;span class="o">(&lt;/span>Shenzhen Bilian electronic&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Nmap scan report &lt;span class="k">for&lt;/span> 192.168.33.118
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Host is up &lt;span class="o">(&lt;/span>0.048s latency&lt;span class="o">)&lt;/span>.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">All &lt;span class="m">1000&lt;/span> scanned ports on 192.168.33.118 are closed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Nmap done at Tue Aug 10 23:56:03 2021 -- 256 IP addresses (7 hosts up) scanned in 89.78 seconds&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We can see from this initial scan that the Domain Controller has a number of services running on it. We can thus determine that the network layout of our lab looks like the following.&lt;/p>
&lt;p>
&lt;figure id="figure-network-diagram-of-the-network">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Network Diagram of the network" srcset="
/post/creating-an-active-directory-lab-environment/network_hu_b95effd86728332a.webp 400w,
/post/creating-an-active-directory-lab-environment/network_hu_8dbd0b236833759f.webp 760w,
/post/creating-an-active-directory-lab-environment/network_hu_18dbc7a334aae8bc.webp 1200w"
src="https://averytan.com/post/creating-an-active-directory-lab-environment/network_hu_b95effd86728332a.webp"
width="760"
height="562"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Network Diagram of the network
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>With this AD homelab set up, we now have an entry point to experiment with Windows Active Directory and test out some Active Directory-specific attacks and exploits as well as practise hardening the network and other blue team activities!&lt;/p></description></item><item><title>Bulk Scanning Stuff with VirusTotal</title><link>https://averytan.com/post/bulk-scanning-stuff-with-virustotal/</link><pubDate>Sat, 17 Jul 2021 00:00:00 +0000</pubDate><guid>https://averytan.com/post/bulk-scanning-stuff-with-virustotal/</guid><description>&lt;p>I have a number of files that I have accumulated of unknown and ambiguous origin. I&amp;rsquo;d like to scan these files for any potential malicious contents.&lt;/p>
&lt;p>Virustotal is a website created by Spanish security company Hispasec Sistemas which was acquired by Google. Virustotal aggregates many antivirus products and online scan engines to check for viruses. It allows users to upload files up to 650MB and send suspected malicious URLs for scanning.&lt;/p>
&lt;p>
&lt;figure id="figure-virustotal-webapp">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="virustotal webapp" srcset="
/post/bulk-scanning-stuff-with-virustotal/virustotal-webapp_hu_47f7b8620440044.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/virustotal-webapp_hu_10f9177a6486bf9e.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/virustotal-webapp_hu_300beec4b6bce2e6.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/virustotal-webapp_hu_47f7b8620440044.webp"
width="760"
height="407"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
virustotal webapp
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Looking at all the files I am interested in scanning, we have a total of 287 files that require scanning. It would be beyond tedious to submit each file manually one by one, wait for virustotal to scan it, give back results and then uploading the next file for scanning.&lt;/p>
&lt;p>
&lt;figure id="figure-using-some-bash-magic-to-find-all-the-files">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Using some bash magic to find all the files!" srcset="
/post/bulk-scanning-stuff-with-virustotal/using-find_hu_fb6750918ef966e9.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/using-find_hu_a1024ddb907e6bd1.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/using-find_hu_52a9380fabc7b8ae.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/using-find_hu_fb6750918ef966e9.webp"
width="760"
height="58"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Using some bash magic to find all the files!
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Luckily, virustotal also has an api and command line interface tool that we can use to automate this.&lt;/p>
&lt;p>
&lt;figure id="figure-the-virustotal-command-line-tool-repo">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="The VirusTotal command line tool repo" srcset="
/post/bulk-scanning-stuff-with-virustotal/virustotal-github_hu_34d6f23960987742.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/virustotal-github_hu_de74259cab07da02.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/virustotal-github_hu_113b5e901ac2641d.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/virustotal-github_hu_34d6f23960987742.webp"
width="760"
height="445"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
The VirusTotal command line tool repo
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Inspecting the README file on the repository&lt;/p>
&lt;p>
&lt;figure id="figure-install-instructions">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Install instructions" srcset="
/post/bulk-scanning-stuff-with-virustotal/github-readme_hu_df7b266fad80f6cf.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/github-readme_hu_d6ec24064c7edfa3.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/github-readme_hu_8de7d8fbbdbe758d.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/github-readme_hu_df7b266fad80f6cf.webp"
width="760"
height="624"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Install instructions
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>First we need to download the repository&lt;/p>
&lt;p>
&lt;figure id="figure-cloning-the-repo">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Cloning the repo" srcset="
/post/bulk-scanning-stuff-with-virustotal/installing-cli_hu_2b95f4ef0cf78999.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/installing-cli_hu_d75571b76f24c16.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/installing-cli_hu_1286d4eb0ac61b40.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/installing-cli_hu_2b95f4ef0cf78999.webp"
width="760"
height="237"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Cloning the repo
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>then make&lt;/p>
&lt;p>
&lt;figure id="figure-building-the-tool">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Building the tool" srcset="
/post/bulk-scanning-stuff-with-virustotal/building-cli_hu_b1af7823983d94de.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/building-cli_hu_7315fcc08a333b86.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/building-cli_hu_555fb9f90490fba.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/building-cli_hu_b1af7823983d94de.webp"
width="760"
height="508"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Building the tool
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The documentation mentioned that we need to sign up to get a free apikey in order to use the command line tool. So let&amp;rsquo;s sign up and grab the api key. After signing up, I initialized the command line tool with my api key.&lt;/p>
&lt;p>
&lt;figure id="figure-initializing-vt-with-our-api-key">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Initializing vt with our api key" srcset="
/post/bulk-scanning-stuff-with-virustotal/tool-banner_hu_b3c5c612cfbe6da2.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/tool-banner_hu_8f4b76aa7be2cbf8.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/tool-banner_hu_82b115830d94398e.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/tool-banner_hu_b3c5c612cfbe6da2.webp"
width="760"
height="279"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Initializing vt with our api key
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>VirusTotal processes files in 2 steps. There is an upload step where files are uploaded to VirusTotal and a unique ID is given back to the user. The user can then use this unique ID to query results of the VirusTotal scan. The command line tool has commands for each of these steps.&lt;/p>
&lt;p>We will need a list of all file names and their location on my computer and send this to a file so we have a list of all our files to be scanned.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">find &lt;span class="s1">&amp;#39;/home/s1na/Documents/some/location&amp;#39;&lt;/span> -type f &amp;gt; /home/s1na/Documents/war-room/file-list.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Inspecting this new file, we get a list of all our files we want to scan. We can then read off this file line by line and send a post request to the virustotal api endpoint to get our id.&lt;/p>
&lt;p>
&lt;figure id="figure-a-list-of-our-287-files-to-be-scanned">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="a list of our 287 files to be scanned" srcset="
/post/bulk-scanning-stuff-with-virustotal/list-of-200_hu_ac08f42b77385053.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/list-of-200_hu_1bea43476031982f.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/list-of-200_hu_34199ddd5ed5d0fd.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/list-of-200_hu_ac08f42b77385053.webp"
width="760"
height="353"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
a list of our 287 files to be scanned
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The VirusTotal command line tool has a scan file function that will return us an id that we can then use with the vt analysis command. We can even send in our list of files to it!&lt;/p>
&lt;p>
&lt;figure id="figure-vt-scan-file-command">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="vt scan file command" srcset="
/post/bulk-scanning-stuff-with-virustotal/vt-scan-help_hu_66134b564c2732f4.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/vt-scan-help_hu_9193bf12ab8a32cb.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/vt-scan-help_hu_4d178855ad059666.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/vt-scan-help_hu_66134b564c2732f4.webp"
width="760"
height="417"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
vt scan file command
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Running this and tee-ing the results into a new file. tee outputs results to the screen and also into a seperate file&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">cat ~/Documents/war-room/files-list.txt &lt;span class="p">|&lt;/span> go run main.go scan file - &lt;span class="p">|&lt;/span> tee ~/Documents/war-room/engineering-personal/list-file-id
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Inspecting the new file, we see that our results have given us our list of IDs!&lt;/p>
&lt;p>We can then do some bash processing to take only the IDs and compile them into a separate list to run analysis on them. The structure of the file is a space separated entries of file names and the virustotal ID.&lt;/p>
&lt;p>
&lt;figure id="figure-getting-only-the-ids">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="getting only the IDs" srcset="
/post/bulk-scanning-stuff-with-virustotal/base64-id_hu_3e7cfe146c82d39c.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/base64-id_hu_c9d3c27edec082e.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/base64-id_hu_7734daf77d6ab8f7.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/base64-id_hu_3e7cfe146c82d39c.webp"
width="760"
height="407"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
getting only the IDs
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Let&amp;rsquo;s place these into a new file and running a quick checksum to make sure we have all lines of our code.&lt;/p>
&lt;p>
&lt;figure id="figure-collecting-the-ids-into-a-new-file">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="collecting the IDs into a new file" srcset="
/post/bulk-scanning-stuff-with-virustotal/collecting-id-to-new-file_hu_15c2a53d3509b5cc.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/collecting-id-to-new-file_hu_f5a17be20d01c757.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/collecting-id-to-new-file_hu_6dbee7e6f47887ca.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/collecting-id-to-new-file_hu_15c2a53d3509b5cc.webp"
width="760"
height="94"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
collecting the IDs into a new file
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>let&amp;rsquo;s pull up the analysis command on the tool&lt;/p>
&lt;p>
&lt;figure id="figure-vt-analysis-command">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="vt analysis command " srcset="
/post/bulk-scanning-stuff-with-virustotal/analysis-help_hu_3718f74449eedd88.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/analysis-help_hu_707f16b387f66cd7.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/analysis-help_hu_2014e14fb713e46c.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/analysis-help_hu_3718f74449eedd88.webp"
width="760"
height="458"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
vt analysis command
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>we see that we can pass this command a list just as we did for the file scan command. We&amp;rsquo;ll throw the results into a file aptly named results&lt;/p>
&lt;p>
&lt;figure id="figure-begin-analyzing">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="begin analyzing " srcset="
/post/bulk-scanning-stuff-with-virustotal/analyzing_hu_1f76ac4a0b28fa4c.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/analyzing_hu_3672f1248ad30e74.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/analyzing_hu_262ead8813682d73.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/analyzing_hu_1f76ac4a0b28fa4c.webp"
width="760"
height="308"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
begin analyzing
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Once this completes, we&amp;rsquo;ll need a way to sift through the results in a meaningful way to look for any files that may have been flagged as potentially malicious.&lt;/p>
&lt;p>Upon closer inspection of the results, it is not returned in JSON format. Probing around the tool, I wasn&amp;rsquo;t able to find a way to make it return the results as a JSON object. So we&amp;rsquo;ll have to to do some quick hacking in order to process these results. Note also that this screenshot shows the results for one file entry and we have 287 such entries. The first entry is 532 lines long. I&amp;rsquo;ve also collapsed the results field for brevity.&lt;/p>
&lt;p>
&lt;figure id="figure-a-look-at-the-results">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="a look at the results " srcset="
/post/bulk-scanning-stuff-with-virustotal/json-results_hu_a54e3cec176ff49f.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/json-results_hu_1e75c3c68407f9a9.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/json-results_hu_2d09239265cf4321.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/json-results_hu_a54e3cec176ff49f.webp"
width="760"
height="210"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
a look at the results
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The results field just shows the details of the antivirus scanner used&lt;/p>
&lt;p>
&lt;figure id="figure-the-results-field">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="the results field" srcset="
/post/bulk-scanning-stuff-with-virustotal/results-field_hu_a551aebbc4bec3f2.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/results-field_hu_833c40fa6516df88.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/results-field_hu_848232a40ee71044.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/results-field_hu_a551aebbc4bec3f2.webp"
width="760"
height="678"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
the results field
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The total length of the results file is about ~150 000 lines long! Imagine manually scrolling through the file line by line looking for any malicious or suspicious flags!&lt;/p>
&lt;p>Looking at these results, we are interested in the id field, and the entries within the ‘stats’ field. Particularly, we are interested in any malicious or suspicious entries found.&lt;/p>
&lt;p>We&amp;rsquo;ll use python to perform our analysis on the results. We will use python to read every line from our results file, and when it finds an id field, it will add that id and the 8 &amp;lsquo;stats&amp;rsquo; we are interested in as an entry into a Python Dictionary.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="ch">#!/usr/bin/python3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">f&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;results&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;r&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">#open the file names &amp;#39;results&amp;#39; as read-only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">results_dict&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">#where we will store our final data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">curr_file_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span> &lt;span class="c1">#what current file we are processing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">f&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1">#go through each line in our results file&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">splitted_line&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">#split the line by any whitespace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;-&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span> &lt;span class="c1">#if first element is a -, then this line will give us an id &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">curr_file_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1">#the 3rd element is the id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">#make an entry in our results_dict&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">#if the first element is any of the 8 interesting fields, record the stats&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;confirmed-timeout:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;confirmed-timeout&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;failure:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;failure&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;harmless:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;harmless&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;malicious:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;malicious&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;suspicious:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;suspicious&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;timeout:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;timeout&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;type-unsupported:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;type-unsupported&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;undetected:&amp;#39;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">curr_file_id&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;undetected&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">splitted_line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>this will load only the data we are interested in into a python dictionary called results_dict and allow us to manipulate and ‘query’ this dictionary for any files that may have been flagged as malicious or suspicious.&lt;/p>
&lt;p>we will go through each entry of our results_dict one by one, and check for any malicious or suspicious entries, if we find any, we will add these to an alert_list. Finally, we&amp;rsquo;ll print out the alert list&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Python" data-lang="Python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">alert_list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">#a list that will contain any malicious messages&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">entry&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1">#go through each entry in our processed data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alerts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">#if we have any malicious or suspicious flags, add an error message to alerts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;suspicious&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alerts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">alerts&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">entry&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; has &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;suspicious&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; suspicious flags! &amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;malicious&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alerts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">alerts&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">entry&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; has &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">results_dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">entry&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s1">&amp;#39;malicious&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; malicious flags! &amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">#if alerts has any errors in them, add them to our alert_list&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">alerts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">alert_list&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">alerts&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#print every entry present in alert_list&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">alert_list&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Running the program and boom, we find 2 potentially malicious files. The internet is a scary place.&lt;/p>
&lt;p>
&lt;figure id="figure-2-files-detected-as-malicious">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="2 files detected as malicious" srcset="
/post/bulk-scanning-stuff-with-virustotal/malicious-flagged_hu_bba907860219f825.webp 400w,
/post/bulk-scanning-stuff-with-virustotal/malicious-flagged_hu_b507aa96d64594c3.webp 760w,
/post/bulk-scanning-stuff-with-virustotal/malicious-flagged_hu_feb83263cf46fd7.webp 1200w"
src="https://averytan.com/post/bulk-scanning-stuff-with-virustotal/malicious-flagged_hu_bba907860219f825.webp"
width="760"
height="67"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
2 files detected as malicious
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>This is not a full-proof solution as it could still be very possible that some malicious files have not been submitted or detected by the scanners used by virustotal. Nevertheless we have somewhat increased our confidence, at least a little bit, that besides these 2 files, there is a lower probability that the other files are malicious in nature.&lt;/p></description></item><item><title>Automating HTTP form submissions using Python and cronjobs</title><link>https://averytan.com/post/automating-http-form-submissions-using-python-and-cronjobs/</link><pubDate>Wed, 24 Feb 2021 00:00:00 +0000</pubDate><guid>https://averytan.com/post/automating-http-form-submissions-using-python-and-cronjobs/</guid><description>&lt;p>Nobody likes doing tedious and repetitive tasks. I received an email notifying me that I had to submit a daily ‘health declaration check’ as a form on a website. Since I lead a boring and routine lifestyle, I thought I would automate this task. It would be a good excuse to refresh my knowledge on Python&amp;rsquo;s Requests module as well as get more hands on experience with cron jobs.&lt;/p>
&lt;p>
&lt;figure id="figure-the-health-declaration-webform">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="health declaration form" srcset="
/post/automating-http-form-submissions-using-python-and-cronjobs/health-declaration-form_hu_95d3c5159bb8e12c.webp 400w,
/post/automating-http-form-submissions-using-python-and-cronjobs/health-declaration-form_hu_b0c3fc65d6749692.webp 760w,
/post/automating-http-form-submissions-using-python-and-cronjobs/health-declaration-form_hu_dee2ab38f08e4be1.webp 1200w"
src="https://averytan.com/post/automating-http-form-submissions-using-python-and-cronjobs/health-declaration-form_hu_95d3c5159bb8e12c.webp"
width="760"
height="507"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
The health declaration webform
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>
&lt;figure id="figure-inspecting-the-web-form-using-firefox-developer-tools">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Inspecting the web form using Firefox developer tools" srcset="
/post/automating-http-form-submissions-using-python-and-cronjobs/webform-inspection_hu_c25807ac1d51eb43.webp 400w,
/post/automating-http-form-submissions-using-python-and-cronjobs/webform-inspection_hu_ac1838a1234a1cd9.webp 760w,
/post/automating-http-form-submissions-using-python-and-cronjobs/webform-inspection_hu_b80c875478feb494.webp 1200w"
src="https://averytan.com/post/automating-http-form-submissions-using-python-and-cronjobs/webform-inspection_hu_c25807ac1d51eb43.webp"
width="760"
height="198"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Inspecting the web form using Firefox developer tools
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Using the built-in Firefox dev tools, we see that the web form consist of a pretty standard HTML form tag sent via a HTTP POST request.&lt;/p>
&lt;p>Web traffic travels in requests and responses. HTTP Requests are sent out by hosts (your browser or computer) for specific web resources, which are then answered by HTTP Responses that provide the web resource requested. Requests come in multiple flavours but the most common requests are GET requests and POST requests. GET requests are HTTP requests usually sent to ‘get’ or retrieve information from the web while POST requests are are used to send or &amp;lsquo;post&amp;rsquo; data to the internet (this could be adding a blog post, making a social media comment, or in our case, submitting a web form)&lt;/p>
&lt;p>
&lt;figure id="figure-inspecting-the-post-request-with-burp-suite">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Inspecting the POST request with Burp Suite" srcset="
/post/automating-http-form-submissions-using-python-and-cronjobs/inspecting-post-request_hu_feb119bb2088a3ad.webp 400w,
/post/automating-http-form-submissions-using-python-and-cronjobs/inspecting-post-request_hu_16082b348d78cd4b.webp 760w,
/post/automating-http-form-submissions-using-python-and-cronjobs/inspecting-post-request_hu_89add62c245bdc6c.webp 1200w"
src="https://averytan.com/post/automating-http-form-submissions-using-python-and-cronjobs/inspecting-post-request_hu_feb119bb2088a3ad.webp"
width="760"
height="464"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
Inspecting the POST request with Burp Suite
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>POST Requests are separated into 2 sections called the Head, which contain Headers, and the Body. For this particular request, the Header takes up the first 15 lines of the request and the body of our request begins on line 17.&lt;/p>
&lt;p>We can see where each of the fields in the form correspond to the fields in line 17 of the request. We can thus write some code to modify the contents submittied in line 17 depending on day of the week and where I will be on that day.&lt;/p>
&lt;p>Now that we have some idea of what a valid HTTP request and corresponding response looks like, we can use Python and its request module to craft and send our own custom HTTP POST request to help submit this form for us automatically.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-Python" data-lang="Python">&lt;span class="line">&lt;span class="cl">&lt;span class="ch">#!/usr/bin/python3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">requests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">URL&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://some-endpoint/somewhere/admin-ajax.php&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">headers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;user-agent&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;accept&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;application/json, text/javascript, */*; q=0.01&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;accept-language&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;en-US,en;q=0.5&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;accept-encoding&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;gzip, deflate&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;content-type&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;application/x-www-form-urlencoded; charset=UTF-8&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;x-requested-with&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;XMLHttpRequest&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;content-length&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="s1">&amp;#39;661&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;origin&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;https://some.origin.com&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;DNT&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;1&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;Connection&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;close&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;cookie&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;TawkConnectionTime=0&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s1">&amp;#39;sec-gpc&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s1">&amp;#39;1&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">usual_places&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;home&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Home&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Era&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">1&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Past&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">2&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Cashflow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">3&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Dreams-of-flight&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">4&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Stallion-Cooper-Coffee&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="mi">5&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Ministop&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">url_encoded_comma&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;%2C&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">today&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">day&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">day&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;0&amp;#39;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">day&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">day&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">day&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">month&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">today&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">month&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">month&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">month&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;0&amp;#39;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">month&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">month&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">month&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">home&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">usual_places&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;home&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">places&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">usual_places&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">url_encoded_comma&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="s1">&amp;#39;+&amp;#39;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">usual_places&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;action=formcraft3_form_submit&amp;amp;field1%5B%5D=T&lt;/span>&lt;span class="si">%E&lt;/span>&lt;span class="s2">1%BB%9C+KHAI+Y+T&lt;/span>&lt;span class="si">%E&lt;/span>&lt;span class="s2">1%BA%BE+H%C3%80NG+NG%C3%80Y&lt;/span>&lt;span class="si">%2F&lt;/span>&lt;span class="s2">+DAILY+HEALTH+DECLARATION+FORM+2021&amp;amp;field19=&amp;amp;field3%5B%5D=Avery+Tan&amp;amp;field20=+&amp;amp;field14=&amp;#34;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">day&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">%2F&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">month&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">%2F&lt;/span>&lt;span class="s2">2021&amp;amp;field6%5B%5D=B%C3%ACnh+th%C6%B0&lt;/span>&lt;span class="si">%E&lt;/span>&lt;span class="s2">1%BB%9Dng&lt;/span>&lt;span class="si">%2F&lt;/span>&lt;span class="s2">Normal&amp;amp;field25%5B%5D=&amp;amp;field9%5B%5D=&amp;#34;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">places&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="s2">&amp;#34;&amp;amp;field21=&amp;#34;&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">home&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="s2">&amp;#34;&amp;amp;field30%5B%5D=Kh%C3%A1c&lt;/span>&lt;span class="si">%2F&lt;/span>&lt;span class="s2">Others&amp;amp;field32%5B%5D=&amp;amp;field33%5B%5D=&amp;amp;field34%5B%5D=&amp;amp;field35%5B%5D=&amp;amp;field36%5B%5D=I+use+Grab&amp;amp;field18%5B%5D=I+agree&amp;amp;field28=&amp;amp;website=&amp;amp;id=51&amp;amp;location=https%3A&lt;/span>&lt;span class="si">%2F%2F%2F%2F&lt;/span>&lt;span class="s2">&amp;amp;emails=&amp;amp;hidden=field25, field32, field33, field34, field35&amp;amp;redirect=&amp;amp;type=all&amp;amp;triggerIntegration=undefined&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">requests&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">URL&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">headers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">headers&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># for logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">time_now&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">time_now&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;. Request sent. Response is &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">r&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">f&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/home/kali/Documents/health-automated/health-logs&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;a+&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">f&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">time_now&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;. Request sent. Response is &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">r&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">f&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I map the headers to a Python dictionary and also use another dictionary to record my places of interests.
I perform some logic to ensure that the date is submitted in a format that the form expects it to be.
Finally, I add these variables into my data string that will form the body of our POST request before finally sending out the request.&lt;/p>
&lt;p>Now that we have our simple Python script, we now have to schedule a cronjob to automatically run the script every day.&lt;/p>
&lt;p>Cron jobs are a unix time-based job scheduler where Users can set up and schedule jobs to run periodically at fixed times, dates, or intervals.&lt;/p>
&lt;p>In order for a scheduled cronjob to be run at the set time, the computer needs to be running and connected to the internet during the scheduled job for it to be executed. Instead of running the cronjob from my laptop and having to ensure it is on and connected at the scheduled time of execution, I am going to use the deth* (pronounced deathstar), my raspberry pi 4 server which I have hooked into the back of a tv in my room. It&amp;rsquo;s a kali machine which I use for lightweight, in-depth, thorough scans where leaving my laptop running for 8hours is no option.&lt;/p>
&lt;p>I use Dataplicity, which offers a VPN-like service to connect remotely into your Raspberry Pi without having to set up a portfoward or deal with any dynamic DNS services. Using Dataplicity, I can remotely access deth* wherever I have internet.&lt;/p>
&lt;p>
&lt;figure id="figure-deth-my-raspberry-pi-4-server">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="deth*" srcset="
/post/automating-http-form-submissions-using-python-and-cronjobs/Featured_hu_3fa0813c32d2a8b.webp 400w,
/post/automating-http-form-submissions-using-python-and-cronjobs/Featured_hu_c4be84905d3084b8.webp 760w,
/post/automating-http-form-submissions-using-python-and-cronjobs/Featured_hu_d2aa00608cb2d73b.webp 1200w"
src="https://averytan.com/post/automating-http-form-submissions-using-python-and-cronjobs/Featured_hu_3fa0813c32d2a8b.webp"
width="760"
height="485"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
deth*, my raspberry pi 4 server
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>Cronjobs can be scheduled on a per-user basis via something called crontab. To access the user-specific crontab, we do so via:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">crontab -e
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>
&lt;figure id="figure-crontab">
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="crontab" srcset="
/post/automating-http-form-submissions-using-python-and-cronjobs/crontab_hu_fb5677cffbb5fb6a.webp 400w,
/post/automating-http-form-submissions-using-python-and-cronjobs/crontab_hu_afe91496cfedbb7a.webp 760w,
/post/automating-http-form-submissions-using-python-and-cronjobs/crontab_hu_653ace778a4115e0.webp 1200w"
src="https://averytan.com/post/automating-http-form-submissions-using-python-and-cronjobs/crontab_hu_fb5677cffbb5fb6a.webp"
width="760"
height="414"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;figcaption>
crontab
&lt;/figcaption>&lt;/figure>
&lt;/p>
&lt;p>The syntax of a cronjob command is 5 optional parameters followed by the command you want to run at the specified time.
I&amp;rsquo;m running my cronjob at the start of the 50th minute of the 2nd hour of the day, or at 02 50 UTC time, which is about 9.50am local time.&lt;/p>
&lt;p>I also redirect output from the python file to logs so that I can check periodically to make sure the cronjob is running as expected.&lt;/p>
&lt;p>With this done, I can rest easy knowing that my forms are being submitted in a automated non-intervention-requiring manner!&lt;/p></description></item><item><title>Creating a swapfile for Linux machines running a btrfs filesystem</title><link>https://averytan.com/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/</link><pubDate>Thu, 11 Feb 2021 00:00:00 +0000</pubDate><guid>https://averytan.com/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/</guid><description>&lt;p>I have an i5 Huawei Matebook with 4GB RAM. This isn’t the worst specs for most things I need to do day to day (save for something like maybe intense password cracking). However, I also run virtualbox on my computer to host an instance of Parrot OS that I use for other purposes and I have noticed performance issues which at times would force me to do a hard shutdown of the whole computer.&lt;/p>
&lt;p>Swap is a space on a disk that is used when a computer’s physical RAM is full. It’s a way to use harddisk space in order to simulate extra RAM on a computer. When a computer’s RAM is full, idle programs are copied over to the swapfile to free up RAM memory.&lt;/p>
&lt;p>When I initially set up my native Parrot OS on my computer, I was given the option to create separate partitions for a swap disk. The alternative setting was to settle for a swap file (I have since learnt that using a swap file is more desirable compared to a swap disk). Being the Linux noob that I was (and still probably am), I did not want to mess around too much with partitions on the hard disk.&lt;/p>
&lt;p>Only recently have I found out, by running the command swapon –show that a swapfile was not automatically created during the initial installation of Linux. A further obstacle presented itself when I was attempting to provision a swapfile in that the harddisk on my computer was using the
. At the point of enabling the swapfile, I was getting constant Invalid Argument errors.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="swapon error" srcset="
/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/error-1_hu_54bf626d70be6fb1.webp 400w,
/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/error-1_hu_8738383c3686143c.webp 760w,
/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/error-1_hu_6e9d60911bacca37.webp 1200w"
src="https://averytan.com/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/error-1_hu_54bf626d70be6fb1.webp"
width="760"
height="91"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>A bit of further research and I found out the required extra steps to configuring a swapfile. The steps needed to be taken to create a swapfile on a btrfs harddisk is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">touch /swapfile
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We’ll create the swapfile at the root directory.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">chattr +C /swapfile
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is the magic command that fixed the Invalid argument error I was getting when trying to enable my swapfile previously. This command is asking for COW (a btrfs-specific resource management technique) to be disabled.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">sudo fallocate -l 4G /swapfile
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In my case, I’m creating a swapfile of size 4GB&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">chmod &lt;span class="m">0600&lt;/span> /swapfile
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Change the file permissions to be read and write only by root.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">mkswap /swapfile
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>set up this directory as a swap area&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">swapon /swapfile
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>enable the swapfile&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">swapon --show
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>check that your swapfile is enabled and how much of it is being used at the moment&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="checking swap" srcset="
/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/checking-swap_hu_b3dc0dfe2bbc4e3f.webp 400w,
/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/checking-swap_hu_16a611a8e57a9313.webp 760w,
/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/checking-swap_hu_86e7e8e8969a3e61.webp 1200w"
src="https://averytan.com/post/creating-a-swapfile-for-linux-machines-running-a-btrfs-filesystem/checking-swap_hu_b3dc0dfe2bbc4e3f.webp"
width="760"
height="179"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>Do not forget to set a swappiness value. Swappiness can be set between values of 0 to 100 and determine how often or how aggressively the computer system will make use of the swapfile. A lower value means the kernel avoids swapping as much as possible while a high value will allow the kernel to more liberally make use of the swapfile. By default swappiness is set to 60. There are circles of engineers discouraging the use of frequent read and write operations into SSD harddrives due to degradation concerns. While I disagree in terms of the degree of impact these read and write operations during swapping will actually cause, and have a high level of confidence in our modern harddrive technologies, I set my swappiness to a level of 10. (No need to use the space if you actually don’t need it right)&lt;/p>
&lt;p>To do this, add&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">vm.swappiness &lt;span class="o">=&lt;/span> &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>to /etc/sysctl.conf.&lt;/p>
&lt;p>This can also be temporarily set by using&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">sudo sh -c &lt;span class="s1">&amp;#39;echo 10 &amp;gt; /proc/sys/vm/swappiness&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>to temporarily change the swappiness value. The changes will not persist after reboot though.&lt;/p>
&lt;p>After setting up a swapfile, I have noticed increased performance and reliability. I have not had to perform a hard shutdown of my PC since. It’s never run as smoothly and has rid me of the frustration of having to reboot the VM, losing all work and worse yet when I have to reboot not only the VM but the entire system.&lt;/p>
&lt;p>Using a swapfile, I turned my machine with 4GB of RAM into a machine with 8GB of RAM, and I didn’t have to spend a single dollar on RAM sticks.&lt;/p></description></item><item><title>An example preprint / working paper</title><link>https://averytan.com/publicationcopy/preprint/</link><pubDate>Sun, 07 Apr 2019 00:00:00 +0000</pubDate><guid>https://averytan.com/publicationcopy/preprint/</guid><description>&lt;p>This work is driven by the results in my
on LLMs.&lt;/p>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Create your slides in Markdown - click the &lt;em>Slides&lt;/em> button to check out the example.&lt;/span>
&lt;/div>
&lt;p>Add the publication&amp;rsquo;s &lt;strong>full text&lt;/strong> or &lt;strong>supplementary notes&lt;/strong> here. You can use rich formatting such as including
.&lt;/p></description></item><item><title>An example journal article</title><link>https://averytan.com/publicationcopy/journal-article/</link><pubDate>Tue, 01 Sep 2015 00:00:00 +0000</pubDate><guid>https://averytan.com/publicationcopy/journal-article/</guid><description>&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Click the &lt;em>Cite&lt;/em> button above to demo the feature to enable visitors to import publication metadata into their reference management software.&lt;/span>
&lt;/div>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Create your slides in Markdown - click the &lt;em>Slides&lt;/em> button to check out the example.&lt;/span>
&lt;/div>
&lt;p>Add the publication&amp;rsquo;s &lt;strong>full text&lt;/strong> or &lt;strong>supplementary notes&lt;/strong> here. You can use rich formatting such as including
.&lt;/p></description></item><item><title>An example conference paper</title><link>https://averytan.com/publicationcopy/conference-paper/</link><pubDate>Mon, 01 Jul 2013 00:00:00 +0000</pubDate><guid>https://averytan.com/publicationcopy/conference-paper/</guid><description>&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Click the &lt;em>Cite&lt;/em> button above to demo the feature to enable visitors to import publication metadata into their reference management software.&lt;/span>
&lt;/div>
&lt;div class="flex px-4 py-3 mb-6 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-600 dark:text-primary-300">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Create your slides in Markdown - click the &lt;em>Slides&lt;/em> button to check out the example.&lt;/span>
&lt;/div>
&lt;p>Add the publication&amp;rsquo;s &lt;strong>full text&lt;/strong> or &lt;strong>supplementary notes&lt;/strong> here. You can use rich formatting such as including
.&lt;/p></description></item><item><title>Blog</title><link>https://averytan.com/blog/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://averytan.com/blog/</guid><description/></item></channel></rss>