I Finally Dropped My Custom 1996 iptables Firewall Script—And Why It Was Time

Thirty years. That is how long my home network routing infrastructure relied on a monolithic shell script I wrote back in 1996. I crafted it back in the golden era of early Linux packet filtering, packed it with raw configuration lines, and tweaked it over decades as a hobby project to survive moving from ipfwadm to ipchains, and eventually to iptables.

It was a personal point of pride. Whenever I set up a new tool in my home lab, I didn’t click buttons; I opened a massive text file, added a manual tracking line, flushed the kernel tables, and reloaded the whole script.

But this week, on my Fedora router, the limitations finally caught up. It was time to pull the plug, learn the modern way of doing things, and migrate my entire home routing setup to firewalld and native nftables.

Here is the story of how I transitioned my home lab, the weird architectural traps I fell into, and how I built a modern, locked-down zone security posture.


The Catalyst: Tracking Down the Ghosts in the Logs

The transition started where all good home lab projects begin: digging through log streams. By default, firewalld drops packets silently. If you want to see what is actually happening behind the scenes, you have to tell it to start logging denied traffic.

I turned on global drop logging using this quick command:

bash

sudo firewall-cmd --set-log-denied=all

Use code with caution.

(Pro-tip: If your logs get too flooded later on after everything is working, you can easily turn this back off by running sudo firewall-cmd --set-log-denied=off).

With logging activated, my journalctl stream instantly exposed the ghosts haunting my network. My internal server was actively throwing network blocks:

text

filter_IN_internal_REJECT: IN=eth1 OUT= MAC=... SRC=10.0.10.55 DST=10.0.10.200 PROTO=TCP DPT=443 SYN

Use code with caution.

Because of these newly introduced blocks from the transition to firewalld, the computers inside my house suddenly couldn’t load local secure web pages, my email client setups were dropping synchronization steps, and my smart devices were choking on network discovery packets. While my legacy 1996 script had seamlessly allowed this internal traffic for years, firewalld‘s strict, out-of-the-box zone behaviors completely locked down the single machine from serving both functions until I configured it correctly.


Step 1: Restoring the Internet Gateway (Masquerading)

The first order of business was transforming my Fedora server into an efficient edge gateway router for the house. In the old days, this meant writing explicit POSTROUTING -o eth0 -j MASQUERADE loops.

With firewalld, we instead split the hardware interfaces into discrete logical security boundaries: external (the WAN modem link on eth0) and internal (the private LAN switch on eth1). To feed the household devices out to the web, we built a dedicated outbound routing policy:

bash

# Set up a clean outbound forwarding pipeline with NAT masquerading
sudo firewall-cmd --permanent --new-policy=internalToExternal
sudo firewall-cmd --permanent --policy=internalToExternal --add-ingress-zone=internal
sudo firewall-cmd --permanent --policy=internalToExternal --add-egress-zone=public
sudo firewall-cmd --permanent --policy=internalToExternal --set-target=ACCEPT
sudo firewall-cmd --permanent --policy=internalToExternal --add-masquerade
sudo firewall-cmd --reload

Use code with caution.

Suddenly, outbound data—including seamless features like Wi-Fi Calling on our phones—slid out to the internet natively without requiring a single manually defined high-number port mapping.


Step 2: Falling Into the “Policy Object” Trap

My first instinct as a hobbyist was to consolidate everything into elegant inter-zone Policy Objects. I attempted to route all inbound public web, mail, and application traffic through a custom externalToInternal forward policy.

The firewall immediately threw a massive architectural roadblock:
Error: INVALID_ZONE: 'forward-port' cannot be used because egress zone 'internal' has assigned interfaces

The lesson learned:firewalld Policy Objects completely forbid port-forwarding rules if the destination zone is attached to a real, physical network card. Policies are designed for virtual spaces (like isolated Docker networks or hypervisor VM bridges).

