<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Insecure By Design</title>
        <link>https://misczak.github.io/blog</link>
        <description>A blog about cloud security, home labbing, and various other tech topics.</description>
        <lastBuildDate>Fri, 23 Jan 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Building Out The HomeLab: Photos with Immich]]></title>
            <link>https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich</link>
            <guid>https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich</guid>
            <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A write-up about my new self-hosted photo service built on Immich.]]></description>
            <content:encoded><![CDATA[<p>In my post last month about <a href="https://www.misczak.com/posts/rethinking-my-personal-tech-stack/" target="_blank" rel="noopener noreferrer" class="">rethinking my personal tech stack</a>, I had mentioned that I had been using <a href="https://ente.io/" target="_blank" rel="noopener noreferrer" class="">Ente Photos</a> as an alternative to Google Photos, but had also been evaluating Immich. I also mentioned that one day in the future I might move over to Immich after figuring out a better backup strategy for my HomeLab.</p>
<p>That day came this month as I had some time to figure out the last few items I needed to feel secure in using Immich as my primary photo platform, running entirely on my Beelink S12 Pro running Proxmox.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="requirements">Requirements<a href="https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich#requirements" class="hash-link" aria-label="Direct link to Requirements" title="Direct link to Requirements" translate="no">​</a></h2>
<p>Before I had kids, my photo library would fit into the free tier of basically any cloud photo service that is out there. Afterwards, it's an entirely different story - I now have thousands of photos and videos of my kids through their first few years of life. I consider this collection to be one of the most critical pieces of data I have, far surpassing old college papers and the blog posts I cobble together for this site Therefore, making sure I am being careful to protect against data corruption, data loss, and accidental public exposure is of paramount importance. The last one in particular is pretty critical, as protecting photos and videos of my kids from being used to train LLM models is one of the reasons I was looking to get away from Google Photos to begin with!</p>
<p>When looking to fully self-host my photos, I had three main requirements I felt like I had to satisfy before I felt comfortable moving away from Ente:</p>
<ol>
<li class="">My photo collection cannot, under any circumstance, be exposed to the public Internet or used for model training/advertising/tracking by a company. For the purposes of facial detection, a model hosted entirely on my own devices is acceptable.</li>
<li class="">The solution needs to be able to scale to meet my increasing storage needs in a cost effective manner, as I only see myself taking more photos over the years. I really don't like how most cloud photo services automatically send you up to the 1 or 2 terabyte tiers once you exceed 200 GB.</li>
<li class="">I want to be able share individual photos/videos as well as certain albums with people who do not use the same app as me and do not have access to my home network.</li>
</ol>
<p>Of these, requirement two is easily met by any service that I can self-host at home. It's requirements 1 and 3 that become much more difficult to meet while attempting to match the functionality that more popular services offer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="setting-up-immich">Setting Up Immich<a href="https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich#setting-up-immich" class="hash-link" aria-label="Direct link to Setting Up Immich" title="Direct link to Setting Up Immich" translate="no">​</a></h2>
<p>I looked up the installation instructions in Immich's excellent <a href="https://docs.immich.app/install/requirements" target="_blank" rel="noopener noreferrer" class="">documentation</a> and saw that the recommended approach was through Docker compose. I could set it up in a Linux Container (LXC) in Proxmox, but I decided to go for the well supported path and instead provisioned an Ubuntu VM with a static IP address, 2 CPU, 4 GB of RAM, and 40 GB of storage space. My goal is to use my Synology NAS as the storage for all of my photos and just mount the photos share to the VM, so I only need some storage in the VM itself for container images and other files. Once the VM was created, I authorized its static IP to connect to my currently empty NFS photos share on Synology and mounted it in <code>/etc/fstab</code>.</p>
<p>I then built my docker compose file using the <a href="https://docs.immich.app/install/docker-compose" target="_blank" rel="noopener noreferrer" class="">instructions</a> on the Immich, taking care to point my photo upload location to the <code>/mnt/photos</code> directory that was the mountpoint for my NFS photos share, then ran <code>docker compose up</code> to bring up the service on my network.</p>
<p>Once it was running, I made an account for myself and installed the mobile app on my phone. However, I didn't login on mobile yet; instead, I wanted to do a bulk import of all of my photos before connecting my phone.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="importing-existing-photos">Importing Existing Photos<a href="https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich#importing-existing-photos" class="hash-link" aria-label="Direct link to Importing Existing Photos" title="Direct link to Importing Existing Photos" translate="no">​</a></h2>
<p>Immich is a little different than most photo services in that it supports two options of adding photos to your collection - as uploads through your own account, or attached as an <a href="https://docs.immich.app/features/libraries#import-paths" target="_blank" rel="noopener noreferrer" class="">external library</a>. It would have been easy to just include my existing photos as an external library, but this would create a weird division between photos I had before installing Immich and photos I took after installing it, which would be uploaded through the mobile app and added to my regular library.</p>
<p>Instead, I wanted to do a full import of all of my existing photos through my actual user account, as if Immich was ingesting them naturally. However, downloading all of my photos to my phone and then uploading them through Immich would take forever. Fortunately, there is a great utility called <a href="https://docs.immich.app/features/libraries#import-paths" target="_blank" rel="noopener noreferrer" class="">immich-go</a> that can take a Google Takeout file (or multiple files) and import them into your Immich library using an API key you generate in Immich's administration page. This turned out to be an incredibly fast and effective way of uploading nearly 200 GB of photos and videos into Immich without any errors or corruption issues. As part of the upload, it ended up writing my photos to the photos share on my Synology NAS, which is exactly where I wanted it.</p>
<p>Once that was complete, I logged in through the mobile app and enabled mobile backup from the Recents album on my phone. Since all of my photos had already been imported into Immich, it didn't have to upload anything from my phone, bringing both endpoints in sync.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="immichs-jobs">Immich's Jobs<a href="https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich#immichs-jobs" class="hash-link" aria-label="Direct link to Immich's Jobs" title="Direct link to Immich's Jobs" translate="no">​</a></h2>
<p>Inside the Immich administration page are a number of jobs that get run when adding photos to your library or at regular intervals (such as overnight, when most people wouldn't be using the system). These jobs include grouping faces into people for easier identification across photos, analyzing text in images, and transcoding videos for better device compatibility. After the initial import, some of these jobs took a while to run, so I didn't ask too much of Immich for the next day or so.</p>
<p><a href="https://misczak.github.io/assets/files/immichjobs-682c26d992656d9e9953b87d044cf20d.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="A few of Immich&amp;#39;s Jobs " src="https://misczak.github.io/assets/images/immichjobs-682c26d992656d9e9953b87d044cf20d.png" width="905" height="1120" class="img_ev3q"></a></p>
<p>If you have a GPU in the machine that you are running Immich on, you can use that for the machine learning container and chew through these jobs much quicker. I initially passed the GPU from my Beelink through to this VM to handle the initial import, then removed it so my other LXCs (like Plex) could use it instead. In the future, I may move the machine learning container to my desktop PC so it can use my fully powered Nvidia GPU there instead.</p>
<p>Once these jobs were done, I turned on Immich's Storage Template feature to better sort my photos on my NFS share by date. You can get pretty deep into the customization here, but the default format worked for me - I just wanted to be able to easily drill down into a given year or month on my NAS if need be. After configuring that, I ran the Storage Template Migration job to allow Immich to re-organize the photos I had already uploaded.</p>
<p><a href="https://misczak.github.io/assets/files/storagetemplate-64267d0d01087d60c2019340900bc0d7.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Storage Template Configuration " src="https://misczak.github.io/assets/images/storagetemplate-64267d0d01087d60c2019340900bc0d7.png" width="1009" height="496" class="img_ev3q"></a></p>
<p>I also tested that my preexisting <a href="https://www.misczak.com/posts/building-out-the-homelab-proxmox-and-tailscale/#tailscale" target="_blank" rel="noopener noreferrer" class="">Tailscale LXC setup</a> allowed me access to my server when outside the house without exposing it to the public Internet. I turned off wifi on my phone, connected to my tailnet, and tried to access Immich through the mobile app on my phone. It worked just as planned!</p>
<p>At this point, I felt like I had met requirement 1 - Immich was up and running locally, with a local facial recognition model sorting photos into people.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="public-sharing">Public Sharing<a href="https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich#public-sharing" class="hash-link" aria-label="Direct link to Public Sharing" title="Direct link to Public Sharing" translate="no">​</a></h2>
<p>With my first two requirements met, I set about figuring out how to share photos, videos, and albums from my Immich library with external/public users. There is a whole focused discussion about this on the Immich discord, where users debate the best way of exposing Immich without jeopardizing the privacy of their photos.</p>
<p>Immich has the ability to generate links for selected photos/videos or an entire album, which I would then distribute to people so they are able to view my media. This link needs to be publicly routable for them to be able to view the content; if my Immich server is running off an internal IP or DNS record (something like 192.168.x.x), it's not going to work.</p>
<p>One option is that I could just expose Immich behind a reverse proxy to the entire Internet, but that opens up the potential for a single security flaw in Immich to expose my library. Fortunately, somebody had already recognized this problem and created something called <a href="https://github.com/alangrainger/immich-public-proxy" target="_blank" rel="noopener noreferrer" class="">Immich Public Proxy</a>, which acts as an intermediary that I can expose to the public internet since it has no credentials or sensitive paths itself. Instead, all it can do is map external requests to internal API calls in Immich.</p>
<p>This looks great, but now I needed to be able to expose Immich Public Proxy to the public internet. I could open ports on my router itself, as it only requires a single one - but I prefer not to do this if at all possible. Instead, I opted to use a feature from Cloudflare called <a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/" target="_blank" rel="noopener noreferrer" class="">Cloudflare Tunnels</a> which allows me to place a daemon in my internal infrastructure that creates outbound connections to Cloudflare's network to route traffic as mapped in your Cloudflare DNS records.</p>
<p>Both Immich Public Proxy and the Cloudflare Tunnels daemon (cloudfared) can be added to a Docker network, so I added a bit more to my Docker Compose file for Immich:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">immich-public-proxy</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">container_name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> immich</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">public</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">proxy</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> alangrainger/immich</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">public</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">proxy</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">latest</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">restart</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> always</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">ports</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"3000:3000"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">environment</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">PUBLIC_BASE_URL</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">IMMICH_SHARE_URL</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">IMMICH_URL</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> http</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">//immich</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">server</span><span class="token punctuation" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">2283</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">healthcheck</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">test</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> curl </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">s http</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">//localhost</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">3000/share/healthcheck </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">o /dev/null </span><span class="token punctuation" style="color:#393A34">|</span><span class="token punctuation" style="color:#393A34">|</span><span class="token plain"> exit 1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">start_period</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 10s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token key atrule" style="color:#00a4db">timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> 5s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">tunnel</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">container_name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> cloudflared</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> cloudflare/cloudflared</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">restart</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> unless</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">stopped</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">command</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> tunnel run</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">env_file</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> .env</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">environment</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> TUNNEL_TOKEN=$</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">TUNNEL_TOKEN</span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>First, I'm passing in some references that are used to configure these services - the public share base URL I'll be using in my Cloudflare DNS records for shareable links, and the token generated by Cloudflare when creating a tunnel. Then it's a pretty straightforward configuration that points Immich Public Proxy at my existing Immich server. On the Cloudflare side, I configured the tunnel so that a CNAME record for a subdomain gets routed to my Immich Public Proxy container and port that now runs on the same Docker network as cloudflared.</p>
<p><a href="https://misczak.github.io/assets/files/cloudflaretunnel-50608154f3a21e45a9dd32885a3b7d72.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Configuring the Cloudflare Tunnel " src="https://misczak.github.io/assets/images/cloudflaretunnel-50608154f3a21e45a9dd32885a3b7d72.png" width="950" height="560" class="img_ev3q"></a></p>
<p>There's one last step I had to do inside Immich itself - in Administration &gt; Settings &gt; Server Settings, I configured the external domain with the CNAME entry I had just hooked up to the tunnel route in Cloudflare. Once that's done, any shareable link I generate in Immich should be able to be routed via the Cloudflare tunnel to my internal Immich instance, but without exposing any ports on my router or making any part of the Immich server itself publicly reachable.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="backups">Backups<a href="https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich#backups" class="hash-link" aria-label="Direct link to Backups" title="Direct link to Backups" translate="no">​</a></h2>
<p>The last thing I had to resolve was how to back all of this up in a way that made me feel confident that I could use it as my sole photo service moving forward. Immich has a great <a href="https://docs.immich.app/administration/backup-and-restore" target="_blank" rel="noopener noreferrer" class="">documentation page about backups</a>, and even just added the ability to do restores through its web UI. In essence, there's two main things you need to worry about when backing up Immich:</p>
<ol>
<li class="">The database that Immich uses to underpin the server</li>
<li class="">All of your photos and videos themselves</li>
</ol>
<p>Immich has a built in job to create database backups, storing them in $UPLOAD_LOCATION/backups. It creates several of these over the course of ten days since they are pretty small. Combined with your actual photos and videos, this is everything you would need to restore Immich should you encounter disaster.</p>
<p>Since Immich is just another VM on my Proxmox host, I configured Proxmox to back up my Immich VM every day to a secondary disk in the mini PC itself. Then, once a week I make a backup to my Synology NAS in a backups NFS share as well. This means that I always have a semi-recent database backup on my NAS, alongside my photos and videos.</p>
<p>From there, I ended up signing up for Synology C2 to encrypt and backup these database backups as well as my actual photos and videos to the cloud. I had compared Synology C2 to Backblaze and other backup providers, and while Synology is a little more expensive for my current amount of storage needs, I really liked how well integrated it was and how easy it is to roll back to previous versions of a file if I need to.</p>
<p><a href="https://misczak.github.io/assets/files/synologyc2restore-777dffd4b46506506b9517cede99ad02.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Synology C2 Restore Wizard in Hyper Backup " src="https://misczak.github.io/assets/images/synologyc2restore-777dffd4b46506506b9517cede99ad02.png" width="1118" height="703" class="img_ev3q"></a></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://misczak.github.io/blog/building-out-the-homelab-photos-with-immich#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>I've written a lot of words about how I got this running, without actually mentioning how I find the Immich software - but it's quite good! They have put in a ton of work over 2025 and now 2026 to get it a state that I think is pretty close to Google Photos. Most importantly, I've been able to cut paying a subscription fee for my photos and have plenty of runway in my NAS as my storage needs grow. The tradeoff is that I'm now paying for a backup of my backup with Synology C2, but that also allows me to back up a lot more than just photos for the same cost.</p>]]></content:encoded>
            <category>Homelab</category>
        </item>
        <item>
            <title><![CDATA[Building Out The HomeLab: Proxmox and Tailscale]]></title>
            <link>https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale</link>
            <guid>https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale</guid>
            <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Talking about how I setup Proxmox and Tailscale to form the foundational pieces of my new homelab.]]></description>
            <content:encoded><![CDATA[<p>Several years ago, I bought a Synology DS920+ to use as a network attached storage (NAS) appliance in the house. I don't work as a photographer or video editor so my work doesn't really have any massive storage needs; rather, I wanted someplace to host media for the family that didn't require an ongoing subscription service to access. For a few years, it served that purpose almost exclusively, both as a drive I could mount and as a Plex server.</p>
<p>Recently, I had begun tinkering with a few other services and seeing how far I could push my Synology. It has a Container Manager service that allows you to run Docker containers, although the interface is very GUI-driven and there's always a few gotchas. For example, Synology reserves port 80 and 443 for their own web station and reverse proxy services, which makes it difficult to run your own reverse proxy on the device.</p>
<p>For these reasons, I started to consider buying a separate device to act as a home server and repository for all my containers that I would have full control over, and relegate the Synology back to its role as just dumb storage.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="choosing-a-mini-pc">Choosing a Mini PC<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#choosing-a-mini-pc" class="hash-link" aria-label="Direct link to Choosing a Mini PC" title="Direct link to Choosing a Mini PC" translate="no">​</a></h2>
<p>I had used Raspberry Pi's <a href="https://www.misczak.com/posts/tracking-temperature-and-humidity-at-home-with-time-series-data/" target="_blank" rel="noopener noreferrer" class="">here and there</a> over the years for little hobbyist projects, but the prices on newer models have increased to the point where they are getting pretty close to proper PCs. I ended up going with a <a href="https://www.amazon.com/dp/B0G62PNXZB" target="_blank" rel="noopener noreferrer" class="">Beelink Mini S12 Pro</a>, which packs an Intel Alder Lake N100 with 16 GB of memory and a 500GB NVMe SSD. I wanted the Intel CPU to support hardware transcoding for Plex, and the idle power draw for the entire device is really low (somewhere between 6 to 10 Watts).</p>
<p>Having read some reviews and impressions from previous owners of the S12 Pro, it seems like the quality of the SSD that it comes with is questionable. I was probably going to want to upgrade the storage anyway, so I also ordered a <a href="https://www.amazon.com/dp/B0DBR3DZWG" target="_blank" rel="noopener noreferrer" class="">Kingston 1TB NVMe SSD</a> to replace it with. The S12 Pro also has an additional connector for a 2.5" SATA drive, so I grabbed a <a href="https://www.amazon.com/dp/B07YD579WM" target="_blank" rel="noopener noreferrer" class="">Crucial 1TB SATA SSD</a> as well.</p>
<p>When all the pieces arrived, I opened up the mini PC, swapped out the NVMe drive with a single screw, connected and screwed in the SATA drive, then put the cover back together and powered it on.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="proxmox">Proxmox<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#proxmox" class="hash-link" aria-label="Direct link to Proxmox" title="Direct link to Proxmox" translate="no">​</a></h2>
<p>Before buying the Beelink, I had put some thought into what I wanted the host OS to be. At one point, I was considering just throwing Ubuntu on there and running something like <a href="https://coolify.io/docs/" target="_blank" rel="noopener noreferrer" class="">Coolify</a> to deploy some things. In the end, I settled on <a href="https://www.proxmox.com/en/" target="_blank" rel="noopener noreferrer" class="">Proxmox</a> for a few reasons:</p>
<ol>
<li class="">I really like the virtualization approach and the flexibility of using both virtual machines and Linux containers (LXCs) on the same platform.</li>
<li class="">There is a great community behind Proxmox that has already developed <a href="https://community-scripts.github.io/ProxmoxVE/" target="_blank" rel="noopener noreferrer" class="">helper scripts</a> for a variety of services. This would save me from having to reinvent the wheel for some commonly used services.</li>
<li class="">I could easily expand this single Proxmox machine into a cluster eventually for high availability, if things get that serious.</li>
<li class="">Proxmox has great built-in support for snapshots and using them to roll back to previous states of machines and containers.</li>
</ol>
<p>I downloaded the Proxmox 9.1 ISO, threw it onto a USB stick, booted the Beelink from it and installed it right onto the SSD, making sure to give it a static IP from my router during installation.</p>
<p><a href="https://misczak.github.io/assets/files/proxmoxdashboard-ff0a6141b2c7d0caeb24069840930e9c.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="My Current Proxmox Dashboard " src="https://misczak.github.io/assets/images/proxmoxdashboard-ff0a6141b2c7d0caeb24069840930e9c.png" width="1918" height="862" class="img_ev3q"></a></p>
<p>There are a couple of tweaks I had to make as a non-Enterprise user of Proxmox after install. These include disabling the enterprise repositories for package updates and adding ones that don't require a subscription. I made these changes myself in the Proxmox UI under <strong>Updates &gt; Repositories</strong>, but I could also have grabbed a <a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=post-pve-install" target="_blank" rel="noopener noreferrer" class="">Helper Script</a> that has been written to do all of this in an automated fashion. I also had to go into <strong>Datacenter &gt; Storage</strong> and set up my second SSD for it to be usable and visible in the Proxmox UI.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tailscale">Tailscale<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#tailscale" class="hash-link" aria-label="Direct link to Tailscale" title="Direct link to Tailscale" translate="no">​</a></h2>
<p>I have used <a href="https://tailscale.com/" target="_blank" rel="noopener noreferrer" class="">Tailscale</a> for a little while now to give my computers, phone, and tablet connectivity to my NAS even when outside of the house. Synology has a Tailscale app in their package manager that made installation and setup a breeze, and I hadn't needed to go any deeper than that up until this point. But since I'm moving everything over to the Proxmox node, I wanted to put Tailscale on there as well and start taking advantage of its more powerful features.</p>
<p>My requirements for setting this up were simple - I wanted to have a single Tailscale install grant me connectivity to any services I may be running on the server. But I also wanted to run Tailscale in an unprivileged LXC to avoid giving it more permissions than I have to. Tailscale has put out some great <a href="https://www.youtube.com/watch?v=guHoZ68N3XM" target="_blank" rel="noopener noreferrer" class="">tutorial videos</a> about getting started installing it on Proxmox, but their example has you installing Tailscale directly on the Proxmox host itself as root, which is something I didn't want to do.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="installation">Installation<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#installation" class="hash-link" aria-label="Direct link to Installation" title="Direct link to Installation" translate="no">​</a></h3>
<p>Looking at the Helper Scripts repository, there is a <a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=add-tailscale-lxc" target="_blank" rel="noopener noreferrer" class="">script for Tailscale</a>, but it is an "add-on" - something to run inside a previously created and established LXC, not one that will configure an LXC from scratch for you.</p>
<p>I started by creating a new LXC container in Proxmox based on their Ubuntu 24.04 standard template with 10 GB of storage and 512 MiB of memory. After booting up and logging in via the Proxmox console, I made things easier for myself by allowing me to SSH in as root by editing <code>/etc/ssh/sshd_config</code> and commenting out the following line:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">PermitRootLogin prohibit-password</span><br></div></code></pre></div></div>
<p>After running <code>sudo systemctl restart sshd</code>, I could SSH with the root password from my computer's terminal, which allowed me to have some quality of life stuff, like the ability to copy and paste from the terminal.</p>
<p>Next was updating and upgrading the stuff already in the container:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">apt-get update &amp;&amp; apt-get upgrade -y</span><br></div></code></pre></div></div>
<p>Followed by adding curl, which will help install Tailscale but does not come in LXC containers by default:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">apt-get install curl</span><br></div></code></pre></div></div>
<p>Finally I was ready to install Tailscale. The Tailscale website has a handy one liner that lets you curl their install script and execute it. In a production environment, this is not something you want to do, but with a home lab, it's up to you to trust the vendor and inspect the script. I've used this before from Tailscale, so I went ahead and ran it:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">curl -fsSL https://tailscale.com/install.sh | sh</span><br></div></code></pre></div></div>
<p>Finally, I went back to <code>/etc/ssh/sshd_config</code> and uncommented the PermitRootLogin line to block logging in with just the root password again.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuration">Configuration<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#configuration" class="hash-link" aria-label="Direct link to Configuration" title="Direct link to Configuration" translate="no">​</a></h3>
<p>Now Tailscale is installed. I could start it up, but all it would do right now is allow me connectivity back into this container. I want Tailscale running inside this container to be my access point to the rest of my home lab, including the other containers that will be running on Proxmox. To do that, we have to do three things: enable ip forwarding, grant Tailscale access to the host's network settings, and advertise the subnet that we want Tailscale to route traffic to.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="ip-forwarding">IP Forwarding<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#ip-forwarding" class="hash-link" aria-label="Direct link to IP Forwarding" title="Direct link to IP Forwarding" translate="no">​</a></h4>
<p>This is probably the most straightforward of the tasks. I just needed to edit <code>/etc/sysctl.conf</code> and make sure the following lines are presented and uncommented:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">net.ipv4.ip_forward=1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">net.ipv6.conf.all.forwarding=1</span><br></div></code></pre></div></div>
<p>I saved the changes and that's it for IP forwarding!</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="host-networking">Host Networking<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#host-networking" class="hash-link" aria-label="Direct link to Host Networking" title="Direct link to Host Networking" translate="no">​</a></h4>
<p>Because I'm running Tailscale in an unprivileged container, it does not have access to all of the host's hardware and settings. This is exactly what I want, but in this case I need to make an exception for network settings, as I want Tailscale to be able to route traffic outside its container. Tailscale has a great knowledge base article written up <a href="https://tailscale.com/kb/1130/lxc" target="_blank" rel="noopener noreferrer" class="">here</a> about this.</p>
<p>The first step is to shut down the LXC container with Tailscale. Then, I opened a shell to the host Proxmox server and edited the file <code>/etc/pve/lxc/[ID].conf</code>, where <code>[ID]</code> is replaced with the ID of my Tailscale container.</p>
<p>Inside, at the very bottom, I added the following two lines:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">lxc.cgroup2.devices.allow: c 10:200 rwm</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file</span><br></div></code></pre></div></div>
<p><a href="https://misczak.github.io/assets/files/tailscaletun-228b32dfea2611578812600009a7b605.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Granting the Tailscale LCX access to the host networking device" src="https://misczak.github.io/assets/images/tailscaletun-228b32dfea2611578812600009a7b605.png" width="1532" height="558" class="img_ev3q"></a></p>
<p>Those two lines will allow my Tailscale container to have access to Proxmox's network settings. I started the container back up, and all that is left is the last step.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="advertise-subnet-routes">Advertise Subnet Routes<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#advertise-subnet-routes" class="hash-link" aria-label="Direct link to Advertise Subnet Routes" title="Direct link to Advertise Subnet Routes" translate="no">​</a></h4>
<p>Since I'd like to be able to reach the Proxmox web UI via Tailscale without installing Tailscale on the host itself, I need to configure Tailscale in a way that it extends my tailnet to Proxmox itself. Eventually, I will have other containers running here as well and I don't want to have to install Tailscale on each and every one just to have remote access. Tailscale has a feature called <a href="https://tailscale.com/kb/1019/subnets" target="_blank" rel="noopener noreferrer" class="">subnet routers</a> that is designed just for this purpose. All that is needed is to run the right command when bringing Tailscale up in my container where it is installed, and accepting the routes in the admin console.</p>
<p>The first step is to finally turn on Tailscale with my subnet advertised:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">tailscale up --advertise-routes=192.168.50.0/24</span><br></div></code></pre></div></div>
<p>Because this is the first time I started Tailscale in this container, it generated a link that I needed to follow to authenticate the container to my Tailscale account. Once that is done, I was able to go into the admin console on the Tailscale web UI and click the Overflow (...) button on my container Tailscale node and select <strong>Edit Route Settings</strong>. Inside was the subnet I had just advertised and all I had to do was approve it.</p>
<p><a href="https://misczak.github.io/assets/files/subnetroutes-e039aed3d529aa08ca4d408c27ecf032.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Accepting the Advertised Subnet Routes in the Tailscale Admin Console" src="https://misczak.github.io/assets/images/subnetroutes-e039aed3d529aa08ca4d408c27ecf032.png" width="990" height="770" class="img_ev3q"></a></p>
<p>Now I was able to use Tailscale to reach my Proxmox console remotely. One last thing I did was go back to the admin console, click the Overflow button again and choose <strong>Disable Key Expiry</strong>. Normally, Tailscale will refresh its keys for a node every few days, which isn't an issue on systems that you will use and interact with frequently (mobile devices and computers). Since this is a container I'd like to be the main remote entrypoint to my home lab without requiring much intervention, I don't want that - so disabling the key expiry prevents it from silently breaking on me.</p>
<p><a href="https://misczak.github.io/assets/files/keyexpiry-f426a972456226c713df653f3b9c379e.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Disabling the Key Expiry for the Tailscale LXC node" src="https://misczak.github.io/assets/images/keyexpiry-f426a972456226c713df653f3b9c379e.png" width="416" height="632" class="img_ev3q"></a></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wrap-up">Wrap-Up<a href="https://misczak.github.io/blog/building-out-the-homelab-proxmox-and-tailscale#wrap-up" class="hash-link" aria-label="Direct link to Wrap-Up" title="Direct link to Wrap-Up" translate="no">​</a></h2>
<p>And that's it! I have Tailscale properly configured and ready to be my remote entrypoint into any lab service I plan on installing. This configuration is a good foundation to start building out more services, as I'll write about in some future posts.</p>]]></content:encoded>
            <category>Homelab</category>
        </item>
        <item>
            <title><![CDATA[Rethinking My Personal Tech Stack]]></title>
            <link>https://misczak.github.io/blog/rethinking-my-personal-tech-stack</link>
            <guid>https://misczak.github.io/blog/rethinking-my-personal-tech-stack</guid>
            <pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Going through what my personal computing stack looks like as we enter 2026.]]></description>
            <content:encoded><![CDATA[<p>Like many people, I usually take the start of a new year as an opportunity to reevaluate some choices and habits I have and see if there is room for improvement. One such area up for review is the set of services, apps, and subscriptions I use outside of work.</p>
<p>Much of my current computing habits can trace their origins back to the 2000’s, where Gmail marked the start of Google’s growth beyond just search and ads and smartphones began to take off. That was a very long time ago, and a lot has changed in both my own habits and the tech industry since then - so I used most of 2025 to begin researching and evaluating alternatives to what I was currently using. I placed a point on emphasis on identifying options that were cross-platform (avoiding lock in to any one operating system) and gave preference to alternatives that were open source and/or allowed me to either self host or at least keep a copy of my data synced locally, in case of account lockout. I ended up finding a lot of great options, some of which I’ve switched over to entirely.</p>
<p>This post will highlight some of these alternatives - I plan on making more in depth posts about each one specifically at some point in the future.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="email">Email<a href="https://misczak.github.io/blog/rethinking-my-personal-tech-stack#email" class="hash-link" aria-label="Direct link to Email" title="Direct link to Email" translate="no">​</a></h2>
<p>I got my Gmail invite sometime in the middle of 2004, and for over twenty years it has been my main email address. I've used some others over the years for various purposes (most notably my university one), but in the end I forwarded most of those to Gmail. For many years, the benefits Gmail provided were great - a really fast interface built for modern web browsers, ample storage space, and integration with the messaging service Google Talk, which everybody migrated to from AIM in the late 2000's.</p>
<p>Over time, each of these advantages have disappeared. Google fumbled Google Talk, turning it into Hangouts so it could be used in the Google+ push - and when that failed, it became Google Chat and Meet. The storage space became shared across other Google products like Drive and Photos, driving you towards buying more of it with a Google One subscription. And the Gmail interface has become pretty bloated - it's impossible to ignore the slow page load times when navigating through emails and organizing my inbox.</p>
<p>I looked around at a bunch of different email providers including Proton, Tuta, even Mailbox.org. In the end, I went with <a href="https://www.fastmail.com/" target="_blank" rel="noopener noreferrer" class="">Fastmail</a>, a service that has been around since 1999 but I had never paid much attention to before now. Fastmail earns its name with how quick its web interface is, and it focuses just on email, calendar, and contacts - although it does have some basic support for Files. Unlike Gmail, it has first class support for using your own domain name and many aliases, giving you flexibility in how you want to setup your mailbox and rules. Its ability to effortlessly migrate my Gmail inbox and calendar cannot be overlooked, and I love how it integrates with my password manager of choice, 1Password, to generate <a href="https://www.fastmail.com/features/masked-email/" target="_blank" rel="noopener noreferrer" class="">masked emails</a> automatically if I have to sign up for something on a website. Fastmail also seems to be the only service that supports push notifications on the Apple Mail app (on both macOS and iOS) if that matters to you - a feature that not even iCloud Mail can claim!</p>
<p>If you end up checking out Fastmail, feel free to use my <a href="https://join.fastmail.com/59d4c1b7" target="_blank" rel="noopener noreferrer" class="">referral link</a> to save some money.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="photos">Photos<a href="https://misczak.github.io/blog/rethinking-my-personal-tech-stack#photos" class="hash-link" aria-label="Direct link to Photos" title="Direct link to Photos" translate="no">​</a></h2>
<p>I was a pretty early user of Picasa, and that eventually morphed into Google Photos, transitioning my photos through to it. Through both Android devices and iPhone, I have been steadily adding to Google Photos over the years, but the number of pictures I store there really exploded once my kids were born. It has been a solid service all this time, but recently they have been shoving AI features to the forefront all over the app, which greatly annoys me. I had two other concerns with continuing to entrust all my photos to Google: being on a subscription treadmill to a company that will most likely raise rates constantly over the years to come, and the lack of support if I was to ever become locked out of my Google account.</p>
<p>So for most of 2025, I've been experimenting with two different services - <a href="https://ente.io/" target="_blank" rel="noopener noreferrer" class="">Ente Photos</a> and <a href="https://immich.app/" target="_blank" rel="noopener noreferrer" class="">Immich</a>. Both are open source and both can be self hosted, but Ente Photos allows you to pay for them to operate the service and backup your photos. All of the machine learning for facial recognition is done on your device and then the results are synced synced across the other devices. Immich relies on a server (more specifically, a machine learning container) to do all of that work. I like Immich's interface a little bit more, but I am not yet at the point where my home lab is robust enough to entrust it with being the primary mechanism to organize and store my photos for me and the rest of the family.</p>
<p>So for now, I've moved onto Ente Photos, which I quite like. The facial recognition confused my two sons quite a bit initially, but once I spent a little time training it, it's been much better. The iOS app also feels a lot snappier than either Google Photos or Immich, which the less technical members of my family appreciate. One day in the future I may switch over to Immich, once its a little more mature and my home lab is built out a bit more (including a robust backup strategy).</p>
<p>If you want to check out Ente, you can use the referral code MISCZAK to get 10 GB for free once you sign up.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="browser">Browser<a href="https://misczak.github.io/blog/rethinking-my-personal-tech-stack#browser" class="hash-link" aria-label="Direct link to Browser" title="Direct link to Browser" translate="no">​</a></h2>
<p>I've been using Firefox for a number of years due to its <a href="https://support.mozilla.org/en-US/kb/containers" target="_blank" rel="noopener noreferrer" class="">Multi-Account Containers</a> feature to isolate trackers from companies like Facebook, Amazon, and Google, and I doubled down on my usage of it this past year when Chrome disabled Manifest V2, thereby curtailing support for extensions like uBlock Origin. I am not opposed to paying for content and services on the web (as this blog post will illustrate), but I find it hard to tolerate the onslaught of ad tech that appears everywhere. Firefox also added vertical tabs this year, a feature that I've found that I cannot live without - it's just a much smarter way of keeping tabs organized and readable, even when you have a bunch of them open.</p>
<p>One area where I'm not satisfied is my ability to use the same browser on iOS. Firefox is essentially a reskin of Safari there, but it can't use any extensions, which means that I'm once again without ad blocking. Because of this, I have tended to use Safari on my iPhone with some ad blocking extensions installed. If Apple would allow true browser competition in the app store, I would most likely use an enhanced version of Firefox there.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="files">Files<a href="https://misczak.github.io/blog/rethinking-my-personal-tech-stack#files" class="hash-link" aria-label="Direct link to Files" title="Direct link to Files" translate="no">​</a></h2>
<p>Much like I used Gmail and Google Photos previously, Google Drive became my de facto way of keeping files synced across machines. However, I can count on one hand the number of times over the last decade that I've needed access to some documents when I'm not at home - and when that does happen, I usually have time to prepare and bring them with me.</p>
<p>Therefore, I've moved my primary file storage to my Synology NAS, which allows me to keep them synced across my desktop and laptop when at home. I can also use Synology's built in backup and sync features to keep a copy with a cloud provider if I want to - which I do with Dropbox.</p>
<p>I had forgotten that I had about 17 GB of free storage there, as I was an early adopter and beta tester of their Linux client in 2008, and was able to refer a number of friends in exchange for free storage. 17 GB is more enough to keep an off-site backup of some of my most important files, and their <a href="https://www.fastmail.com/blog/files-and-dropbox/" target="_blank" rel="noopener noreferrer" class="">integration with Fastmail</a> makes it easy to include them as attachments if I do need them one day.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="notes">Notes<a href="https://misczak.github.io/blog/rethinking-my-personal-tech-stack#notes" class="hash-link" aria-label="Direct link to Notes" title="Direct link to Notes" translate="no">​</a></h2>
<p>Finally, a service that isn't Google! While I did try Google Keep about a decade ago when I had an Android phone, Apple Notes has been the go-to for a lot of quick notes over the last few years. I have been using <a href="https://obsidian.md/" target="_blank" rel="noopener noreferrer" class="">Obsidian</a> during that time for more detailed notes, like technical documentation that relies on Markdown, which Apple Notes doesn't support. I like how Obsidian is essentially a nested folder structure of .md files on my local storage, so I have consolidated all my note taking into Obsidian for now.</p>
<p>One thing that has held me back from Obsidian in the past is the difficulty in syncing it to all platforms I want to use. I use Obsidian on my iPhone, my laptop (Macbook Pro), and my desktop PC. To keep Obsidian in sync on the iPhone, I could keep it in iCloud Storage - but then that complicates syncing it to Windows/Linux devices. If I use something like Dropbox, then I don't have automatic, consistent sync on iPhone. In the end, I ended up resubscribing to Obsidian's built-in sync service, where I am grandfathered into an early bird discount.</p>
<p>I did pick up a trial of <a href="https://notesnook.com/" target="_blank" rel="noopener noreferrer" class="">Notesnook</a> in order to take it for a spin, but I don't like the interface as much as Obsidian.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="search">Search<a href="https://misczak.github.io/blog/rethinking-my-personal-tech-stack#search" class="hash-link" aria-label="Direct link to Search" title="Direct link to Search" translate="no">​</a></h2>
<p>While I have used Google Search for a long time, their results have recently fallen off a cliff. It's become almost impossible to find good results when entering search queries; most times, I'm relieved if there's a Reddit discussion as one of the top results since it will usually have some relevance and quality.</p>
<p>For that reason, I'm switched to <a href="https://kagi.com/" target="_blank" rel="noopener noreferrer" class="">Kagi</a> as my search engine. The professional plan seems quite steep at first ($10/month for search!) but it cannot be overstated how nice it is to have search results that are actually usable again. Kagi also has a lot of features that make it really easy to finetune your results to improve your signal-to-noise ratio, such as raising/lowering domains in your search results (and comparing your choices to a global leaderboard) and the ability to filter results through lenses, which are preconfigured lists of sources, so that you can get meaningful results even for queries that contain common names or phrases.</p>
<p>Kagi is also working on a browser (Orion) and a Maps service, both of which I'll be keeping an eye on.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="operating-system">Operating System<a href="https://misczak.github.io/blog/rethinking-my-personal-tech-stack#operating-system" class="hash-link" aria-label="Direct link to Operating System" title="Direct link to Operating System" translate="no">​</a></h2>
<p>While I continue to use a Macbook Pro for my day job and as a personal laptop, I've maintained a personal desktop for nearly 20 years that has always been Windows based as its primary purpose is PC gaming. However, Windows has become so enshittified over the last year or two that i felt like my hand was forced to jump into Linux on the PC, even if there will be some casualties (like multiplayer games that use anticheat that is unsupported on Linux).</p>
<p>So before the holidays last month, I installed <a href="https://cachyos.org/" target="_blank" rel="noopener noreferrer" class="">CachyOS</a> on my desktop and haven't looked back. This will definitely get a more in depth post at some point, but I am enjoying how lightweight and snappy it feels. I haven't noticed any adverse impact on game performance in the titles I've tried - which include Star Wars Jedi Survivor, Witcher 3, Dead Space Remake, and Hades 2. It's my first time really using an Arch-based distro as a daily driver, and the fact that most of the stuff I need can be installed right out of the Arch User Repository has made everything really easy.</p>
<h1>Refreshing the List</h1>
<p>I plan on continuing to evaluate additional options, and will keep an up to date list on what I currently use on my About page. I also plan on making a post every year with any major changes or developments.</p>]]></content:encoded>
            <category>opensource</category>
            <category>productivity</category>
        </item>
        <item>
            <title><![CDATA[About the Ruby Central Security Incident]]></title>
            <link>https://misczak.github.io/blog/about-the-ruby-central-security-incident</link>
            <guid>https://misczak.github.io/blog/about-the-ruby-central-security-incident</guid>
            <pubDate>Fri, 10 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Thoughts on the Ruby Central AWS security incident.]]></description>
            <content:encoded><![CDATA[<p>Recently, Ruby Central, the non-profit responsible for maintaining the RubyGems package manager, suffered a security incident where they temporarily lost control of their AWS account. They have since <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/" target="_blank" rel="noopener noreferrer" class="">published a post-mortem</a> of this event that is ostensibly aimed at putting their community's minds at ease. Unfortunately, it had the opposite effect on me, causing me to come away with more questions than  answers.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-incident">The Incident<a href="https://misczak.github.io/blog/about-the-ruby-central-security-incident#the-incident" class="hash-link" aria-label="Direct link to The Incident" title="Direct link to The Incident" translate="no">​</a></h2>
<p>Let's begin by looking at the timeline that is included in the post-mortem. All of the events in the initial timeline take place within a few hours on September 30, 2025. Three particular entries stand out:</p>
<blockquote>
<p><strong>17:23 UTC:</strong> A former maintainer, André Arko, emails the Director of Open Source at Ruby Central stating that he still has access to the RubyGems.org production environment and associated monitoring tools.</p>
</blockquote>
<blockquote>
<p><strong>17:30 UTC:</strong> Joel Drapper (unaffiliated with Ruby Central) publishes a <a href="https://web.archive.org/web/20250930213611id_/https://joel.drapper.me/p/ruby-central-security-measures/" target="_blank" rel="noopener noreferrer" class="">public blog post</a> within minutes describing this access with screenshots taken earlier that day showing root account access.</p>
</blockquote>
<blockquote>
<p><strong>18:20 UTC:</strong> Ruby Central begins its emergency review and learns that the existing credentials for the AWS root account in our password vault are no longer valid.</p>
</blockquote>
<p>This is the earliest time that Ruby Central learns something is amiss with their AWS account. However, if you scroll down to their "Analysis of Events" section you'll see a number of events stretching all the way back to September 18. It turns out that this root password change took place over ten days prior on September 19, 2025, without anyone at Ruby Central being any the wiser about it. This is an astonishing admission, most notably because AWS will send an email notification to the root user email address whenever the password is changed. Apparently, nobody at Ruby Central is looking at the emails sent to this address - otherwise, they would have noticed this initial reset on September 19, eleven days before they eventually did.</p>
<p>They are also extremely fortunate that the threat actor did not end up changing the root account email as well; there seems to be a reason for that, which I'll discuss below. Since this email was not changed, they are able to regain control of the account in the same manner that they lost it:</p>
<blockquote>
<p><strong>18:24 UTC:</strong> Ruby Central initiates an AWS password-reset procedure, validates multi-factor authentication, and regains control of the AWS root account.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="an-incomplete-post-mortem">An Incomplete Post-Mortem<a href="https://misczak.github.io/blog/about-the-ruby-central-security-incident#an-incomplete-post-mortem" class="hash-link" aria-label="Direct link to An Incomplete Post-Mortem" title="Direct link to An Incomplete Post-Mortem" translate="no">​</a></h2>
<p>What's even more perplexing is how quickly this post-mortem jumps to to definitively concluding the scope of the incident. Writing under the section "Extent of the Incident":</p>
<blockquote>
<p>After a careful review, Ruby Central is relieved to report <strong>that we see no evidence</strong> that this security incident <strong>compromised end user data, accounts, gems, or infrastructure availability</strong>
In addition:</p>
<ul>
<li class="">RubyGems.org remained fully operational throughout.</li>
<li class="">No personally identifiable information (PII) of RubyGems.org users nor Ruby Central financial data was accessed or transferred.</li>
<li class="">The production database, S3 buckets, and CI/CD pipeline were unaffected.</li>
</ul>
</blockquote>
<p>At first glance, this feels like a pretty positive outcome of this event. They reclaimed control of the account and it looks like no irreversible damage occurred. But it's the next section that makes me question just how much we can trust these statements:</p>
<blockquote>
<p>After regaining control of the AWS account, Ruby Central:</p>
<ol>
<li class=""><strong>Revoked all existing root and IAM credentials</strong>, created new MFA-protected access, and moved them to a restricted vault with per-user audit logs.</li>
<li class=""><strong>Rotated all related secrets and tokens</strong>, including DataDog, GitHub Actions, and other external system integrations.</li>
<li class=""><strong>Enabled AWS CloudTrail, GuardDuty, and DataDog alerting</strong> for any root login, password change, or IAM modification.</li>
</ol>
</blockquote>
<p>The third item there is a pretty big red flag. Prior to this incident, they weren't configuring CloudTrail (audit logs), Guard Duty (threat detection), or just basic alerting for the account. Without this context being collected and (ideally) being forwarded to a separate repository outside of AWS that would require separate access to tamper with, it casts a lot of doubt on their earlier conclusions.</p>
<p>As bad as that line sounds, AWS still collects and retains 90 days of CloudTrail logs for your account by default, something that can not be disabled. Since the whole timeline of this incident is a little less than two weeks, that falls well within the retention period for an investigation.</p>
<p>But (and this is a big but!) the issue is that this default CloudTrail configuration only includes management events. Data events that capture actions like users downloading objects from S3 buckets, users uploading objects to S3 buckets, API activity on an RDS database cluster, and executing Lambda functions are <strong>not</strong> included in that default configuration. Therefore, it is quite possible that Ruby Central does not have any logs that can conclusively say that the actor did <strong>not</strong> download objects from S3 or touch the production database. Of course, they may know this, which is why their statement is structured to say they "see no evidence" - they don't have any to check.</p>
<p>I would've also hoped for them to provide a full list of actions the actor took that they can see in the CloudTrail management events to further support their timeline of events - but alas, that was not included either.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-other-side-of-the-story">The Other Side of the Story<a href="https://misczak.github.io/blog/about-the-ruby-central-security-incident#the-other-side-of-the-story" class="hash-link" aria-label="Direct link to The Other Side of the Story" title="Direct link to The Other Side of the Story" translate="no">​</a></h2>
<p>The plot thickens, however, with a <a href="https://andre.arko.net/2025/10/09/the-rubygems-security-incident/" target="_blank" rel="noopener noreferrer" class="">blog post published</a> by the alleged actor, André Arko. He acknowledges the rotation of the root user password:</p>
<blockquote>
<p>Given Marty’s claims, the sudden permission deletions made no sense. Worried about the possibility of hacked accounts or some sort of social engineering, I took action as the primary on-call engineer to lock down the AWS account and prevent any actions by possible attackers. I did not change the email addresses on any accounts, leaving them all owned by a team-shared email at rubycentral.org, to ensure the organization retained overall control of the accounts, even if individuals were somehow taking unauthorized actions.</p>
</blockquote>
<p>This sequence of actions is pretty interesting for a few reasons. First, if I am the on-call engineer and a security incident is occurring that forces me to change a credential, I am absolutely notifying the other parties that may need that credential that I am rotating it. This is even more important when its something as significant as an AWS account's root credential. In any incident response process, I am also updating the credential with the rotated value in the shared password vault/secrets manager that my team would use. If the rest of the team is asleep or its the weekend, I might send them a note or a Slack message - but as soon as they come back into business hours, I would make sure they acknowledged the change. Instead, André waits for 11 days for any kind of acknowledgement:</p>
<blockquote>
<p>Within a couple of days, Ruby Central made an (unsigned) public statement, and various board members agreed to talk directly to maintainers. At that point, I realized that what I thought might have been a malicious takeover was both legitimate and deliberate</p>
</blockquote>
<p>The fact that André waited so long to acknowledge this change in credentials and his prolonged access initially made me have some suspicion about his intention here. However, the fact that he left the root user email address the same (thereby enabling Ruby Central's account recovery days later) shows that maybe his actions were just misguided. And he seems to call out the pretty immature security posture of Ruby Central in general:</p>
<blockquote>
<p>In addition to ignoring the (huge) question of how Ruby Central failed to secure their AWS Root credentials for almost two weeks, and <strong>appearing to only be aware of it because I reported it to them</strong>, their reply also failed to ask whether any other shared credentials might still be valid. There were more.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://misczak.github.io/blog/about-the-ruby-central-security-incident#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>All in all, this whole incident and the write-ups that have come out of it do not reflect well on the Ruby community, and would very much shake my confidence in the ability of Ruby Central to be good stewards of RubyGems moving forward.</p>]]></content:encoded>
            <category>AWS</category>
            <category>programming</category>
            <category>ruby</category>
        </item>
        <item>
            <title><![CDATA[Recapping the Biggest Pre:Invent Announcements]]></title>
            <link>https://misczak.github.io/blog/recapping-the-biggest-preinvent-announcements</link>
            <guid>https://misczak.github.io/blog/recapping-the-biggest-preinvent-announcements</guid>
            <pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A recap of all of the new AWS announcements this week leading up to re:Invent.]]></description>
            <content:encoded><![CDATA[<p>While AWS re<!-- -->:Invent<!-- --> doesn't officially kick off until December 2, we are now officially in the lead up to the event, a time that often seea a flurry of new feature and service announcements. While these announcements are often overshadowed by new products that debut at the conference, they are often just as important (if not more so) to the work I do every day. This week has been the strongest example of this trend to date, with several announcements that made me genuinely excited. In the interest of processing that enthusiasm, I decided to write a recap of what I view to be the biggest ones.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="centrally-managing-root-access-for-customers-using-aws-organizations">Centrally Managing Root Access for Customers Using AWS Organizations<a href="https://misczak.github.io/blog/recapping-the-biggest-preinvent-announcements#centrally-managing-root-access-for-customers-using-aws-organizations" class="hash-link" aria-label="Direct link to Centrally Managing Root Access for Customers Using AWS Organizations" title="Direct link to Centrally Managing Root Access for Customers Using AWS Organizations" translate="no">​</a></h2>
<p><a href="https://aws.amazon.com/blogs/aws/centrally-managing-root-access-for-customers-using-aws-organizations/" target="_blank" rel="noopener noreferrer" class="">Blog Post</a></p>
<p>If you've gone through the AWS Trusted Advisor journey with your account team, you're most likely familiar with one of the automated findings being that your accounts' root users are not configured with MFA. This finding was an example of AWS' own framework not really keeping up with the practices that most security teams were pursuing; that is, not enabling root user accounts at all for any new accounts added to your AWS Organization. Even in these mature environments, there were some accounts that predated the arrival of the AWS Organizations feature, which meant that root users did exist, and the credentials still existed <em>somewhere</em> posting some sort of risk. Often times, security teams had to go through an exercise of tracking down these legacy credentials, making sure they were stored securely, rotating them at regular intervals, and adding some form of MFA - a lot of work for something that shouldn't really exist anymore, but was still needed for actions like unlocking S3 bucket policies.</p>
<p><a href="https://misczak.github.io/assets/files/rootaccess-b163837f15bbf0fb65d946aa4c5a72c7.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="The Central Root Access Management Console Page" src="https://misczak.github.io/assets/images/rootaccess-b163837f15bbf0fb65d946aa4c5a72c7.png" width="2890" height="704" class="img_ev3q"></a></p>
<p>This new feature allows a security team to simply remove root user credentials from all accounts in their AWS Organization, block any attempt to 'recover' them in the future (by authorized or unauthorized party), and use short-lived <strong>root sessions</strong> to still perform the actions that require root access, preserving compatibility. Put together, these feature enhancements support the existing best practices (namely, avoiding IAM users and always choosing short lived sessions with roles), finally bringing privileged user management in alignment with regular user management. I would expect many teams to begin putting this into practice shortly if they haven't done so already.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="block-public-access-for-amazon-virtual-private-cloud">Block Public Access for Amazon Virtual Private Cloud<a href="https://misczak.github.io/blog/recapping-the-biggest-preinvent-announcements#block-public-access-for-amazon-virtual-private-cloud" class="hash-link" aria-label="Direct link to Block Public Access for Amazon Virtual Private Cloud" title="Direct link to Block Public Access for Amazon Virtual Private Cloud" translate="no">​</a></h2>
<p><a href="https://aws.amazon.com/blogs/networking-and-content-delivery/vpc-block-public-access/" target="_blank" rel="noopener noreferrer" class="">Blog Post</a></p>
<p>VPCs are an essential component of any AWS account, allowing for multiple workloads in a single AWS account to maintain network isolation. Traffic in and out of VPCs can be governed in a few different ways, with security group rules typically being the most popular mechanism for teams to use. One of the problems with that approach, however, is that security group rules cannot be controlled proactively with Service Control Policies, unlike many other areas prone to misconfigurations in AWS. This gap forces teams to often be reactive when it comes to dealing with overly permissive security groups.</p>
<p>Block Public Access gives security teams some peace of mind by providing a proactive tool to override any misconfiguration done at the individual VPC level itself. Similar to the S3 Block Public Access setting that acts as a master switch to compensate for bad bucket policies, teams can use Block Public Access for Amazon VPC to limit internet connectivity for just ingress traffic, or both ingress and egress. When blocking just ingress, any egress traffic must go through a NAT Gateway or Egress-Only Internet Gateway (for IPv6). The best part of this feature, however, is the ability to add granular exclusions to allow individual VPCs or subnets to bypass this enforcement - so you can still make exceptions when you need to while keeping the safety net intact for everything else.</p>
<p><a href="https://misczak.github.io/assets/files/vpcblockexclusions-86e03b9a8675f6506b6aee5000263c57.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Creating Exclusions for Block Public Access" src="https://misczak.github.io/assets/images/vpcblockexclusions-86e03b9a8675f6506b6aee5000263c57.png" width="626" height="461" class="img_ev3q"></a></p>
<p>This capability already has me thinking about a multitude of ways we can use this in our organization, ranging from driving compliance with network architecture standards and best practices to playing a critical role in incident response, allowing us to quickly isolate a VPC during an investigation if necessary.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="resource-control-policies">Resource Control Policies<a href="https://misczak.github.io/blog/recapping-the-biggest-preinvent-announcements#resource-control-policies" class="hash-link" aria-label="Direct link to Resource Control Policies" title="Direct link to Resource Control Policies" translate="no">​</a></h2>
<p><a href="https://aws.amazon.com/blogs/aws/introducing-resource-control-policies-rcps-a-new-authorization-policy/" target="_blank" rel="noopener noreferrer" class="">Blog Post</a></p>
<p>At AWS re<!-- -->:Inforce<!-- --> this year, I had a conversation with a few AWS people who seemed to imply that they'd like to introduce more security boundary controls on resources themselves - and that seems to be the case with Resource Control Policies (RCPs). Following the debut of Service Control Policies (SCPs) in 2019, RCPs give your team the ability to set universal access controls right on a resource itself. These can be used to block specific actions to an entire service or on specific resources themselves. A key distinction is that SCPs get evaluated for principals trying to perform an action; RCPs get evaluated when a resource receives an access request.</p>
<p><a href="https://misczak.github.io/assets/files/attachrcp-ab80c0203d6019864fc3156366791735.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="Attaching a Resource Control Policy to OUs" src="https://misczak.github.io/assets/images/attachrcp-ab80c0203d6019864fc3156366791735.png" width="963" height="505" class="img_ev3q"></a></p>
<p>RCPs get attached to OUs and specific AWS accounts in a similar way to SCPs, and therefore should be tested just as much before rollout. However, I foresee the time to develop and implement new RCPs will be significantly quicker than SCPs in organizations, as it will be easier to scope RCPs to just protect specific resources (such as security tooling). Your security team can prevent anyone else from using, modifying or removing a resource that it uses to integrate with a configuration management database or cloud security posture management (CSPM) tool, while still allowing regular users to have full control over other resources in their account.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="scaling-to-0-capacity-with-aurora-serverless-v2">Scaling to 0 capacity with Aurora Serverless v2<a href="https://misczak.github.io/blog/recapping-the-biggest-preinvent-announcements#scaling-to-0-capacity-with-aurora-serverless-v2" class="hash-link" aria-label="Direct link to Scaling to 0 capacity with Aurora Serverless v2" title="Direct link to Scaling to 0 capacity with Aurora Serverless v2" translate="no">​</a></h2>
<p><a href="https://aws.amazon.com/blogs/database/introducing-scaling-to-0-capacity-with-amazon-aurora-serverless-v2/" target="_blank" rel="noopener noreferrer" class="">Blog Post</a></p>
<p>Okay, this one may not be as directly related to security as the others, but it fixes a personal pain point I've experienced in the past, so I wanted to highlight it. Several years ago when I stood up the first version of my team's configuration management database, we were using Aurora Serverless v1. One of the reasons we used Aurora Serverless v1 at the time was the fact that it could scale down to zero active nodes, only spinning up when needed. This allowed it to sit 'idle' for the 23 hours of the day that we weren't running the sync job for our configuration management tool, significantly reducing the cost of operating this solution. Aurora Serverless v2 had several benefits we wanted to use, such as IAM authentication, but only scaled down to 0.5 Aurora Capcity Units (ACU) per hour. We ended up migrating to a MongoDB Atlas database for this and other reasons.</p>
<p>Therefore, I'm glad to see that Aurora Serverless v2 now pauses after a period of inactivity, with no charges for compute while paused. This change makes it a viable option again for some smaller services and projects I want to build without worrying about throwing money away if it sits idle for prolonged periods of time.</p>]]></content:encoded>
            <category>AWS</category>
        </item>
        <item>
            <title><![CDATA[An Easier Way to Enable IMDS Defaults Across All Regions]]></title>
            <link>https://misczak.github.io/blog/an-easier-way-to-enable-imds-defaults-across-all-regions</link>
            <guid>https://misczak.github.io/blog/an-easier-way-to-enable-imds-defaults-across-all-regions</guid>
            <pubDate>Thu, 28 Mar 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[An update on AWS enabling account-level defaults for the Instance Metadata Service.]]></description>
            <content:encoded><![CDATA[An update on AWS enabling account-level defaults for the Instance Metadata Service.<!-- -->
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introduction">Introduction<a href="https://misczak.github.io/blog/an-easier-way-to-enable-imds-defaults-across-all-regions#introduction" class="hash-link" aria-label="Direct link to Introduction" title="Direct link to Introduction" translate="no">​</a></h2>
<p>EC2 instances in AWS can have access to something called the instance metadata service, which makes information (namely <em>metadata</em>) about an instance available to applications, services, code, etc. that runs on the instance. For example, a piece of code can query the metadata service to learn what region it is currently running in.</p>
<p>When an EC2 instance is assigned an instance profile that grants it permissions to access other AWS services, the temporary credentials for that instance profile can also be retrieved from the instance metadata service. This setup is extremely useful, as it allows your code to make authenticated calls to AWS APIs without having to use hardcoded credentials, environment variables, or frequent calls to some off-host credential manager.</p>
<p>However, the initial version of the Instance Metadata Service (IMDSv1) had a few issues that opened the door for Server Side Request Forgery attacks against applications running on an EC2 instance to steal AWS credentials from IMDS and use them to take action elsewhere in an account. This flaw was the cause of the <a href="https://krebsonsecurity.com/2019/07/capital-one-data-theft-impacts-106m-people/" target="_blank" rel="noopener noreferrer" class="">Capital One breach back in 2019</a> as well as a number of other security incidents across the industry.</p>
<p>Since then, AWS made <a href="https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/" target="_blank" rel="noopener noreferrer" class="">several improvements in a second version</a> of IMDS called, obviously, IMDSv2. However, this setting was not turned on by default for fear of breaking compatibility with existing workloads. Users can opt into this newer version of IMDS when launching new instances, putting the onus on security teams to put up guard rails to ensure that teams were moving to IMDSv2 and burning down the existing use of IMDSv1.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-new-default">A New Default<a href="https://misczak.github.io/blog/an-easier-way-to-enable-imds-defaults-across-all-regions#a-new-default" class="hash-link" aria-label="Direct link to A New Default" title="Direct link to A New Default" translate="no">​</a></h2>
<p>Back in November 2023, <a href="https://aws.amazon.com/blogs/aws/amazon-ec2-instance-metadata-service-imdsv2-by-default/" target="_blank" rel="noopener noreferrer" class="">AWS announced</a> that by mid 2024, all EC2 instance types will only use IMDSv2. At the same time, they made it so that all quick launches through the AWS console would only use IMDSv2. These were welcome changes, but did not provide additional tools to allow admins to begin enforcing defaults before the mid-2024 date.</p>
<p>All of that changed this past week, as <a href="https://aws.amazon.com/about-aws/whats-new/2024/03/set-imdsv2-default-new-instance-launches/" target="_blank" rel="noopener noreferrer" class="">AWS finally rolled out the ability to set defaults</a> for the Instance Metadata Service at the account level. However, these settings are specific to a single region, meaning that if your account(s) use a large number of regions, you would have to go region by region (through either the CLI or console) to change these settings.</p>
<p>That inspired me to throw together a very, very quick Python script that aims to first look at the active regions in an account and then cycle through each region and set these new defaults in an automated fashion. It aims to be the least disruptive to existing workflows by only changing the IMDS required version and hop limit, leaving the default setting for enabling IMDS and the ability to pass tags to it alone, unless otherwise specified. You can find this script <a href="https://github.com/misczak/aws-imds-defaults" target="_blank" rel="noopener noreferrer" class="">here</a>.</p>
<p>While this script is scoped to a single account, if you administer multiple accounts, you can combine this with tools such as <a href="https://github.com/benkehoe/aws-sso-util" target="_blank" rel="noopener noreferrer" class="">aws-sso-util</a> and <a href="https://github.com/99designs/aws-vault" target="_blank" rel="noopener noreferrer" class="">aws-vault</a> to quickly change this setting across your entire fleet.</p>
<p>I hope this helps others speed up the rollout of IMDSv2 defaults across their organization and allows us to collectively put this issue to bed by the end of 2024.</p>]]></content:encoded>
            <category>AWS</category>
        </item>
        <item>
            <title><![CDATA[Google's Professional Cloud Security Engineer Certification]]></title>
            <link>https://misczak.github.io/blog/googles-professional-cloud-security-engineer-certification</link>
            <guid>https://misczak.github.io/blog/googles-professional-cloud-security-engineer-certification</guid>
            <pubDate>Fri, 18 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[My experience preparing for and passing the GCP Professional Cloud Security Engineer exam.]]></description>
            <content:encoded><![CDATA[My experience preparing for and passing the GCP Professional Cloud Security Engineer exam.<!-- -->
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introduction">Introduction<a href="https://misczak.github.io/blog/googles-professional-cloud-security-engineer-certification#introduction" class="hash-link" aria-label="Direct link to Introduction" title="Direct link to Introduction" translate="no">​</a></h2>
<p>Earlier this year, my team was provided access to a training budget for Google Cloud Platform that we could use in various ways to purchase classroom trainings or licenses to Google's on-demand training platform, Cloud Skills Boost as well as a number of exam vouchers that could be used on various GCP certification exams. While I had previously worked almost exclusively with AWS (only using GCP for some testing of private service connect), I knew that I had some upcoming projects that required me to build services on GCP. For that reason, I jumped at the opportunity to take advantage of this training and hopefully prepare myself enough to write an exam at the end of it.</p>
<p>Unbeknownst to me at the time, Google was also offering us a number of seats in what they call their Certification Journey. This program is essentially a focused, six week schedule to work through the Cloud Skills Boost content for your selected certification, with a few extra bonuses layered in. After doing some of the introductory Cloud Skills Boost content and building on GCP myself for a few months, I decided to enroll in the Certification Journey for the Professional Cloud Security Engineer certification. My cohort was scheduled to start in the middle of May, wrapping up by the end of June.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="preparing-for-the-exam">Preparing for the Exam<a href="https://misczak.github.io/blog/googles-professional-cloud-security-engineer-certification#preparing-for-the-exam" class="hash-link" aria-label="Direct link to Preparing for the Exam" title="Direct link to Preparing for the Exam" translate="no">​</a></h2>
<p>The Certification Journey was an interesting program that lays somewhere between formal classroom SANS training an the open free-for-all that is regular Udemy courses with Discords attached. First, you don't undertake the program alone; you are scheduled into a cohort with others who are aiming for the same certification at that time, and have opted into the program themselves. There's a Google Group set up for discussion among members of the cohort on topics covered in the program, although mine was pretty sparsely used.</p>
<p>The other interesting wrinkle to the Certification Journey program is that your cohort is assigned an instructor, who holds group "office hours" once a week for 90 minutes to go over that week's content, walk you through sample questions similar to those found on the exam, and most importantly explain to you why the correct answer is the right answer. I was skeptical of these sessions initially, but soon came to find them incredibly valuable. Our instructor pointed out pitfalls I had missed when reading documentation or playing around in GCP myself - such as the fact that BigQuery has its data access logs enabled by default, the only service in GCP to do so.</p>
<p>The Cloud Skills Boost content is, for the most part, of a very high quality. This content is accessible <a href="https://www.cloudskillsboost.google/subscriptions" target="_blank" rel="noopener noreferrer" class="">outside the Certification Journey program</a> with just a regular license that runs $29 per month or $300 per year. There are some lectures in there that are less interesting than others, but they're usually kept to byte-sized videos (4-7 minutes in length) that make it easy to fit in between other tasks or meetings in your day. The real value of Cloud Slills Boost, however, are the labs, as they spin up an ephemeral project for you to create resources and mess around in. Once the lab is complete, they tear down the project and any resources within. It's great for peace of mind, as you don't have to worry about making sure you disposed of every resource you used in a lab that could end up billing you.</p>
<p>In addition to all this content, each week there was a series of links to documentation, blog posts, and videos for further reading and watching. I held off on watching these until I got through most of the Cloud Skills Boost content, knowing that each one would be part of a deeper dive into a service or feature. In the last week or so before my exam, I was working through these links non-stop and watching any video I could find on each service. There are some services I would probably never get the chance to play around with hands on (like setting up Cloud Interconnect for the first time), but I felt prepared for everything else.</p>
<p>Interestingly, I didn't feel the need to do a practice exam, as the sample questions from the instructor office hours gave me a good preview of what the questions from the exam itself would be like. These sample questions were scenario based, with each possible answer often being a series of steps and actions you would take in GCP - not just simple recall of a service's name or feature. The questions also leaned away from any "gotchas" where the right answer is surrounded by wrong answers that are just spelled differently, or something similar. This setup made me feel confident that I wouldn't have to memorize the exact form of every IAM predefined role or setting, as just knowing  what role types would have what permissions would suffice.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sitting-the-exam">Sitting the Exam<a href="https://misczak.github.io/blog/googles-professional-cloud-security-engineer-certification#sitting-the-exam" class="hash-link" aria-label="Direct link to Sitting the Exam" title="Direct link to Sitting the Exam" translate="no">​</a></h2>
<p>I've done a couple of certification exams before, and the registration and check-in process was pretty similar to those. The exam itself was 45 questions, and I flew through the first ten questions before finding the next thirty-five much more detailed and challenging. However, I didn't have any questions where I was completely at a loss to answer - instead, if I wasn't 100% sure of my answer, I marked it for review and moved on. At the end of my first pass of the exam, I had about nineteen questions marked for review and did another loop through those, which whittled it down to nine questions I still was 50/50 on between two answers. I spent some additional time on those and then submitted my exam. Total time was about one hour, ten minutes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://misczak.github.io/blog/googles-professional-cloud-security-engineer-certification#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h2>
<p>The test results screen immediately showed that I had achieved a provisional pass, but would have to wait for an email from Google Cloud for confirmation. This took about a day and a half to arrive, with links to setup a profile for my badge on Accredible. There was also a token to use in the swag store for those who passed Professional tier GCP exams, which allowed me to have a certification welcome kit to be mailed to me. I've heard in years past this used to be a hoodie or something similar, but the only option on the store available to me was the thermal mug seen in the picture below.</p>
<p><a href="https://misczak.github.io/assets/files/gcpwelcomekit-4f1694708f777202a7903ca70fabeb3e.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="The GCP certification welcome kit I received" src="https://misczak.github.io/assets/images/gcpwelcomekit-4f1694708f777202a7903ca70fabeb3e.png" width="3024" height="4032" class="img_ev3q"></a></p>
<p>All in all, I would say my experience of preparing for and achieving this certification was a positive one. The material did not feel endless, instead really focusing in on the best ways to set up secure organizations and projects in GCP. I came to appreciate the format of the questions, which avoided focusing on what I would call trivia and instead emphasizing really understanding the sequence of steps and nuance needed for good security.</p>]]></content:encoded>
            <category>GCP</category>
        </item>
        <item>
            <title><![CDATA[Automatically tagging resources in AWS with Owner Information]]></title>
            <link>https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information</link>
            <guid>https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information</guid>
            <pubDate>Mon, 31 Jul 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[A detailed write-up on extending an AWS Cloud Operations & Migrations Blog post example for automatically tagging EC2 instances in AWS.]]></description>
            <content:encoded><![CDATA[A detailed write-up on extending an AWS Cloud Operations &amp; Migrations Blog post example for automatically tagging EC2 instances in AWS.<!-- -->
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="background">Background<a href="https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information#background" class="hash-link" aria-label="Direct link to Background" title="Direct link to Background" translate="no">​</a></h2>
<p>One of AWS' best practices for building and managing infrastructure in the cloud is to use consistent, accurate tags for the purposes of cost management, correct owner attribution (during operations and security incidents), and even attribute-based access control. This type of tag compliance can be easier said than done, however, especially when dealing with an organization that has a number of accounts owned by different teams with very different tooling and working styles.</p>
<p>My team has dealt with a number of security incidents over the last year where finding the correct owner of an EC2 instance took too much time, in our eyes, at the beginning of the incident. No tags were added to the instance besides <code>Name</code>, and while CloudTrail would capture the the events that created the resource (and the principal involved), some resources predated our currently retained CloudTrail logs - meaning we did not have a record of the user that created them.</p>
<p>While it is possible to require tags to be added to resources at the time of creation (otherwise blocking the creation of the resource), my team preferred not to pursue this approach for a few reasons. Implementing that type of compliance check would take a very long time to incorporate into the various workflows across the dozens of accounts for which we have oversight. Moreover, restricting resource creation until somebody enters the correct tag information can make the security team look like blockers instead of team players, which is something we are always looking to avoid.</p>
<p>For these reasons, I wanted to see what could be done to leverage information AWS has about our resources in order to attach tags about the owner to the instance's metadata itself without, so it would live alongside the instance for the duration of its lifetime. The preferred approach would not require any additional work on behalf of the individual or team creating the resources in the first place, and not interfere with their existing workflows.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="researching-previous-work">Researching Previous Work<a href="https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information#researching-previous-work" class="hash-link" aria-label="Direct link to Researching Previous Work" title="Direct link to Researching Previous Work" translate="no">​</a></h2>
<p>A lot of times when trying to work on projects such as this one, I end up with the thought process that somebody, somewhere, has to have solved this particular problem before. So I began my research process across the Internet, Reddit, and even the Cloud Security Slack.</p>
<p>What became immediately apparent is that AWS already has this information available to you; however, it's just not in a format (or part of a service) that makes it easy to extract. There is a tag called <code>aws:createdBy</code> that can be activated in the Billing Console via the management account, <a href="https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/aws-tags.html" target="_blank" rel="noopener noreferrer" class="">after which AWS will apply it to a subset of resources</a>.</p>
<p>The only issue with this tag is that it is only available in the Billing console and reports - it does not appear alongside other tags for your resources. There are some <a href="https://github.com/wolfeidau/aws-billing-store/" target="_blank" rel="noopener noreferrer" class="">great projects</a> out there that look to make this report information more easily queryable - but they still didn't feel like the right fit for us, as many of the projects pushed the tag information into a separate repository, with tag information about newly created resources susceptible to a delay.</p>
<p>Finally, I found a <a href="https://aws.amazon.com/blogs/mt/auto-tag-aws-resources/" target="_blank" rel="noopener noreferrer" class="">post from the AWS Cloud Operations &amp; Migrations blog</a> from back in November 2020 that was exactly what I was looking for. It was a solution that used CloudWatch events and a Lambda function to read incoming <code>RunInstances</code> CloudTrail events and use the information from within the event to tag the newly created resource. It even had sample Python code for the Lambda function! There were only one problem with this example that prevented it from being a slam dunk - it was focused on only one account and one region. To make this solution useful in our environment, we would have to extend it to work across all of the accounts in a certain OU, and across all regions.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-auto-tagging-solution-explained">The Auto Tagging Solution, Explained<a href="https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information#the-auto-tagging-solution-explained" class="hash-link" aria-label="Direct link to The Auto Tagging Solution, Explained" title="Direct link to The Auto Tagging Solution, Explained" translate="no">​</a></h2>
<p>In case you don't want to click through to the AWS blog post itself, I'll briefly explain the solution. When a principal starts an EC2 instance in AWS, a <code>RunInstances</code> event is generated and logged by CloudTrail. These events contain a variety of information, including the time of the event, the principal that triggered it, the account that the instance was created in, and the region that the instance was created in. All of this information is extremely useful to capture.</p>
<p><a href="https://misczak.github.io/assets/files/cloudtrailevent-dbb2e6e28a8e9ca65f3860d57bbfbe1e.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="An example of a CloudTrail event for RunInstances" src="https://misczak.github.io/assets/images/cloudtrailevent-dbb2e6e28a8e9ca65f3860d57bbfbe1e.png" width="803" height="709" class="img_ev3q"></a></p>
<p>A service called AWS EventBridge (formerly CloudWatch Events) allows for rules to be created that can monitor CloudTrail for specific events, and then perform some action based on the event. The auto tagging solution includes a rule that monitors for the <code>RunInstances</code> events, and then sends the information from a captured event to an AWS Lambda function. This Lambda function reads the information in the event (mostly the instance ID and principal information), and then uses that information to apply a tag to the instance with the information about the principal.</p>
<p>The chief limitation of this solution, as presented in the blog post, is that CloudTrail in a given region will only capture the <code>RunInstances</code> events that happen in that region. Additionally, a Lambda function would only be able to apply tags to instances in the same account as the function itself.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="modifying-the-aws-example">Modifying the AWS Example<a href="https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information#modifying-the-aws-example" class="hash-link" aria-label="Direct link to Modifying the AWS Example" title="Direct link to Modifying the AWS Example" translate="no">​</a></h2>
<p>The first problem - that the example solution only covered a  single region - can be solved easily enough. Whereas the example used a single Event Rule, we would have to end up deploying the same rule in every region (of every account) that we planned on supporting as part of this solution. This requirement could be met by running a CloudFormation StackSet that created the rule in each region that each account uses.</p>
<p>The second problem - that the example focused on just one account - was a little trickier to think about. Event rules can send events to Lambda functions only in the same account as the rule. We needed a way to send events from other accounts in the organization to our account, and then another mechanism to apply the tags back onto the correct instances in those various accounts.</p>
<p>We ended up creating an event bus in our Security account, solely for the purpose of receiving these resource creation events from other accounts. Each event rule would send their events to this event bus instead of directly to a Lambda function. The bus, in turn, would be the doorway to our Lambda function, as it would live in the same account as the function itself.</p>
<p>Next, we created two IAM roles in each managed account. One role has the <code>PutEvent</code> permission, used by the Event Rule to send events to the event bus in the Security account. The second role has the <code>ec2:CreateTags</code> and <code>ec2:DescribeVolumes</code> permission, and its trust policy allows our Lambda's execution role to assume it. CloudFormation is again used to help deploy these to all accounts in scope. It's best to deploy the CloudFormation for creating the IAM roles first, as you can then reference the <code>PutEvent</code> role in your CloudFormation for deploying the Event Rule itself.</p>
<p>Lastly, we added permissions to our Lambda execution role; namely, a policy to assume the auto tagging role in any account:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"Statement"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Action"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token string" style="color:#e3116c">"sts:AssumeRole"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Effect"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Allow"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Resource"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"arn:aws:iam::*:role/auto-tag-role"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Sid"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ResourceAutoTaggerAssumeRole"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"Version"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"2012-10-17"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>Once the Lambda function was invoked, it would mostly use the code provided by the AWS blog post example. However, we modified a couple of key areas. We used the source code from the <a href="https://github.com/aws-samples/resource-auto-tagger" target="_blank" rel="noopener noreferrer" class="">blog post example GitHub repo</a> and set about modifying <code>resource-auto-tager.py</code>. In the <code>lambda_handler</code> function, we added some code to pull the account ID and region from the CloudTrail event:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Parse the passed CloudTrail event and extract pertinent EC2 launch fields</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    event_fields </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cloudtrail_event_parser</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    accountId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> event_fields</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"account_id"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    region </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> event_fields</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"region"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">info</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"Instance created in Account: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">accountId</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c"> in region: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">region</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">\n"</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<p>We then pass <code>accountID</code> and <code>region</code> to the <code>set_ec2_instance_attached_vols_tags</code> function, alongside the <code>ec2_instance_id</code> and <code>resource_tags</code> as seen in the example. Once inside that function, we use the <code>account_id</code> to assume the IAM role that we deployed to every account for the purposes of applying the tags, and build an EC2 client object on the back of that assumed role session and the region.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">set_ec2_instance_attached_vols_tags</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ec2_instance_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> resource_tags</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> account_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> region</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">try</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token comment" style="color:#999988;font-style:italic"># First assume the role created in the account that has permission to tag the resources</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">info</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Attempting to assume role in target account\n"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            assumed_role_response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sts_client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">assume_role</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                RoleArn</span><span class="token operator" style="color:#393A34">=</span><span class="token string-interpolation string" style="color:#e3116c">f"arn:aws:iam::</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">account_id</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">:role/auto-tag-role"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                RoleSessionName</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"auto-tag-session"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            assumed_role_session </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> boto3</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Session</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">aws_access_key_id</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">assumed_role_response</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'Credentials'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'AccessKeyId'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                        aws_secret_access_key</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">assumed_role_response</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'Credentials'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'SecretAccessKey'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                        aws_session_token</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">assumed_role_response</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'Credentials'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'SessionToken'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            assumed_role_ec2_client </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> assumed_role_session</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">client</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ec2"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> region_name</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">region</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            log</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">info</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Assumed role in target account successfully\n"</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<p>Because we have used CloudFormation to deploy the same role to every account, it will be waiting for us, no matter which <code>account_id</code> gets passed in here and used as part of the ARN. And because the Lambda's execution role is trusted by each of those roles in the various accounts, it will be able to assume it without issue. The rest of the code is almost entirely unmodified.</p>
<p>Stepping back and looking at the complete solution, it can be summarized by this diagram:</p>
<p><a href="https://misczak.github.io/assets/files/autotagdiagram-8a506a9e10a36e91659e195990b4d3ae.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="A simple diagram showing the flow of events through the solution" src="https://misczak.github.io/assets/images/autotagdiagram-8a506a9e10a36e91659e195990b4d3ae.png" width="1700" height="916" class="img_ev3q"></a></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="applying-the-service-control-policy">Applying the Service Control Policy<a href="https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information#applying-the-service-control-policy" class="hash-link" aria-label="Direct link to Applying the Service Control Policy" title="Direct link to Applying the Service Control Policy" translate="no">​</a></h2>
<p>Once the tags are created, we want to prevent someone from removing them, either accidentally or in an attempt to cover their tracks. This can be handled by a pretty straightforward service control policy (SCP) that just blocks the tags that you have decided to apply. In this case, we denied the <code>ec2:DeleteTags</code> action for the <code>owner</code> and <code>dateCreated</code> tags, and attached it to the OU of accounts where we would deploy the solution.</p>
<div class="language-terraform codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-terraform codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">data "aws_iam_policy_document" "auto_tag_delete_policy" {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    statement {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        effect          = "Deny"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        actions         = [</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            "ec2:DeleteTags"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        resources       = ["*"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        condition {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            test        = "ForAnyValue:StringEquals"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            variable    = "aws:TagKeys"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            values      = [</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                "dateCreated",</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                "owner"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            ]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    }</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">resource "aws_organizations_policy" "prevent_auto_tag_delete" {</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    name                = "prevent-auto-tag-modification"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    description         = "Prevents removal of tags automatically applied by InfoSec"</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    content             = data.aws_iam_policy_document.auto_tag_delete_policy.json</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">resource "aws_organizations_policy_attachment" "managed_ou"{</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    policy_id           = aws_organizations_policy.prevent_auto_tag_delete.id</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    target_id           = var.managed_ou</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">}</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="results-and-additional-considerations">Results and Additional Considerations<a href="https://misczak.github.io/blog/automatically-tagging-resources-in-aws-with-owner-information#results-and-additional-considerations" class="hash-link" aria-label="Direct link to Results and Additional Considerations" title="Direct link to Results and Additional Considerations" translate="no">​</a></h2>
<p>Once everything was in place and enabled, tags began being automatically applied to our EC2 instances as they were created.</p>
<p><a href="https://misczak.github.io/assets/files/tagscreated-4ac9ef5d902c1b4c91a1b8e4c4206498.png" target="_blank" class=""><img decoding="async" loading="lazy" alt="An example of the dateCreated and owner tags applied to an instance" src="https://misczak.github.io/assets/images/tagscreated-4ac9ef5d902c1b4c91a1b8e4c4206498.png" width="724" height="235" class="img_ev3q"></a></p>
<p>Through an extensive testing and monitoring process, I learned a few things and wanted to point out some additional things to keep in mind:</p>
<ul>
<li class="">Regions further away from where your event bus and Lambda function are will take a few seconds longer to have tags applied to them, due to round trip times. Make sure you adjust the default Lambda timeout threshold to something like 3 minutes just to make sure you're not losing any tagging due to a slightly delayed event.</li>
<li class="">While the SCP prevents someone from deleting the tags, it does nothing to stop them from modifying them. Overwriting tags falls under the <code>ec2:CreateTags</code> permission, which is what is used to apply these tags in the first place. Therefore, it would be trivial for someone to edit the <code>owner</code> tag after the fact to cover their own tracks. It is recommended you combine this solution with a tool or service that takes routine inventory snapshots of your instance fleet, such as <a href="https://www.cloudquery.io/" target="_blank" rel="noopener noreferrer" class="">CloudQuery</a> so that you can track changes to tags over time.</li>
<li class="">We actually ended up adding an additional tag called "WhatIsThis" with a value that was a URL that pointed to a page on our internal security wiki explaining what auto-tagging was and why it was being applied to resources. While we sent out communications to the account owners and administrators prior to rolling the solution out, there was still going to be some users who would be confused why certain tags were being applied to instances that they had created. Depending on your organization's culture, it may be a good idea to include something like this to prevent a torrent of emails or Slack messages inquiring about the tags.</li>
<li class="">If your organization has teams already using infrastructure as code to manage their resources, they may need to add some configuration to ignore these tags as sources of drift. For example, Terraform has an <code>ignore_tags</code> <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs#ignore_tags" target="_blank" rel="noopener noreferrer" class="">configuration block for the AWS provider</a> that will allow the automatically applied tags to be ignored.</li>
<li class="">For accounts that routinely spin up large numbers of EC2 instances at the same time, make sure you review the current rate limits and quotas for the <code>DescribeInstances</code>, <code>CreateTags</code>, and <code>DescribeVolumes</code> API calls, as you can quickly hit the rate limit when large numbers of resources are created concurrently.</li>
<li class="">Although this solution works great for newly created EC2 instances, it does nothing for instances that already exist. For those use cases, you're probably better off using something like <a href="https://github.com/wolfeidau/aws-billing-store/" target="_blank" rel="noopener noreferrer" class="">Mark Wolfe's excellent project</a> that automates pushing Cost and Usage report information into an Athena table.</li>
</ul>]]></content:encoded>
            <category>AWS</category>
        </item>
        <item>
            <title><![CDATA[Building on GCP]]></title>
            <link>https://misczak.github.io/blog/building-on-gcp</link>
            <guid>https://misczak.github.io/blog/building-on-gcp</guid>
            <pubDate>Mon, 27 Feb 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Discussing my recent experience using GCP and comparing it to AWS.]]></description>
            <content:encoded><![CDATA[<p>Recently, I had to complete a project that involved running the open source tool <a href="https://cloudquery.io/" target="_blank" rel="noopener noreferrer" class="">Cloudquery</a> to create an inventory of resources in a GCP organization. This assignment turned out to be a great introduction to learning how Google Cloud Platform works, as I had almost exclusively used AWS previously (with only minor trials in both Azure and GCP during that time). Throughout the project, I found myself often mapping certain concepts back to what they would be called or how they would be done in AWS. In doing so, I started to keep score of what I liked more than AWS and what I liked less - and now I'm finally sitting down to organize some of those thoughts.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-like-more-on-gcp">What I Like More on GCP<a href="https://misczak.github.io/blog/building-on-gcp#what-i-like-more-on-gcp" class="hash-link" aria-label="Direct link to What I Like More on GCP" title="Direct link to What I Like More on GCP" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-the-organizaton-project-folder-hierarchy">1. The Organizaton-Project-Folder Hierarchy<a href="https://misczak.github.io/blog/building-on-gcp#1-the-organizaton-project-folder-hierarchy" class="hash-link" aria-label="Direct link to 1. The Organizaton-Project-Folder Hierarchy" title="Direct link to 1. The Organizaton-Project-Folder Hierarchy" translate="no">​</a></h3>
<p>In GCP, the top node in your environment is called an organization; under that, you can have projects, which act like AWS accounts but tend to be even more logically segmented. The reason for that strict segmentation is that you'll often place projects under folders to better organize them under a team or department or business unit. Projects allow you to easily create and control very fine grained permissions, as you can place just a few resources in a project to limit the blast radius of any permissions granted there - without having to implement lots of IAM policies.</p>
<p>AWS has a concept of an organization for central management, but it is by no means a requirement to get started. You can still see its DNA as a feature that came later on, as you have to invite existing accounts to the organization. And the management account still runs the show in the organization, so you'll still have one account that quite a bit more powerful than the others.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-almost-every-aspect-of-iam">2. Almost Every Aspect of IAM<a href="https://misczak.github.io/blog/building-on-gcp#2-almost-every-aspect-of-iam" class="hash-link" aria-label="Direct link to 2. Almost Every Aspect of IAM" title="Direct link to 2. Almost Every Aspect of IAM" translate="no">​</a></h3>
<p>Building on the organization-folder-project hierarchy, the IAM structure in GCP immediately seems more logical once you understand how it was constructed. Every principal in GCP is tied to an email address - a byproduct of an organization requiring a domain to get started. That principal's email address can be added to any organization, folder, or project and granted permissions in that entity. These permissions can cascade from the top of the hierarchy downwards; for example, you can grant permissions at the Engineering folder level and have it apply to that principal for every project underneath it.</p>
<p>Where this really becomes powerful (and convenient) is providing different permissions at different levels. For example, I may want to give the service account that my workload will use the 'Security Reviewer' predefined role at the organization level, which grants List permissions for nearly every service as well as Get (Read) permissions for many of them. Those permissions will cascade to every folder and therefore every project in that organization. But then, in a few specific projects, I can add the principal for my service account and grant it more specific, powerful permissions, such as Storage Object Creator to write objects to Cloud Storage Buckets in those projects. Essentially, two CLI commands are needed to set up these different tiers of permissions throughout the organization.</p>
<p><img decoding="async" loading="lazy" alt="The difference in how roles and permissions are assigned - some inherited from the organization and others applied directly in that project" src="https://misczak.github.io/assets/images/iampermissions-50789482b2bd73f5d41e933e178d3682.png" width="2388" height="628" class="img_ev3q"></p>
<p>On the AWS side, I would most likely need to get roles or trust relationships added to every account in my organization for my workload to use to emulate this setup. And even then, the policies for those roles have to probably be tailored to specific resources because each account contains lots of resources my workload shouldn't have access to.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-less-tedious-networking-for-the-most-part">3. Less Tedious Networking (For the Most Part)<a href="https://misczak.github.io/blog/building-on-gcp#3-less-tedious-networking-for-the-most-part" class="hash-link" aria-label="Direct link to 3. Less Tedious Networking (For the Most Part)" title="Direct link to 3. Less Tedious Networking (For the Most Part)" translate="no">​</a></h3>
<p>Some of the biggest differences in comparing GCP to AWS come at the network level; specifically, VPCs in GCP are global and go across regions, while subnets are regional and can go across zones. This allows me to setup a highly available workload spanning multiple regions while using just a single VPC. Additionally, I can spread my subnets across different zones for better fault tolerance. While you can easily emulate this on AWS with multi-region setups, there's some additional effort that goes into designing different VPCs for each region and different subnets for each zone.</p>
<p>The benefits become more clear when you realize that in GCP, you don't attach a CIDR block at the VPC level - instead, it's done at the subnet level. Such a setup creates an interesting tradeoff when it comes to firewall rules for these networks; while you can have a firewall rule apply for an entire global VPC and all of its subnets, that firewall rule is inherently tied to that VPC and cannot be repurposed elsewhere, like you can with security groups.</p>
<p>One last note here - I love how every subnet comes with an option to enable 'Private Google Access', which just allows Compute Engine instances in that subnet to reach the external IP addresses of GCP services even if they only have internal IP addresses. This is a really simple option to toggle without having to worry about setting up a VPC endpoint, as you do in AWS.</p>
<p><img decoding="async" loading="lazy" alt="The toggle for Private Google Access on the subnet creation page" src="https://misczak.github.io/assets/images/privategoogleaccess-2b556367c8236c768879541e8e52f6be.png" width="1132" height="710" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-focus-on-cli-commands-even-in-the-console">4. Focus on CLI commands, Even in the Console<a href="https://misczak.github.io/blog/building-on-gcp#4-focus-on-cli-commands-even-in-the-console" class="hash-link" aria-label="Direct link to 4. Focus on CLI commands, Even in the Console" title="Direct link to 4. Focus on CLI commands, Even in the Console" translate="no">​</a></h3>
<p>For many pages that I visited in the console to create a resource or change a configuration, there was an option near the bottom that allowed me to see and copy the equivalent CLI command for the changes I had just made. By putting this front and center during console use (as opposed to burying it in a reference page somewhere), it became really simple for me to just copy those commands to my personal notes, making my configuration repeatable in the future if I wanted to replicate it in another project or environment.</p>
<p><img decoding="async" loading="lazy" alt="The dialog showing the equivalent command line instruction for creating a network and subnet that was just configured in the console" src="https://misczak.github.io/assets/images/clicommand-3cf4d8618520d1476717d692f24a3018.png" width="1518" height="1204" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-like-more-on-aws">What I Like More on AWS<a href="https://misczak.github.io/blog/building-on-gcp#what-i-like-more-on-aws" class="hash-link" aria-label="Direct link to What I Like More on AWS" title="Direct link to What I Like More on AWS" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-no-domain-requirement">1. No domain requirement<a href="https://misczak.github.io/blog/building-on-gcp#1-no-domain-requirement" class="hash-link" aria-label="Direct link to 1. No domain requirement" title="Direct link to 1. No domain requirement" translate="no">​</a></h3>
<p>As I mentioned above, the organization in GCP is tightly coupled with a domain, for good reason - principals and resources become entities under that domain. But this can be a pain when trying to spin up a new environment as a sandbox for your team or if you just want to start a new project that can be brought under the management of an organization at a later date. While it's easy to undestand that every principal is an email address in GCP, I found myself still preferring to just reference a principal by account number and role name. Some of the email addresses for principals in GCP can get extremely long and be very close in terms of spelling and project IDs - I once granted the permission to the the Compute Engine Service <strong>Account</strong> used for my workload instead of the Compute System Service <strong>Agent</strong>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-apis-are-already-enabled">2. APIs are Already Enabled<a href="https://misczak.github.io/blog/building-on-gcp#2-apis-are-already-enabled" class="hash-link" aria-label="Direct link to 2. APIs are Already Enabled" title="Direct link to 2. APIs are Already Enabled" translate="no">​</a></h3>
<p>The first time you attempt to call the API of a service in a GCP project, you'll have to <em>Enable</em> it which requires you to accept the Terms of Service and billing responsibility for the API. While this is done presumably to prevent a lower privileged user from attempting to use and consume valuable (and costly) resources in your project, it can be extremely counterintuitive, especially when you are trying to engineer and debug tools that are reaching across projects and using lots of different APIs.</p>
<p>In the case of using the CloudQuery tool, I was getting lots of inconsistent results - for example, the tool was not returning any data for certain resources, like Cloud Functions, that I knew existed in the organization. You can get around this by either enabling all of the APIs in your project so that they can be used by your workload, or by granting your workload the <code>serviceusage.services.enable</code> permission so it can enable the APIs on its own as it needs to.</p>
<p><img decoding="async" loading="lazy" alt="GCP requiring you to explicitly enable the API for a service before you can begin using it." src="https://misczak.github.io/assets/images/enableapi-31ceea386f091d83d0098a34310c4fcd.png" width="1542" height="726" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-no-service-accountservice-agents-confusion">3. No Service Account/Service Agents Confusion<a href="https://misczak.github.io/blog/building-on-gcp#3-no-service-accountservice-agents-confusion" class="hash-link" aria-label="Direct link to 3. No Service Account/Service Agents Confusion" title="Direct link to 3. No Service Account/Service Agents Confusion" translate="no">​</a></h3>
<p>As mentioned in the first item of this section, I once granted the <code>Compute Instance Admin</code> predefined role to a GCP service account in my project instead of the Compute System Service Agent. This role was required to allow my Compute Engine instances to make use of Instance Schedules, which would start and stop them at predefined times.</p>
<p>The reason for this mix-up was partially due to the fact that Service Agents are hidden on the IAM page until you toggle the box labeled <strong>Include Google-provided role grants</strong>. I also found it counterintuitive because every other permission I needed for my workload to function correctly was granted to the service account that I had attached to the Compute Engine instance running the workload.</p>
<p>I can't recall having a mix-up like this while using AWS; there is a pretty clear map of what permissions need to be granted to which roles and how you can attach that role to compute workloads (instance profile, IRSA, etc.).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-the-arn">4. The ARN<a href="https://misczak.github.io/blog/building-on-gcp#4-the-arn" class="hash-link" aria-label="Direct link to 4. The ARN" title="Direct link to 4. The ARN" translate="no">​</a></h3>
<p>The Amazon Resource Name, or ARN, is a unique identifier assigned to an AWS resource. An ARN can represent a role, an EC2 instance, an S3 bucket, a VPC, and so on. Maybe it is the fact that I learned AWS first, but I have become so accustomed to looking for the ARN and using that as the identifier for a resource that it felt like my toolbox was missing something without an equivalent in GCP. Instead, for some GCP resources, you'll have a name in the format of something like <code>projects/[PROJECT NAME]/locations/[REGION]/functions/my-function</code>, but that name is not as useful for you in other projects and across the organization. Whereas AWS will often ask you to provide the ARN for a resource in a permissions policy or for a principal in a trust policy, you're more likely to be splitting resources into projects in GCP.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-private-service-connect">5. Private Service Connect<a href="https://misczak.github.io/blog/building-on-gcp#5-private-service-connect" class="hash-link" aria-label="Direct link to 5. Private Service Connect" title="Direct link to 5. Private Service Connect" translate="no">​</a></h3>
<p>While Private Google Access was easier to setup for the use case of a workload in a subnet with only private IPs trying to access Google services, Private Service Connect supports the use case of establishing a one way connection between your VPC to another VPC, whether that is in your organization or another organization - such as those of service providers. This makes it roughly equivalent to AWS' Private Link.</p>
<p>However, where Private Link allows you to <a class="" href="https://misczak.github.io/blog/mongodb-world-2022-talk-look-ma-no-public-ip">create some pretty interesting architectures</a> by providing it with peering for transitive routing and sharing of endpoints, Private Service Connect endpoints used for third party services can only be accessed from within the VPC where they are created, and only by subnets in the same region. You cannot use a peering connection to "hop" to the VPC and region where the Private Service Connect endpoint lives, and then use that to travel further along to your destination in a transitive manner.</p>
<p>The one exception to this is that you can use a Cloud VPN to get to the VPC with the Private Service Connect endpoint; in some cases, it may be recommended to use a Cloud VPN between two VPCs if you want to share a Private Service Connect endpoint between them. Obviously, this introduces additional complexity over the Private Link setup required by AWS.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://misczak.github.io/blog/building-on-gcp#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>All in all, I've enjoyed my time building on GCP, as it's been a great learning experience and a useful method of challenging assumptions that I've developed over time by heavily using AWS. Since I spent some time learning how it does IAM in and out, I feel pretty comfortable using it to support our current project and any future ones that may come up.</p>]]></content:encoded>
            <category>AWS</category>
            <category>GCP</category>
        </item>
        <item>
            <title><![CDATA[AWS re:Invent 2022 Security Recap]]></title>
            <link>https://misczak.github.io/blog/aws-reinvent-2022-security-recap</link>
            <guid>https://misczak.github.io/blog/aws-reinvent-2022-security-recap</guid>
            <pubDate>Thu, 15 Dec 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Highlighting some of the announcements I found most notable in terms of security at AWS re:Invent this year.]]></description>
            <content:encoded><![CDATA[<p>The annual AWS re<!-- -->:Invent<!-- --> conference has come and gone. As usual, there is an overwhelming amount of new product launches, feature enhancements, and other service offering announcements to parse through. You could spend several days just sifting through all of the information on 'new stuff'. What I was interested this time, however, was some of the announcements for security-related services and features, especially those that solve pain points I've experienced in the past.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="amazon-security-lake---preview">Amazon Security Lake - Preview<a href="https://misczak.github.io/blog/aws-reinvent-2022-security-recap#amazon-security-lake---preview" class="hash-link" aria-label="Direct link to Amazon Security Lake - Preview" title="Direct link to Amazon Security Lake - Preview" translate="no">​</a></h2>
<p>This one seemed to get all the press and attention from the jump, and for good reason - it's an easy way to centralize data from security-related sources in both AWS and your on-premises environment. They're doing some automatic conversions to <a href="https://github.com/ocsf/" target="_blank" rel="noopener noreferrer" class="">Open Cybersecurity Schema Framework</a> for supposed interoperability. A number of AWS services like Route 53, CloudTrail, Lambda, S3, Security Hub, Guard Duty, and Macie will support this right away, but you can also create your own source integrations as long as you give your source the ability to write to Security Lake and invoke AWS Glue with the following policies:</p>
<ul>
<li class=""><code>AWSGlueServiceRole</code> Managed Policy</li>
<li class="">The following inline policy:</li>
</ul>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"Version"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"2012-10-17"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"Statement"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Sid"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"S3WriteRead"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Effect"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Allow"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Action"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token string" style="color:#e3116c">"s3:GetObject"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token string" style="color:#e3116c">"s3:PutObject"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token property" style="color:#36acaa">"Resource"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token string" style="color:#e3116c">"arn:aws:s3:::aws-security-data-lake-{region}-xxxx/*"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>However, the aspect of this that really caught my eye was the <a href="https://docs.aws.amazon.com/security-lake/latest/userguide/integrations-third-party.html" target="_blank" rel="noopener noreferrer" class="">ecosystem of third-party integrations</a> they already available for use in the Preview. Security lake has source integrations for a lot of commonly used services like Ping Identity, Okta, Orca, CrowdStrike, Zscaler, and more - but the subscriber integrations is even more interesting. Sure, you can integrate with Splunk or Sumo Logic, but there's also large consulting firms such as PwC, Deloitte, Accenture, and others that offer to do the analysis and anomaly detection for you. Security Lake seems like it could be a boon to firms that act as Managed Security Service Providers (MSSPs) by really streamlining the ability to aggregate and provide access to data from an organization's disparate systems.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cloudwatch-logs-sensitive-data-protection">CloudWatch Logs Sensitive Data Protection<a href="https://misczak.github.io/blog/aws-reinvent-2022-security-recap#cloudwatch-logs-sensitive-data-protection" class="hash-link" aria-label="Direct link to CloudWatch Logs Sensitive Data Protection" title="Direct link to CloudWatch Logs Sensitive Data Protection" translate="no">​</a></h2>
<p>This one is for anyone who has tried to run an App Sec program and get various application teams to go back and fix their logging. Instead of trying to convince teams to spend precious sprint cycles on fixing logging, you can just set up a data protection policy in the application's CloudWatch Log Group and specify the data that you want to have redacted.</p>
<p>This approach doesn't have to be something you continuously come back and check on either - you can have alarms that fire on the <code>LogEventsWithFindings</code> metric that will tabulate how many times sensitive information was redacted in a log group. That metric could also be useful if you want to show improvement across teams as you burn down this particular area of risk. Additionally, you can offload reports of these findings to another log group, S3, or through Kinesis to the destination of your choosing.</p>
<p><img decoding="async" loading="lazy" alt="Redaction of sensitive information in a CloudWatch log group" src="https://misczak.github.io/assets/images/cloudwatchsensitive-bcb0b7b8c8a61014d403e135aff825fe.png" width="2156" height="896" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cloudwatch-cross-account-observability">CloudWatch Cross-Account Observability<a href="https://misczak.github.io/blog/aws-reinvent-2022-security-recap#cloudwatch-cross-account-observability" class="hash-link" aria-label="Direct link to CloudWatch Cross-Account Observability" title="Direct link to CloudWatch Cross-Account Observability" translate="no">​</a></h2>
<p>AWS accounts are often treated as an isolation boundary in organizations, with individual teams having some level of control over their own account, even if they are part of the same AWS organization. However, there may be times when you want to implement some form of log or telemetry data capture from many accounts without imposing an unncessary burden on them with bespoke tooling or excessive permissions.</p>
<p>Cross-Account Observability in CloudWatch sets out to solve exactly that problem by allowing you to designate a central "monitoring account" and one or more "source accounts" that will feed the monitoring account data and logs. Instead of having to manually implement some form of regular data capture-and-forward, CloudWatch will do the plumbing for you, after you provide the list of source accounts and opt-in to the sharing from each source account.</p>
<p><img decoding="async" loading="lazy" alt="Choosing the method to link the source account to the monitoring account in CloudWatch" src="https://misczak.github.io/assets/images/cloudwatchcrossaccount-afd0ee3823522c177d1e6c47061968e1.png" width="1024" height="912" class="img_ev3q"></p>
<p>One caveat with this feature - it will only work for the region it is configured in. If you span multiple regions in the source accounts, you'll have to configure this in each region to feed into the monitoring account in all of those regions.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="vpc-lattice---preview">VPC Lattice - Preview<a href="https://misczak.github.io/blog/aws-reinvent-2022-security-recap#vpc-lattice---preview" class="hash-link" aria-label="Direct link to VPC Lattice - Preview" title="Direct link to VPC Lattice - Preview" translate="no">​</a></h2>
<p>I've been in a lot of AWS networking discussions that involve some combination of VPC Peering, Transit Gateways, Private Endpoints, and VPN attachments. Depending on the requirements, there's often at least one good answer and design that can be solutioned out, but it will often come with some drawbacks - additional infrastructure to set up and manage, additional safeguards that have to be put into place, or even a rearrangement of existing resources in terms of VPC and subnet topology.</p>
<p>VPC Lattice hopes to provide another solution for this type of problem by implementing a logical <strong>service network</strong> to abstract away the realities of networking and allow services in different accounts and VPCs to talk to each other via DNS. If the picture below reminds you of ELBs, you're not wrong - a lot of the same terminology and principals apply.</p>
<p><img decoding="async" loading="lazy" alt="A diagram of a basic VPC Lattice service network setup, with services, listeners, rules, and target groups" src="https://misczak.github.io/assets/images/vpclattice-50fb0a3cf51ed72d4471b1246a30b3fc.png" width="1024" height="576" class="img_ev3q"></p>
<p>There's listeners that dictate what type of traffic is expected; those listeners have rules with priority and conditions to dictate which actions to take to forward traffic to the appropriate group of targets. The <em>really</em> nice part about this service, however, is that you can associate an IAM resource policy with individual services in the network to only allow certain services and principals access to designated services.</p>
<p>It's networking without networking - <a href="https://youtu.be/Pex_0zg9EsE?t=112" target="_blank" rel="noopener noreferrer" class="">and yet it's all still networking</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kms-external-key-store">KMS External Key Store<a href="https://misczak.github.io/blog/aws-reinvent-2022-security-recap#kms-external-key-store" class="hash-link" aria-label="Direct link to KMS External Key Store" title="Direct link to KMS External Key Store" translate="no">​</a></h2>
<p>AWS services that aim to encrypt data at rest use a key for their specific service, known as the data encryption key. However, because the service needs access to that key, it has to remain with the service itself. But this setup means that the key likely lives next to the data it's protecting - if this one area of storage is compromised, everything is lost.</p>
<p>To solve this problem, the data encryption key is itself encrypted by a root key that the customer manages in AWS Key Management Service (KMS). The root key is generated and stored in a hardware security module (HSM) that is tamper resistant. The key material never leaves the HSM. This works fine if you are using KMS to manage your root key and only need to encrypt and decrypt data keys for AWS services - but what happens if you want to integrate with services that don't live on AWS and don't have any connectivity to KMS?</p>
<p>That's where AWS KMS External Key Store (XKS) comes into play. Instead of using AWS KMS and forcing every service to talk to it, your root key can be generated and remain inside an HSM outside of AWS. For services that live outside of AWS, they can talk directly to this external HSM as they normally would. But what about services you may still be using that reside in AWS?</p>
<p><img decoding="async" loading="lazy" alt="A diagram showing how services in an AWS VPC that typically use KMS APIs will talk to the XKS proxy in front of an external HSM" src="https://misczak.github.io/assets/images/xks-4c687920ccd8c03e145351a45833d428.png" width="800" height="298" class="img_ev3q"></p>
<p>With XKS, these AWS services will still make their API calls to AWS KMS; however, KMS will be aware that an External Key Store is configured, and instead forward the requests to your external HSM via an XKS proxy. This proxy is designed to translate the AWS KMS API request to a format that the external HSM expects. This setup lets you run services both inside and outside of AWS with just one location for your root keys that can remain firmly under your control.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://misczak.github.io/blog/aws-reinvent-2022-security-recap#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>That's just a drop in the bucket of announcements. There was also improvements to managing controls in Control Tower, Verified Access Preview (which I have yet to dig into) that aims to allow for secure remote access without a VPN, and more improvements for finding sensitive data in S3 with Macie. Hopefully I'll have time to try out each of them before reInforce sneaks up on me later this year.</p>]]></content:encoded>
            <category>AWS</category>
            <category>Security</category>
            <category>networking</category>
        </item>
        <item>
            <title><![CDATA[MongoDB World 2022 Talk: Look Ma, No Public IP!]]></title>
            <link>https://misczak.github.io/blog/mongodb-world-2022-talk-look-ma-no-public-ip</link>
            <guid>https://misczak.github.io/blog/mongodb-world-2022-talk-look-ma-no-public-ip</guid>
            <pubDate>Tue, 16 Aug 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[A lightning talk about connecting to complex, distributed architectures using only Private Endpoints and VPC/VNET Peering.]]></description>
            <content:encoded><![CDATA[<p>This is a lightning talk I gave at <a href="https://www.mongodb.com/world-2022" target="_blank" rel="noopener noreferrer" class="">MongoDB World 2022</a>.</p>
<!-- -->
<p>As your application continues to grow and scale, you may choose to take advantage of powerful MongoDB Atlas features, such as multi-region clusters and sharding, in order to provide a good user experience. While these features are useful, they can also introduce complexities to your application’s architecture if configured incorrectly. In this session, we’ll look at common architectures when designing multi-region sharded clusters and walk through how MongoDB Atlas allows your team to securely connect to each of the clusters without exposing unnecessary components to the public internet.</p>
<p>You can watch the talk <a href="https://www.youtube.com/watch?v=DQb63kSfqSY" target="_blank" rel="noopener noreferrer" class="">here</a>.</p>]]></content:encoded>
            <category>mongodb</category>
            <category>AWS</category>
            <category>Azure</category>
            <category>Security</category>
            <category>networking</category>
        </item>
        <item>
            <title><![CDATA[Tracking Temperature and Humidity at Home with Time Series Data]]></title>
            <link>https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data</link>
            <guid>https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data</guid>
            <pubDate>Mon, 30 May 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Generating temperature and humidity time series data and using MongoDB's time series collections, charts and window functions to analyze it.]]></description>
            <content:encoded><![CDATA[Generating temperature and humidity time series data and using MongoDB's time series collections, charts and window functions to analyze it.<!-- -->
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="background">Background<a href="https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data#background" class="hash-link" aria-label="Direct link to Background" title="Direct link to Background" translate="no">​</a></h2>
<p>Several years back, I received a Raspberry Pi 3 Model B+ kit from <a href="https://www.canakit.com/" target="_blank" rel="noopener noreferrer" class="">CanaKit</a> alongside <a href="https://amzn.to/3wPWOMI" target="_blank" rel="noopener noreferrer" class="">this book</a>. I spent some time doing the basic stuff with it (blinking LEDs, running a Linux server, etc.) but eventually turning it into a <a href="https://retropie.org.uk/" target="_blank" rel="noopener noreferrer" class="">RetroPie</a> and installing lots of retro games on it. After a while, I lost interest in tinkering with it and just let it collect dust in my office for a number of years.</p>
<p>I was recently cleaning out my office and rediscovered both the old RaspberryPi and the book, and was thumbing through the projects when I noticed one that caught my eye. Listed in the book as Project 12, it's a simple temperature and humidity data logger that didn't really draw my interest a few years ago. Since then, however, I've moved to a new place where the bedrooms are essentially on the third floor (making them much warmer than the rest of the house). Because of this setup, one of the things that my wife and I constantly ask each other is how warm or cold it is in my son's nursery compared to the downstairs portions of the house. With this in mind, I decided to see if I could combine this simple Raspberry Pi project with the new <a href="https://www.mongodb.com/developer/products/mongodb/new-time-series-collections/" target="_blank" rel="noopener noreferrer" class="">Time Series collections</a> that MongoDB offers starting with version 5.0. The idea was to make a tool that would display the current conditions of my son's nursery while also giving me the ability to show how the conditions changed throughout the day and look for patterns if I felt the need to do so.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="acquiring-the-hardware">Acquiring the Hardware<a href="https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data#acquiring-the-hardware" class="hash-link" aria-label="Direct link to Acquiring the Hardware" title="Direct link to Acquiring the Hardware" translate="no">​</a></h2>
<p>To build the temperature and humidity sensor, the book lists the following components:</p>
<ul>
<li class="">Raspberry Pi (obviously)</li>
<li class="">Breadboard</li>
<li class="">DHT22 temperature and humidity sensor (can substitute DHT11 or AM2302 as well)</li>
<li class="">4.7k ohm resistor</li>
<li class="">Jumper wires</li>
</ul>
<p>That's not an overwhelming number of components, but it's still a bunch of pieces that will make for a pretty messy package if you're trying to leave it somewhere in a discreet manner. You would have to connect GND on the Pi to the breadboard's blue rail, 3.3V on the Pi to the breadboard's red rail, and then connect the sensor to the breadboard and Pi as well, using the resistor on the 3.3V connection for DHT22 pin 2.</p>
<p>Instead, I found <a href="https://thepihut.com/products/dht22-temperature-humidity-sensor" target="_blank" rel="noopener noreferrer" class="">this DHT22 sensor</a> that comes with a handy 3-pin wire that you can plug directly into the Pi. DOUT goes to GPIO (pin 4) on the Pi, VCC goes to pin 1 on the Pi, and GND goes to pin 6 on the Pi. Using the case that came with my Pi from CanaKit, I was able to enclose the Pi and just had the wires escape out the top to the sensor, greatly cleaning up the overall appearance.</p>
<p><img decoding="async" loading="lazy" alt="The enclosed Raspberry Pi and DHT22 sensor" src="https://misczak.github.io/assets/images/rpi-ffb42ae5cfc13155eae932a43c5a971e.png" width="999" height="749" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="booting-the-pi">Booting the Pi<a href="https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data#booting-the-pi" class="hash-link" aria-label="Direct link to Booting the Pi" title="Direct link to Booting the Pi" translate="no">​</a></h2>
<p>This was by far the easiest part of this project. The <a href="https://www.raspberrypi.com/software/" target="_blank" rel="noopener noreferrer" class="">Raspberry Pi OS</a> (formerly known as Raspbian) gives you an easy to use Linux OS. Better yet, the <a href="https://www.youtube.com/watch?v=ntaXWS8Lk34" target="_blank" rel="noopener noreferrer" class="">Imager utility</a> helps you set up your install so it will work exactly the way you want right out of the box. You can setup a user for SSH so once it powers up, you can connect directly to it in a headless fashion. Once you have your Pi booted up and are able to SSH into it, the real work begins.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="creating-the-time-series-database">Creating the Time Series Database<a href="https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data#creating-the-time-series-database" class="hash-link" aria-label="Direct link to Creating the Time Series Database" title="Direct link to Creating the Time Series Database" translate="no">​</a></h2>
<p>We'll be using the Time Series collections feature of MongoDB 5.0. The easiest way to get started is to spin up a <a href="https://www.mongodb.com/docs/atlas/tutorial/deploy-free-tier-cluster/" target="_blank" rel="noopener noreferrer" class="">free tier cluster on MongoDB Atlas</a> and then connect in with the new mongo shell, <a href="https://www.mongodb.com/docs/mongodb-shell/" target="_blank" rel="noopener noreferrer" class="">mongosh</a>. You'll also want to make sure you create a database user with credentials that the script can use to connect to the database, as well as add an entry on the IP Access List to allow the connection in the first place.</p>
<p>Once connected to your cluster through <code>mongosh</code>, you'll want to create the time series collection as follows:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">use ts</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">db.createCollection("homeclimate", { </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    timeseries: { </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        timeField: "timestamp", </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        metaField: "metadata", </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        granularity: "seconds" </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    }, expireAfterSeconds: 604800 </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    } )</span><br></div></code></pre></div></div>
<p>The above command does the following:</p>
<ul>
<li class="">Creates a time series collection called 'homeclimate' in the 'ts' database</li>
<li class="">Establishes a granularity of seconds for document ingestion</li>
<li class="">Tells MongoDB that the 'timestamp' field will be used to represent the time of each reading, and the 'metadata' field will hold the information used to identify where the reading is coming from (such as what sensor)</li>
<li class="">Makes documents in this collection expire after 604800 seconds or a little over 7 days, as we don't want to pay for an ever increasing data size and any data older than that is probably of low analytical value</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="scripting-data-collection">Scripting Data Collection<a href="https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data#scripting-data-collection" class="hash-link" aria-label="Direct link to Scripting Data Collection" title="Direct link to Scripting Data Collection" translate="no">​</a></h2>
<p>The projects book linked to the <a href="https://github.com/adafruit/Adafruit_Python_DHT" target="_blank" rel="noopener noreferrer" class="">Adafruit Python DHT library</a> but if you go to the Github repo, you'll see that it's deprecated. The new version is <a href="https://github.com/adafruit/Adafruit_CircuitPython_DHT" target="_blank" rel="noopener noreferrer" class="">CircuitPython</a> which can be easily installed on the Raspberry Pi OS using pip:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">pip3 install adafruit-circuitpython-dht</span><br></div></code></pre></div></div>
<p>From there, it's time to actually start pulling data from the sensor with a continuously running script. Create a new Python script and edit it (I recommend using Visual Studio Code in Remote Mode to make this easier) to contain the following:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> time</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> board</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> adafruit_dht</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> datetime</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> pymongo</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Initial the dht device, with data pin connected to:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dhtDevice </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> adafruit_dht</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DHT22</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">board</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">D4</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Set up python client for MongoDB</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">client </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pymongo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">MongoClient</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">MONGODB CONNECTION STRING</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">db </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ts</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">collection </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">homeclimate</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">sensor </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">while</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">try</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># Pull the values right from the sensor device</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        temperature_c </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> dhtDevice</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">temperature</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        temperature_f </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> temperature_c </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">9</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">32</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        humidity </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> dhtDevice</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">humidity</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># Write a document with the temperature and humidity to the time series collection</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        document </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"metadata"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"sensorId"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> sensor</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"type"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"climate"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token string" style="color:#e3116c">"timestamp"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> datetime</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">datetime</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">utcnow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token string" style="color:#e3116c">"temperature_fahrenheit"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> temperature_f</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token string" style="color:#e3116c">"temperature_celsius"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> temperature_c</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token string" style="color:#e3116c">"humidity"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> humidity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        doc_id </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> collection</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">insert_one</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">document</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">inserted_id</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Recorded reading to document {}"</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">format</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">doc_id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        time</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">sleep</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">except</span><span class="token plain"> RuntimeError </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> error</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># Errors will happen but need to keep going. Can make up for missed readings </span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># in the Time Series collection</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">error</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">args</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        time</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">sleep</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">2.0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">continue</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">except</span><span class="token plain"> Exception </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> error</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        dhtDevice</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">exit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">raise</span><span class="token plain"> error</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    time</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">sleep</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">10.0</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<p>There's a few things going on in this script:</p>
<ol>
<li class="">First, we're setting up the actual sensor by using the driver to build a device object and saying its connected on pin 4 (GPIO) on the Raspberry Pi.</li>
<li class="">We're creating a connection to our MongoDB cluster and specifying the database and collection we'll be storing data in. We also setup this sensor's identifier. Since this was my first sensor, I gave it a sensor ID of 1. Future sensors will increment this value.</li>
<li class="">In a infinite while loop, we grab the temperature and humidity values from the sensor. Since I live in the US, I also convert this temperature to farenheit and put both temperature values alongside with humidity into a MongoDB document. Since we're using a Time Series collection, I have an embedded document with my metadata values - a sensorID and a sensor type that may come in handy in the future as I scale these out. The only other value in the document is the current UTC timestamp, which is critical for helping build the time series view.</li>
<li class="">After taking the reading and writing it to the database, we wait ten seconds until we do it again. This time can be adjusted up or down depending on how granular you want your time series data to be.</li>
</ol>
<p>If you've configured your MongoDB connection string correctly and added the entry in the IP Access List in Atlas, running this script should start writing to your Time Series collection. You should see the results after just a few seconds by going to the Collections view in Atlas:</p>
<p><img decoding="async" loading="lazy" alt="Documents populating the time series collection" src="https://misczak.github.io/assets/images/tscollectionview-49696fb269a68512342abdd1d4e56223.png" width="1978" height="1015" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="using-mongodb-charts-to-visualize-the-data">Using MongoDB Charts to Visualize the Data<a href="https://misczak.github.io/blog/tracking-temperature-and-humidity-at-home-with-time-series-data#using-mongodb-charts-to-visualize-the-data" class="hash-link" aria-label="Direct link to Using MongoDB Charts to Visualize the Data" title="Direct link to Using MongoDB Charts to Visualize the Data" translate="no">​</a></h2>
<p>Now that the readings are coming into MongoDB, you can use the Charts feature of MongoDB Atlas to create a dashboard and display useful information and visualizations derived from the raw data. Start by clicking on the <strong>Charts</strong> tab at the top of MongoDB Atlas, then select <strong>Data Sources</strong> in the left hand menu. On that page, click the <strong>Add Data Source</strong> button in the upper right and then select your cluster. It should only have one database - <code>ts</code> - and one collection - <code>homeclimate</code>. Select those and click <strong>Finish</strong> to set up the data source.</p>
<p><img decoding="async" loading="lazy" alt="Using the time series collection as a Charts data source" src="https://misczak.github.io/assets/images/chartsdatasource-42ec0bc1b6d11155186bdef0e0ad6cc4.png" width="1418" height="734" class="img_ev3q"></p>
<p>Now that the time series collection shows up in the Data Sources list, look to the <em>Pipeline</em> column near the right hand side. Click the <strong>Add Pipeline</strong> button. We're going to use the <a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/" target="_blank" rel="noopener noreferrer" class="">$setWindowFields</a> aggregation operator to build a window function on the time series collection; specifically, we're going to run a calculation on only the readings from the last hour.</p>
<p>Inside the Aggregation Pipeline Edit modal that appears, paste in the following pipeline:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token literal-property property" style="color:#36acaa">$setWindowFields</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">partitionBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"$metadata.sensorId"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">sortBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">timestamp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">output</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">lastHourAverageTemp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">$avg</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"$temperature_fahrenheit"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">window</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token literal-property property" style="color:#36acaa">documents</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">360</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">lastHourAverageHumidity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">$avg</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"$humidity"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">window</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token literal-property property" style="color:#36acaa">documents</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">360</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">]</span><br></div></code></pre></div></div>
<p>This window function creates two new fields for us to use in MongoDB Charts - the <code>lastHourAverageTemp</code> and <code>lastHourAverageHumidity</code>. Note that I used the original Fahrenheit temperature as the source for this new average temperature field - depending on where you're located, you may want to swap in Celsius instead. Since we're collecting readings every 10 seconds in our original script, we go back 360 readings (6 readings a minute multipled by 60 minutes in an hour). Click <strong>Save</strong> when you've pasted this in.</p>
<p>Now click on <strong>Dashboards</strong> on the left hand navigation, then select the <strong>Add Dashboard</strong> button on the far right. Call the dashboard whatever you want. Once it's open, we'll have to add some charts. Click on Add Chart and you'll be brought to the MongoDB Charts editor. If you've never used it before, I recommend checking out the <a href="https://www.mongodb.com/docs/charts/" target="_blank" rel="noopener noreferrer" class="">Charts documentation</a> to check out all the features it has.</p>
<p>For now, we're going to add a simple chart to display the current temperature. In the upper left under data source, select <strong>ts.homeclimate</strong> which should be the only option available. Choose <strong>Text</strong> as the <em>Chart Type</em>, then select <strong>Top Item</strong> as want the most recent temperature reading. Now drag 'timestamp' to the <em>Sort</em> field placeholder, and click the sort button to the right of it to make sure it's in descending order. Drag <code>temperature_fahrenheit</code> to the <em>Display</em> field placeholder and you should now get the most recent reading's Fahrenheit temperature value. Click <strong>Save and Close</strong> at the top.</p>
<p><img decoding="async" loading="lazy" alt="The Current Temperature Chart Configuration" src="https://misczak.github.io/assets/images/currenttemp-b6a9aec952d2438b2c11af34ffc61f26.png" width="1515" height="1002" class="img_ev3q"></p>
<p>Now we can add another chart and do the same exact thing, but instead of the regular 'temperature_fahrenheit' field being used as the reading, we can use the 'lastHourAverageTemp' field that was added via the pipeline we attached to the data source. This will give us a view of the average temperature of the last hour in case the most recent temperature becomes an outlier, like if someone opens a window or starts running a heater.</p>
<p><img decoding="async" loading="lazy" alt="The Last Hour Average Temperature Chart Configuration" src="https://misczak.github.io/assets/images/averagetemp-f014c769f79f424119062e630bf4bfaf.png" width="1749" height="1044" class="img_ev3q"></p>
<p>Then we can add a third chart to visualize the change in temperature over time from the sensor. Add a new chart and this time, for <em>Chart Type</em> select <strong>Line</strong>. In the Encode tab, for the <em>X-Axis</em> select <code>timestamp</code> and turn on Binning, selecting <em>Hour</em> from the dropdown. For the <em>Y-Axis</em> select <code>temperature_fahrenheit</code>, and under the Aggregate dropdown select <em>Mean</em>. The combination of these two settings will combine all of an hour's readings into one group and take the average to represent that hour on the line chart. Finally, under <em>Series</em> we can select <code>sensorId</code> under <code>metadata</code>. While we only have one sensor currently, if we add more sensors in the future, this will show different lines for each sensor.</p>
<p>Now on the Filter tab, drag <code>timestamp</code> in as the filter and select <em>Period</em>, <em>Previous</em>, and <em>3 Day</em> to limit the chart to only consider the previous three days worth of data. Again, you can adjust this period up or down to suit your needs.</p>
<p><img decoding="async" loading="lazy" alt="Visualization of the Hourly Change in Temperature" src="https://misczak.github.io/assets/images/changeintemp-882d7c692f4ea8afeb8b217d7ed74840.png" width="1919" height="1201" class="img_ev3q"></p>
<p>If you save and close, you'll have a nice dashboard of three charts showing you some basic temperature inforamtion from your sensor. As your sensor continues writing data, the line chart will become more useful as well. You can repeat this process to create three charts with the humidity data as well to give you a full representation of readings from the sensor.</p>
<p><img decoding="async" loading="lazy" alt="The Complete Dashboard of Temperature and Humidity Data" src="https://misczak.github.io/assets/images/completedashboard-2d797cd60fa9562a8ab05f6d19d8bd89.png" width="1793" height="933" class="img_ev3q"></p>]]></content:encoded>
            <category>mongodb</category>
            <category>raspberry pi</category>
            <category>python</category>
        </item>
        <item>
            <title><![CDATA[Building Event Driven Experiences with MongoDB Realm and AWS]]></title>
            <link>https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws</link>
            <guid>https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws</guid>
            <pubDate>Sun, 20 Mar 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Using MongoDB Realm triggers and functions to connect to AWS services.]]></description>
            <content:encoded><![CDATA[<p>Most developers by now are familiar with MongoDB, a NoSQL database that stores data as documents instead of rows. MongoDB's fully managed service product, MongoDB Atlas, comes with MongoDB Realm, which is a set of services that help facilitate mobile and web development by providing a scalable, serverless backend for your application. Realm offers a lot of services (more than we can cover in just this post), but today I wanted to focus on how to use two of them in tandem to help connect a MongoDB Atlas database into a distributed, event driven architecture that can be built on AWS.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pre-requisites">Pre-requisites<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#pre-requisites" class="hash-link" aria-label="Direct link to Pre-requisites" title="Direct link to Pre-requisites" translate="no">​</a></h2>
<p>Before we get started, there are a couple of things you should have setup already. These include:</p>
<ol>
<li class="">
<p>An AWS account with an IAM user created, and an access key created for that IAM user. The user should have permissions to create, edit, and retrieve parameters from AWS Systems Manager and put objects, list buckets, and get objects from S3.</p>
</li>
<li class="">
<p>A MongoDB Atlas account. It is <a href="https://www.mongodb.com/cloud/atlas/register" target="_blank" rel="noopener noreferrer" class="">free to sign up</a>, and there is a free tier that you can use to follow along with this post. The Community version of MongoDB does not include MongoDB Realm, so this post does not apply to it.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="emulating-an-application-environment">Emulating an application environment<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#emulating-an-application-environment" class="hash-link" aria-label="Direct link to Emulating an application environment" title="Direct link to Emulating an application environment" translate="no">​</a></h2>
<p>We're going to have to setup a few things to make this look and feel like an environment a real application is using. Most modern applications are not just writing data to a database and reading it back; rather, they are consuming events from/publishing events to a message queue or stream, interacting with cloud storage, and relying on slight changes to data to kick off workflows. To keep things simple, our goal will be to monitor our database for new documents that are inserted and to upload a copy of them to S3 at the exact moment they are written to the database. In our theoretical application, we are using the copy of the document to do some additional processing or analysis with an AWS service that is reading from S3.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="set-up-mongodb-cluster">Set up MongoDB Cluster<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#set-up-mongodb-cluster" class="hash-link" aria-label="Direct link to Set up MongoDB Cluster" title="Direct link to Set up MongoDB Cluster" translate="no">​</a></h3>
<p>Log into your MongoDB Atlas account. Create a project to act as the container for your cluster, then create a cluster. If you want to use the free tier, choose the <strong>Shared</strong> category at the top of the Create Cluster page. Use a recommended, free-tier region such as <code>us-east-1</code> or <code>us-east-2</code> and then scroll down select the <strong>M0 Sandbox</strong> Cluster Tier. Change the name of the cluster from <strong>Cluster0</strong> to <strong>Sandbox</strong> and click on <strong>Create Cluster</strong>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="set-up-the-s3-bucket">Set up the S3 Bucket<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#set-up-the-s3-bucket" class="hash-link" aria-label="Direct link to Set up the S3 Bucket" title="Direct link to Set up the S3 Bucket" translate="no">​</a></h3>
<p>While the MongoDB cluster is creating, head over to the AWS console and login as your IAM user. Go to Amazon S3 in the AWS console and choose <strong>Create bucket</strong>. Give it a name like <code>{yourname}-event-bucket</code> and select the same region that you created your MongoDB Atlas cluster in. Leave the other options as the default selections and click <strong>Create bucket</strong> at the bottom of the page.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="set-up-parameters-in-ssm">Set up parameters in SSM<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#set-up-parameters-in-ssm" class="hash-link" aria-label="Direct link to Set up parameters in SSM" title="Direct link to Set up parameters in SSM" translate="no">​</a></h3>
<p>In the AWS console, search for AWS Systems Manager (SSM) and navigate to the service page. Once there, select <strong>Parameter Store</strong> on the left hand navigation, under Application Management. Select <strong>Create Parameter</strong>.</p>
<p>On the Create parameter page, enter <code>event-target-bucket</code> as the name for the parameter. Leave the Tier as Standard, the Type as String, and the Data type as text. Under value, enter the name of the S3 bucket you created in the previous step.</p>
<p><img decoding="async" loading="lazy" alt="Setting the SSM Parameter for S3 bucket name" src="https://misczak.github.io/assets/images/ssm-76b82a5f3c1962d6b35f7cafa43db5c5.png" width="896" height="829" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuring-mongodb-atlas-and-realm">Configuring MongoDB Atlas and Realm<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#configuring-mongodb-atlas-and-realm" class="hash-link" aria-label="Direct link to Configuring MongoDB Atlas and Realm" title="Direct link to Configuring MongoDB Atlas and Realm" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="load-data-into-cluster">Load Data Into Cluster<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#load-data-into-cluster" class="hash-link" aria-label="Direct link to Load Data Into Cluster" title="Direct link to Load Data Into Cluster" translate="no">​</a></h3>
<p>Return to the MongoDB Atlas console. When your sandbox is finished provisioning, click the <strong>...</strong> button and choose <strong>Load Sample Dataset</strong>. This will give us a few databases and collections of data to use for our testing purposes.</p>
<p><img decoding="async" loading="lazy" alt="Loading sample data into MongoDB Atlas" src="https://misczak.github.io/assets/images/atlassandbox-272a484a71947136b5edb05cc44bede9.png" width="735" height="391" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="set-up-realm-app">Set up Realm App<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#set-up-realm-app" class="hash-link" aria-label="Direct link to Set up Realm App" title="Direct link to Set up Realm App" translate="no">​</a></h3>
<p>While that data is being loaded, click on the <strong>Realm</strong> tab near the top of the screen. We want to select <strong>Create a New App</strong> and name it <code>AWS-Event-App</code>. Under "Link your Database", select <strong>Use an existing MongoDB Atlas Data Source</strong> and choose the Sandbox cluster you created. Then click <strong>Create Realm Application</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Creating a MongoDB Realm App" src="https://misczak.github.io/assets/images/createrealmapp-d1c9662551f7ac2e6c9da097b37cf352.png" width="920" height="655" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="set-up-realm-values-and-secrets">Set up Realm Values and Secrets<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#set-up-realm-values-and-secrets" class="hash-link" aria-label="Direct link to Set up Realm Values and Secrets" title="Direct link to Set up Realm Values and Secrets" translate="no">​</a></h3>
<p>In order to hook into our AWS account and use the bucket and the parameter we just set up, we have to authenticate to the account from MongoDB Realm. Since we'll be using our AWS IAM user's access key and secret key, we don't want to hardcode it into any code we end up writing or reading from source control. Therefore, we will use Realm's Values feature to securely store these values.</p>
<p>Realm's Values feature allows you to store two types of values - regular values and secrets. Secrets cannot be directly accessed by the Realm API; instead, they must be mapped to their own regular value in order to be able to be retrieved. This prevents you from inadvertantly exposing a secret you did not intend to.</p>
<p>Click on <strong>Values</strong> in the left hand navigation of your Realm app. Click on <strong>Create New Value</strong>. For the value's name, enter <code>AccessKeyID</code>. Choose <code>Value</code> as the type, then for Content select <code>Custom Content</code> and paste in your Access Key wrapped in double quotations to mark it as a string. Then click on <strong>Save Draft</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Setting up the Access Key ID as a Realm Value" src="https://misczak.github.io/assets/images/realmvalue1-d2563c1a8a5fbf510d2f73ddb7b5fd46.png" width="1219" height="658" class="img_ev3q"></p>
<p>Click on <strong>Create New Value</strong> again. Enter <code>SecretAccessKeySecret</code> as the value name, and this time select <code>Secret</code> under Type. For the value of secret, paste in your AWS IAM user's Secret Access Key, then click <strong>Save Draft</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Setting up the Secret Key as a Realm Secret" src="https://misczak.github.io/assets/images/realmsecret-d0a57fd15978d2a581e3ddbe45821192.png" width="1224" height="465" class="img_ev3q"></p>
<p>We've entered the Secret Access Key, but we cannot directly access it via the Realm API since it is a secret. We now have to create a value and map it to that secret. Click on <strong>Create New Value</strong> a third time. Enter <code>SecretAccessKey</code> and select <code>Value</code> as the type. Under Add Content, choose <code>Link to Secret</code> and select the <code>SecretAccessKeySecret</code> you just created. Click <strong>Save Draft</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Linking a Realm Value to the Realm Secret" src="https://misczak.github.io/assets/images/realmvalue2-a153ec8b22b6345f4e6d59b595392609.png" width="1233" height="522" class="img_ev3q"></p>
<p>We have one more value to set up before we're done. Click on <strong>Create New Value</strong> again, and this time enter <code>Region</code> as the value name. Leave Type as <code>Value</code> and Add Content as <code>Custom Content</code>. In the content field, enter the AWS region where you set up your S3 bucket and SSM parameter, again wrapped in double quotations. Then click <strong>Save Draft</strong>.</p>
<p>You may ask why we bothered with SSM at all to store the parameter of the S3 bucket name. It is true that we could also store the bucket name here in Realm's values; however, for the scope of the post we will assume the application environment has already been using SSM for parameters such as these, and therefore it will be less work for us to leave it that way.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="create-realm-functions">Create Realm Functions<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#create-realm-functions" class="hash-link" aria-label="Direct link to Create Realm Functions" title="Direct link to Create Realm Functions" translate="no">​</a></h3>
<p>Now we are ready to start writing some code. First, let's bring in the AWS SDK which we will use in our code to make calls to the specific AWS services we will use. Select <strong>Functions</strong> in the left hand navigation then click the <strong>Dependencies</strong> tab at the top of the page. Select <strong>Add Dependency</strong>. On the modal that appears, enter <code>aws-sdk</code> as the package name. You can leave the version blank, but if you run into issues, you may want to come back and change the version to 2.737.0, which is what I used to write this post. When you're done, click <strong>Add</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Adding the aws-sdk library as a dependency" src="https://misczak.github.io/assets/images/realmnpmlibrary-5338e908fac75cc732216e98515c7b1e.png" width="592" height="411" class="img_ev3q"></p>
<p>The <code>aws-sdk</code> node library should be added as a dependency. Now click <strong>Create New Function</strong> button in the upper right. In the Add Function page, enter <code>MoveDocToQueue</code> as the function and for now choose <code>System</code> under Authentication. Click on the <strong>Function Editor</strong> tab at the top, and paste this code in over what is currently there.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function-variable function" style="color:#d73a49">exports</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">AWS</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">require</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'aws-sdk'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> config </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">accessKeyId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">values</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"AccessKeyID"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">secretAccessKey</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">values</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"SecretAccessKey"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">region</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">values</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Region"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token maybe-class-name">SSMparams</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">Name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'event-target-bucket'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">WithDecryption</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> doc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">fullDocument</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">SSM</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">AWS</span><span class="token class-name punctuation" style="color:#393A34">.</span><span class="token class-name">SSM</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">config</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> ssmPromise </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">SSM</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">SSMparams</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">promise</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  bucketName </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> ssmPromise</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Parameter</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Value</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token maybe-class-name">S3params</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">Bucket</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> bucketName</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">Key</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"queue-"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">fullDocument</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">_id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">Body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> doc</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">S3</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">AWS</span><span class="token class-name punctuation" style="color:#393A34">.</span><span class="token class-name">S3</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">config</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> s3Promise </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">S3</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">putObject</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">S3params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">promise</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  s3Promise</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">then</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">function</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'Put Object Success'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">function</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div>
<p><img decoding="async" loading="lazy" alt="Configuring the Realm Function" src="https://misczak.github.io/assets/images/realmfunction1-87f5677075253b0bc2097a60694209d1.png" width="1223" height="542" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="Editing the code of the Realm Function" src="https://misczak.github.io/assets/images/realmfunction2-32335381231704645ccc982f9f91623f.png" width="1237" height="750" class="img_ev3q"></p>
<p>Normally with Realm functions, you can click on <strong>Run</strong> near the bottom right to test out the function. However, since this function will require a change event in the database to run, we will need to map it to a trigger first and modify some data to test it out. For now, click on <strong>Save Draft</strong>.</p>
<p>We now have the actual code we want to run to move documents to our S3 bucket. But how do we get it to run whenever we insert data? The answer is a Realm Trigger. Select <strong>Triggers</strong> on the left hand navigation, then add a new trigger.</p>
<p>On the Add Trigger page, enter <code>moveDocToQueueTrigger</code> as the name, then select your Sandbox cluster under Trigger Source Details. For database, select <code>sample_mflix</code> and for collection select <code>movies</code>. Under Operation type, ensure <code>Insert</code> is checked as we want this trigger to fire on every new document inserted into the sample_mflix.movies collection. Keep scrolling down and ensure that Full Document is turned to <code>ON</code>. Under Select an Event Type, choose Function and select the <code>MoveDocToQueue</code> function that we just created. Then click <strong>Save</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Configuring the Realm Trigger Part 1" src="https://misczak.github.io/assets/images/realmtrigger1-a401e76e59b7ba18a829d9993d65f7c6.png" width="1196" height="856" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="Configuring the Realm Trigger Part 2" src="https://misczak.github.io/assets/images/realmtrigger2-98b72abf58dbc3b9ef13e832ac21d1fb.png" width="1186" height="561" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="deploying-and-testing">Deploying and Testing<a href="https://misczak.github.io/blog/building-event-driven-experiences-with-mongodb-realm-and-aws#deploying-and-testing" class="hash-link" aria-label="Direct link to Deploying and Testing" title="Direct link to Deploying and Testing" translate="no">​</a></h2>
<p>We are now ready to try our first test! On the blue banner at the top of the page, you can now choose to Deploy the changes to your Realm app. The values, secrets, function, and trigger should now be ready to go. Open up a connection to your MongoDB cluster and insert a new document into the <code>sample_mflix.movies</code> collection. You can do this via the <a href="https://www.mongodb.com/try/download/shell" target="_blank" rel="noopener noreferrer" class="">mongo shell</a>, a MongoDB driver, <a href="https://www.mongodb.com/products/compass" target="_blank" rel="noopener noreferrer" class="">MongoDB Compass</a>, or just by using the Data Explorer built into Atlas to duplicate an existing document in that collection. Keep in mind that if you are using the shell, a driver, or Compass, you will need to <a href="https://docs.atlas.mongodb.com/security/add-ip-address-to-list/" target="_blank" rel="noopener noreferrer" class="">add your current IP address to the IP Access list</a> in your Atlas project.</p>
<p>After you have inserted a new document, return to MongoDB Realm and select Logs in the left hand navigation. You should an entry in the History showing when your trigger was invoked. Expanding the entry will show you the logs from that particular function - if you set up your values and secrets correctly, you should see the log message "Put Object Success", meaning that our attempt to put the document into the S3 bucket appears to be successful. (If you do not see this output, double check your values and secrets, your IAM user permissions, and your S3 and SSM configuration.)</p>
<p><img decoding="async" loading="lazy" alt="Seeing the log output showing a successful upload" src="https://misczak.github.io/assets/images/logoutput-ef3f2ec72050272ab8ce95dd49fa530c.png" width="957" height="491" class="img_ev3q"></p>
<p>Let's double check that this actually sent the document to S3 by going back to our AWS console and navigating to the S3 service page. Find the bucket that you created at the beginning of this process and open it up. You should see an object in there starting with <code>queue-</code> and then the _id value of the document you inserted. The object is named this way because we set this as the Key value when setting up the S3 parameters in our Realm function. If you see the document, then your setup is successful!</p>
<p><img decoding="async" loading="lazy" alt="Seeing the document show up in S3" src="https://misczak.github.io/assets/images/s3result-31ed0b3832e9ffa3557053b2477e26cb.png" width="1199" height="265" class="img_ev3q"></p>
<p>You can continue experimenting by bringing in other AWS services as well, such as sending the document to a Kinesis data stream instead of just an S3 bucket.</p>]]></content:encoded>
            <category>mongodb</category>
            <category>AWS</category>
            <category>nodejs</category>
        </item>
    </channel>
</rss>