Adventures of a YAML engineer

I want to brag about a bit of YAML code I wrote back in March for SecureDrop's completed migration to Ubuntu Noble that I neglected to mention in the blog post explaining the technical details. Yes, YAML, is a programming language.

We offered SecureDrop Administrators the option for a "semiautomated" upgrade: they run one command, ./securedrop-admin noble_migration, and it'll take care of the rest. The main advantage for doing so was that the upgrade would happen at the time you chose, and if something happened to go wrong, you were already on hand to deal with it!

Under the hood the semiautomated upgrade was starting an Ansible playbook that edited our JSON control file to mark the server as ready to be upgraded and then started the systemd service. And then it just waits until the upgrade completes, which ended up being the harder part to implement.

During the upgrade, the server reboots twice (once before installing updates and once after), which means Ansible will lose its SSH connection. I used Ansible's wait_for_connection module to reconnect instead of error out, and naively had it wait for that to happen twice before checking if the upgrade had finished.

But during testing we found a problem when using SSH-over-Tor, in which Ansible would disconnect three times. It disconnected on the first pre-upgrade reboot, then during the upgrade when the Tor package was restarted, and then again during the second post-upgrade reboot.

And, to make it even more fun, this was subject to a race condition. In at least one instance, it took long enough for Tor to come back that the server rebooted before it reconnected, so there were only two disconnections.

Knowing that, a naive solution wasn't going to cut it anymore, so I implemented the same state machine as the Rust code, just in the YAML playbook. It now parsed the JSON state file, looked up where in the overall process it was, and then calculated how many reboots are likely remaining. Once it disconnected and reconnected, it looked at the state file again, so it knew how many more to expect.

Here's the end result, it ended up being just over 200 lines of YAML (including comments).

Alternative clickbait titles for this post include: "Porting some of my Rust code to YAML" and "Writing a state machine in YAML".


A small change in plans

A small change in plans: I'm starting law school in the fall. I'll be attending the CUNY School of Law right here in Queens to become a public interest-focused lawyer.

I plan to continue working full time at the Freedom of the Press Foundation and go to school in the evenings, part time. And yes, law school is something I have always wanted to attend.

Going forwards you'll probably see me switch up the standard disclaimer to something like IANALY (I Am Not A Lawyer Yet).


Thoughts from vibecoding

I've been vibecoding for a while now and wanted to share some thoughts that I haven't seen discussed elsewhere regarding the process. To quote the original vibecoding definition by Andrej Karpathy:

It's not too bad for throwaway weekend projects, but still quite amusing. I'm building a project or webapp, but it's not really coding - I just see stuff, say stuff, run stuff, and copy paste stuff, and it mostly works.

And that's pretty much what I do as well. Some of vibecoded projects are on my website (said listing was also vibecoded — but all the text was written by hand), there are plenty more sitting in my ~/Downloads folder that I haven't bothered to clean up and publish.

Also note that I haven't gotten started with the whole "agentic" thing yet, so I'm just still typing prompts into a web browser and copying code out of it.

My standards have gone up#

Being a software engineer generally means that when something goes wrong with computers, you have a rough idea of what might have caused the error. Usually my response is along the lines of, well, all software is buggy, that's life.

These days I'm interested in seeing if I can do something better, both to make my life better, but also to just prove that we can make better software. As an example, a few months ago, I was filming a video for work and was relying on a random teleprompter website for our script.

I was struggling a bit with getting the emphasis right; the website didn't support any type of bold or italic formatting. Knowing what I know about HTML and JavaScript, I guessed that the website was treating everything as plain text, and that in theory, could support rich text if it used contenteditable.

I spent 10 minutes prompting Claude and then an hour fixing small bugs in the CSS/JS, and boom, an "Improved Teleprompter".

When the MTA released a new mobile app, it was okay, but not exactly the way I wanted my routes laid out. I built my own subway stops tracker that presents upcoming trains just how I want to see them, plus it stores all my data locally.

This doesn't necessarily mean the code is better, but rather the user experience is.

JavaScript is privacy-friendly#

Main article: Thinking of JavaScript as privacy-friendly tech

For the longest time, my stance was that JavaScript was a privacy-violating technology that needed to be avoided. I don't think that stance was originally irrational, given that most online tracking uses it, and tools like NoScript and RequestPolicy/uMatrix were effective at stopping that.

But today I think JavaScript has just as much to offer as privacy-friendly technology through things like client-side cryptography but also just keeping computing local instead of happening on someone else's computer.

You can get pretty far with JavaScript, localStorage, and optionally, some remote APIs. (Though CORS can be super annoying...)