To achieve a pristine setup, the correct move was to put forwarding rules back onto the zone itself, but keep the raw underlying system ports completely closed off to the public router space.


Step 3: Embracing “Option B” (Strict Local Lockdown)

When configuring internal communication, I faced a fork in the road. Option A was a loose, blanket ACCEPT target for the whole internal network. Option B was a hardened, granular lockdown where only verified application services could move data.

I chose Option B.

Because firewalld drops negative priority policies when traffic is destined for the host itself, trying to manage local server loops through a policy wrapper caused endless rejections. The correct architectural solution was to map my application signatures directly onto our physical internal zone gate.

We stripped out manual numeric ports and used pre-configured service wrappers to expose exactly what we needed, keeping everything else locked tight against unauthorized lateral movement:

bash

# Lock down the internal gate to ONLY trusted local infrastructure services
sudo firewall-cmd --permanent --zone=internal --add-service=http
sudo firewall-cmd --permanent --zone=internal --add-service=https
sudo firewall-cmd --permanent --zone=internal --add-service=ssh
sudo firewall-cmd --permanent --zone=internal --add-service=dns
sudo firewall-cmd --permanent --zone=internal --add-service=smtp
sudo firewall-cmd --permanent --zone=internal --add-service=smtps
sudo firewall-cmd --permanent --zone=internal --add-service=imaps
sudo firewall-cmd --permanent --zone=internal --add-service=plex
sudo firewall-cmd --permanent --zone=internal --add-service=samba
sudo firewall-cmd --permanent --zone=internal --add-port=8123/tcp  # Home Assistant
sudo firewall-cmd --reload

Use code with caution.


The Final Verdict: A Pristine Architecture

After sweeping out legacy clutter (like default desktop samba-client wrappers and unneeded dhcpv6-client daemons on the inner switch interface), the firewall configuration achieved absolute stability.

Running a diagnostic audit prints out an incredibly clean, legible system baseline:

text

external (active)
  interfaces: eth0
  services: http https imaps smtp smtps ssh

internal (active)
  interfaces: eth1
  services: dns http https imaps plex samba smtp smtps ssh
  ports: 8123/tcp

Use code with caution.

Everything is fully persistent and gracefully survives cold reboots. Local devices can map storage drives over Samba, stream Plex, resolve local DNS queries, and view internal pages. Meanwhile, the public web interface remains blind to everything except our secure edge proxies—and public pings remain active to satisfy network path diagnostics and MTU discovery boundaries as recommended by universal best practices.

It took me thirty years to finally retire that old text script. But seeing a silent journalctl stream and a flawless, granular zone layout?

It was an awesome weekend project upgrade.

ASSP mod_inst.pl Error After Upgrading Fedora 42 to 43

I recently upgraded my system from Fedora 42 to Fedora 43 after Fedora 42 reached end-of-life.

When I started ASSP, I received errors indicating that the required Perl modules were incorrect or missing, so I decided to run mod_inst.pl again to reinstall the necessary modules.

I downloaded the latest mod_inst.pl from the ASSP website and ran it, but it immediately failed with the following error:

error calling required module LWP::Simple - Proxy must be specified as absolute URI; 'proxyuser' is not at /usr/local/share/perl5/5.42/LWP/Simple.pm line 30.

After spending quite a while searching for solutions, most references pointed towards issues with the proxy settings. However, my assp.cfg file had blank values for proxyserver, proxyuser, and proxypass, and this configuration had worked fine before the Fedora upgrade.

While debugging mod_inst.pl and mod_inst_ocr.pl, I noticed the following commented-out section around line 722:

###########################################
# please change the following to your needs
#
# uncomment and change the following line(s) if you use a HTTP-Proxy and ENV is not set
#$ENV{HTTP_proxy} = 'http://yourproxy:port';
#$ENV{HTTP_proxy_user} = 'proxy-user';
#$ENV{HTTP_proxy_pass} = 'proxy-user-password';
###########################################

To resolve the issue, I uncommented the HTTP_proxy line and set it to my proxy server:

$ENV{HTTP_proxy} = 'http://10.0.0.1:765';

After making this change, mod_inst.pl ran successfully and installed the required modules without any further errors.

This information may already exist somewhere in the documentation, but I couldn’t find it. Hopefully this helps anyone else who encounters the same issue after upgrading Fedora or Perl.


Unlocking Hermes: A Guide to Setting Up the API Gateway

The Hermes AI Agent is an incredibly powerful tool right in your terminal, capable of running commands, reading files, and interacting with your local machine. But what if you want to access its power from outside the
terminal? What if you could programmatically send tasks to your agent?

This is where a core, and sometimes overlooked, feature comes into play: the Hermes API Gateway.

The gateway transforms your personal AI assistant into a full-fledged reasoning engine that can be accessed over your network. It’s the key to unlocking scripted automations and building custom interactions. In this post,
I’ll provide a clear, step-by-step guide on how to enable and use it.
Why Do You Need a Gateway? The “Walled Garden” Problem

By default, your interaction with Hermes happens in one place: your terminal. This is great for direct, interactive use. However, this creates a “walled garden.” The agent can’t be easily controlled by a script, triggered
by an event, or integrated into a larger workflow.

The API Gateway breaks down these walls. By exposing a standard HTTP endpoint, it allows any application that can send a web request to start a conversation with your agent.
The Solution: A Doorway for Your AI

Think of the gateway as a secure, public-facing front door for your agent. Instead of having to be physically at the terminal to type a command, you can send a message to a URL. Hermes receives the message, does the work,
and sends a response right back.

This simple concept is incredibly powerful and opens the door to a new level of automation.
The Setup: A Step-by-Step Guide

Getting the gateway running is straightforward. It involves editing one configuration file and then knowing how to “knock” on the new front door.

Step 1: Configure and Enable the Gateway

First, you need to tell Hermes to start the gateway. This is done in the agent’s main configuration file, located at ~/.hermes/config.yaml.

Open this file in your favorite text editor. You will need to find (or add) the gateway section.

yaml
~/.hermes/config.yaml

gateway:
#This is the master switch. Set it to true to enable the gateway.
enabled: true
#The host IP the gateway will listen on.
#'0.0.0.0' makes it accessible from other machines on your network.
#'127.0.0.1' (localhost) restricts access to only the same machine.
host: 0.0.0.0
#The port it will run on. 5000 is a common default.
port: 5000
SECURITY:

#This is the most important setting. The gateway is
#unprotected by default. Set a strong, secret token here.
api_key: "your-super-secret-and-long-password-here"


Security is paramount. The api_key setting is not optional for any serious use. Without it, anyone on your network could access and control your agent. I recommend generating a long, random string to use as your key.

Step 2: Restart the Hermes Agent

The changes you made to config.yaml will only apply after you restart the Hermes agent. Stop your current session and start it again.

As Hermes boots up, you should see a new line in the logs confirming that the gateway is active and listening:

INFO | uvicorn.main | Started server on http://0.0.0.0:5000

This confirms your gateway is live.

Step 3: Send Your First API Request

With the gateway running, it’s time to test it. The easiest way is with a curl command from another terminal window. You will send a POST request to the /api/v1/chat endpoint.

Your request must contain two key things:
1. The Authorization header with your secret api_key.
2. A JSON payload with your message and some metadata.

Here is the command structure:

bash
curl -X POST http://127.0.0.1:5000/api/v1/chat \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-super-secret-and-long-password-here" \
-d '{
"platform": "api",
"chat_id": "api-test-session",
"user_id": "default-user",
"prompt": "Hello from the API! Please list the files in the current directory."
}'

Let’s break down the JSON data:
platform: Identifies the source of the message.
chat_id: This is a crucial field. It groups messages into conversations. Using the same chat_id across multiple requests allows Hermes to remember context, just like in a normal chat.
user_id: Identifies the user sending the message.
prompt: The actual task or question for the agent.