Modifying is harder, but#

Modifying vibecoded projects is definitely harder because I didn't write the original code, and often it's not in the style I would have used. But I've found it's usually easy enough to just start over from scratch, replaying your original prompts, but with whatever modifications I needed.

I expect this is an area that I might have a different experience once I get around to agentic LLMs.

Content-Security-Policy is great#

This is not really news, I think it's well understood that the introduction of Content-Security-Policy has significantly improved web security. And that extends to vibecoded projects, especially if you haven't 100% reviewed everything.

The first version of my JSON diff tool that Claude created had a pretty glaring XSS that I noticed and fixed right away. But even if I hadn't, I deployed it with a policy of script-src 'self', so any inline <script> execution would be blocked.

In general it's nice to be able to clearly see and understand exactly what external websites (if any) these small projects will connect to.

I also really like how easy it is to share single-file HTML/CSS/JS projects (pages?), but having inline styles and scripts requires a weaker CSP. So I wrote resource-rewriter to use a single-file for deployment and then automatically move them to separate files during deployment.


In support of Zohran Mamdani

Unfortunately I don't have 8.3 million dollars to spend in support of a political candidate, but I do have my blog.

In the ongoing New York City mayor's race (specifically the Democratic primary), I'm supporting, canvassing and voting for my assembly member, Zohran Mamdani. His entire platform is centered around making NYC more affordable, specifically:

  1. freezing the rent for rent-stabilized tenants (previously done by de Blasio)
  2. making buses fast and free (he won a 1-year pilot on this, it was reasonably successful)
  3. free childcare (I didn't have a parenthetical for this)

This is not to mention his various plans to build more housing, both creating new public housing and speeding up construction of private housing.

And he has a plan to create a "Department of Community Safety", which will task dedicated professionals and mental health experts on helping people with homelessness and other crisis response. And that will let police do actual police things.

I know for sure that he can deliver on the first part of his platform, freezing the rent, since the mayor appoints all the members of the rent control board. The rest requires collaboration from the city council and most likely Albany.

It's not a guarantee that it's possible, but if he wins, there will be a public mandate for it, and suddenly, it'll be realistic.

Ultimately I want a mayor who is willing to try new ideas instead of constantly being stuck doing what is "safe" and continuing old policies that have gotten us here. Zohran is that person and my #1 vote.

Brad Lander#

The more I learn about Brad Lander, the more I like him. Out of all the candidates (including Zohran), I think he is best suited to hitting the ground running as mayor on day one. He seems to have the best grasp on the NYC bureaucracy and has incredibly detailed and technical plans on how to address, well, everything.

I ranked him #2 (in line with Zohran's cross endorsement), but respect and support anyone who ranks him #1 and Zohran #2. If Zohran ends up winning, I hope he gives Brad Lander a significant role in his administration.

Don't Rank Evil Andrew for Mayor#

I never actually lived in New York during Andrew Cuomo's tenure, but I've read enough from the time and everything that's come out since. The fact that he was governor for 10 years, and HUD secretary for another 4 means that he had the opportunity to fix it in the past, but didn't. It's time for new leadership.

I think this is a perfect example showing that letting people voluntarily resign under pressure is a bad idea; if he had been impeached and removed from office, there wouldn't have been a comeback.

Final thoughts#

Zohran has been a great representative for me, and I am looking forward to sharing him with the rest of the city.

I told someone once that I'm supporting Zohran because as my assembly member, he's the first elected official to represent me that I'm not embarrassed by. I don't mean that we agree on everything (we mostly do, but not 100%) — rather I think he has a good set of core guiding principles, and sticks by them in ways that are understandable and justifiable.

After having an incredibly embarrassing mayor for the past 4 years, I'm looking forward to one I respect and appreciate. I hope you'll rank Zohran #1 (and Lander #2).

Voting is open today, June 22 (9am-5pm), and then again for the last time on June 24 (6am-9pm).


Creating IPv4-only and IPv6-only containers with podman

By default, newer versions of podman run containers with a dual stack network that supports IPv4 and IPv6 (yay). But if you're doing something specific, you can set up IPv4-only and IPv6-only networks.

(Note: I tested this all with rootless podman 5.5.0, the current version in Fedora 42.)

I'm primarily writing this because it took me a while to figure this out, I got entirely tripped up by the --ipv6 option which turned out to not be what I wanted, despite the name implying it enables IPv6.

The documentation for it is technically accurate, as it says:

Enable IPv6 (Dual Stack) networking. If no subnets are given, it allocates an ipv4 and an ipv6 subnet.

The most important part is in parenthesis — it enables a dual-stack network. Which means that passing --ipv6 when creating a network doesn't just enable IPv6, it also enables IPv4!

Real IPv6-only#

What you actually want is:

$ podman network create --subnet fd00::/64 --gateway fd00::1 ipv6-only

You can verify that IPv4 doesn't work by:

$ podman pull quay.io/curl/curl:latest
$ podman run --rm -it --net=ipv6-only curl -v4 https://en.wikipedia.org
* Host en.wikipedia.org:443 was resolved.
* IPv6: (none)
* IPv4: 208.80.154.224
*   Trying 208.80.154.224:443...
* Immediate connect fail for 208.80.154.224: Network unreachable
* Failed to connect to en.wikipedia.org port 443 after 13 ms: Could not connect to server
* closing connection #0
curl: (7) Failed to connect to en.wikipedia.org port 443 after 13 ms: Could not connect to server

And that IPv6 works:

$ podman run --rm -it --net=ipv6-only curl -I6 https://en.wikipedia.org
HTTP/2 301 
date: Fri, 23 May 2025 00:29:41 GMT
...

IPv4-only#

And now for IPv4, which is even simpler:

$ podman network create ipv4-only

Yep, no options needed, you just need a network in which IPv6 is not enabled by the subnet and doesn't pass the --ipv6 flag.

Final notes#

The default networking stack for rootless containers is documented (under "pasta") as "IPv4 and IPv6 addresses and routes, as well as the pod interface name, are copied from the host". In my testing this is correct, but this is an entirely separate thing from podman network that appears to exist by default, which is IPv4-only.

I ended up figuring out the whole misleading --ipv6 thing thanks to a GitHub comment, which explicitly spelled out "The --ipv6 flags means dual-stack", and even explained the rationale why: "this is fully compatible with docker ..."

I shouldn't be too surprised that Claude also got tripped up by the --ipv6 flag and gave me bad advice. ¯\_(ツ)_/¯

Final final note: if you try a plain podman run curl ... without first pulling the image, it won't know which image you actually want, and none of the three prompts it gives you (registry.fedoraproject.org, registry.access.redhat.com, docker.io/library) are the official upstream image. I've submitted a PR to the containers/shortnames repo to fix that, so a plain curl image name will automatically be aliased to the upstream image.


The point release that upgrades your entire OS

For the past two weeks, we've been putting out SecureDrop point releases that, as the title gives away, upgraded the underlying server's entire operating system. Definitely the kind of change you expect when upgrading from 2.12.3 to 2.12.4.

(An alternative title for this post could've been "Committing semver crimes" — thankfully we don't strictly follow semver.)

We've previously written about how the migration works on a technical level, I wanted to write a little bit about how we rolled it out. The hook about versioning was just clickbait, I couldn't care less about version numbers in this context.

A few relevant pieces of context:

SecureDrop is set up with two servers: the Application Server hosts the web interface as an onion service, while the Monitor Server runs an intrusion detection system and fires off email notifications. Both servers run unattended upgrades to pick up new updates and reboot every night.

We released 2.12.0 in mid-March; after upgrading each server generated a random number between 1 and 5 (inclusive) and saved it. This effectively grouped the servers in 5 buckets, so we could target ~20% of all servers at a time.

SecureDrop administrators had slightly over a month to manually do what we called the "semiautomated migration". It was the same code as the normal migration, except it was initiated by the administrator, at their convenience.

After that period ended, we started issuing point releases that started the migration in a fully automated manner. Even though we were relatively confident in the code after seeing a number of successful semiautomated migrations, we still did a staged rollout just as a safety measure.

It would be really bad if there was some unexpected edge case that caused a significant amount of SecureDrops to go down all at once! And we did end up catching a bug after the first round of fully automated upgrades, so the caution paid off.

We started out slowly and then quickly ramped up the cadence of releases:

  • April 22: 2.12.2 released, upgrading 20% of Application Servers.
  • April 28: 2.12.3 released, upgrading 40% of Application Servers.
  • April 30: 2.12.4 released, upgrading 60% of Application Servers.
  • May 5: 2.12.5 released, upgrading 100% of Application Servers.
  • May 6: 2.12.6 released, upgrading 20% of Monitor Servers (in addition to all Application Servers).
  • May 7: 2.12.7 released, upgrading 40% of Monitor Servers
  • May 8: 2.12.8 released, upgrading 100% of Monitor Servers

The official Focal end-of-life was scheduled for May 29, so we made with just a few weeks to spare. Phew!