After running the command, you will get a JSON response from the agent containing its answer.
The Possibilities Are Now Open

You now have a fully functional API for your Hermes agent. This is the foundational building block for countless new applications. Whether you want to write a simple script to automate a repetitive task or build a more
complex system that leverages the agent’s reasoning abilities, you now have the key to do it. You’ve successfully turned your personal AI assistant into a programmable platform.

Why I fired Dropbox: How Nextcloud is saving me hundreds every year

For years, I was a loyal Dropbox subscriber. It was the “gold standard” for syncing files, and like most people, I just accepted the monthly fee as a necessary “tech tax.” But as my storage needs grew, so did the bill.

When I hit my storage limit and was prompted to upgrade to a more expensive professional tier, I finally asked myself: Why am I paying someone else to hold my data on their hard drives?

Enter Nextcloud. After switching to this self-hosted powerhouse, I’ve realized it’s not just about privacy—it’s a massive win for my wallet. Here’s how Nextcloud saved me money and why it might be time for you to make the switch too.

The “Subscription Trap” vs. The Gear You Already Own

The biggest drain on a modern budget is “subscription creep.” Dropbox starts at around $10–$12 a month. That doesn’t sound like much until you realize you’ll be paying it forever. Over five years, that’s $600 to $720 for a single service.

With Nextcloud, the software is free and open-source. My only costs were:

  1. Hardware: I didn’t spend a dime here—I just used the Linux box I’ve had for years. Instead of buying a new gadget, I repurposed an old machine that was already sitting in my house.
  2. Electricity: A negligible amount to keep my existing setup running.

The hardware was already paid for years ago. Now, every month I don’t pay Dropbox is pure profit.

Unlimited Storage (Without the Upsell)

In the world of paid cloud storage, “more space” always equals “more money.” If I wanted 10TB on a commercial platform, I’d be looking at a hefty enterprise-grade monthly fee.

With Nextcloud, if I run out of space, I just plug in a larger hard drive. I’m only limited by the physical hardware I choose to buy, not by a tiered pricing plan designed to squeeze my budget.

The Learning Curve Is the Only “Cost”

Is it “plug and play” like Dropbox? Not exactly. You need to spend an afternoon setting it up (I recommend using Docker or a pre-configured image). But once it’s running, the interface feels remarkably similar to the big-name providers. I have a mobile app that auto-uploads my photos, a desktop client that syncs my work files, and a web interface I can access from anywhere.

The Bottom Line

Switching to Nextcloud gave me two things: data sovereignty and financial relief. I no longer worry about price hikes, privacy policy changes, or storage limits. My data is in my house, on my hardware, and my monthly bill is exactly $0.00.

If you’re tired of the “subscription tax,” give Nextcloud a look. Your bank account will thank you.

Installed Radicale3 for hosting my own calendar and contacts data

So I finally got around to installing something so I can host my own calendar and contacts so that data is not shared with the big four corporate companies.

As I just need basic calendar and contacts functionality I chose the open source project Radicale. As I have run Fedora to control the house and my private data since 1998 I firstly installed the Radicale rpm via DNF.

dnf install radicale

This should install radicale3 as that is the latest version at the time of writing this blog post.

I will be running it off a sub domain of my public domain so I have set that up in the DNS with my main domain hosting company.

I have used a sub domain with a reverse proxy rather than opening another port [5232] on the firewall. I initially set it up with the port open but then rethought my approach as its more secure than to open the port on the firewall.

One other good thing using a sub domain is that you just need to have the proxy settings for that host rather than using location settings with the pathing.

Apache virtual host looks like this

<VirtualHost *:443>
 
 ServerName <subdomain.domain>
 DocumentRoot <pathtodomain>

 ProxyRequests Off
 ProxyPreserveHost On

 <Proxy *>
 Order deny,allow
 Allow from all
 </Proxy>

 # Forward requests to the backend Radicale server
 ProxyPass / http://localhost:5232/
 ProxyPassReverse / http://localhost:5232/

 # Inform Radicale about its location behind the proxy
 RequestHeader set X-Script-Name /radicale

 SSLCertificateFile <path to public key>
 SSLCertificateKeyFile <path to private key>

</VirtualHost>

Set up the config file for radicale to use it locally running on localhost on port 5232 and I am using my dovecot server to authenticate the users.

Then you should just be able to surf to the subdomain and be presented with the login page of the radicale server. Login and add your collections for the calendars and contacts.

Then configuring all the clients should be pretty straight forward for PC, Mac, iOS and Android.

Failed to unpack the windows 10 IoT Core installation package

Always fails. At first I thought there was an issue with the SD card. Then I read that you have to right-click and run as Administrator from your windows box. I could not get this to work – it never ran. In the end I had to enable the local Administrator on my windows box and login to that account and run the windows dashboard from that account. It then worked. Hope that helps someone.

MMEncode is not available on Linux distros any more.

Not sure what happened to this or why but its no longer available. I used to have a script that used it to convert binary data to text for attachments on emails. Having searched high and low I came up with a new solution – use openssl.

You can do the same that you were doing with mmencode by doing the following

openssl base64 -e < $FILE

Enjoy.

Visual Studio 2015, Windows 10 under VMWare Fusion 7 on a Mac

Trying to run up my windows project using Xamarin for windows phone 8. Kept getting this error when the windows phone 8.1 emulator

"Failed to start the virtual machine because one of the Hyper-V components is not running"

Researched a bit and had a few goes and quite a few different solutions but had to manually edit .vmx file which is inside the vmware folders.

Had to add

hypervisor.cpuid.v0 = "FALSE"

Make sure you do this with the VM shutdown. And bobs your uncle it now runs up the windows 8.1 emulators. Took a while to run them up the first time but it did finally come up. Can now debug my Xamarin project on windows phone 8.1.

Enjoy

Xamarin: Unable to install new debug version of Android app on a Motorola Ultra

On running from the Xamarin studio, I could no longer deploy to the companie’s Motorola Ultra. From the deploy to device window I was seeing the following error: [INSTALL_FAILED_UPDATE_INCOMPATIBLE]

This normally means that there is an old version hanging around on the device which you can’t overwrite – usually because the older version is signed and the new version is not because its running as debug from the Xamarin studio. On most Android devices they show the package name of the app in the installed apps and you can just go and remove it by the usual un-install method.

For some reason on my companies Motorola Ultra this was not showing. Took me a while but this is how I did uninstall it. You have to run up the Android debugger for which on my mac is located in …

/Users/<loggedinuser>/Library/Developer/Xamarin/android-sdk-macosx/platform-tools

Then you issue the command

./adb uninstall com.<package>.<name>

And Bob’s your uncle the package is uninstalled and you can then run the new one up and it gets deployed to the phone with no issue.

Hope that helps someone.

Moving From One Computer To Another With Existing Checkouts In TFS (Team Foundation Server)

My laptop was corrupted and decided to move to another laptop at work. I had existing checkouts so I copied all the code involved to the new laptop. Then I couldn’t add the workspace with the same name as it kept saying the workspace exists on my old machine.

I looked around at posts on the internet and they were suggesting to use the tfs commands

I tried to use

tf workspaces [/updateComputerName:oldComputerName][workspacename]

Now I don’t know what was wrong but it didn’t work at all. So I took the brute force approach and updated the TFS database itself using the SQL management studio.

I used

UPDATE [TFSDBName].[dbo].[tbl_Workspace]

SET Computer = ‘NewMachineName’ WHERE Computer = ‘OldMachineName’

and that worked a treat.   If your worried then do a select first and make sure you know how many it should be updating first and do a begin transaction before the statement – just to make sure your updating the right amount.  Or even do a select afterwards to see the data changed and then issue a commit command.