How to Run ASSP As A Systemd Service With Systemctl

Running the Anti-Spam SMTP Proxy (ASSP) reliably on a modern Linux server requires a proper service manager. By default, ASSP needs to start as root to bind to privileged mail ports (like port 25), after which it forks and spawns its main worker processes under a non-privileged user like nobody.

Using a custom systemd service file allows Linux to seamlessly handle this handoff, manage automatic restarts on failure, and handle process tracking without dealing with legacy PID files.


The Complete systemd Service File

Create a new service unit configuration file using your preferred text editor:

sudo nano /etc/systemd/system/assp.service

Paste the following optimized configuration into the file:

[Unit]
Description=ASSP (Anti-Spam SMTP Proxy)
After=network.target

[Service]
Type=forking
WorkingDirectory=/usr/local/assp
ExecStart=/usr/bin/perl /usr/local/assp/assp.pl
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Why This Configuration Works

  • Type=forking: Crucial for ASSP. It tells systemd that the initial process (started as root) will spawn a child process and then exit. systemd will wait for this handoff to complete rather than assuming the application crashed.
  • Root Execution: By omitting a User= directive in the service file, systemd launches the Perl script as root. This gives ASSP the authority to bind to port 25 before it internally drops its own privileges down to the nobody process.
  • No PID File Required: Modern systemd uses Linux control groups (cgroups) to track process trees. It will automatically detect and monitor the spawned nobody worker process without needing a legacy PIDFile directive.
  • Automatic Restarts: The Restart=always and RestartSec=5 directives ensure that if ASSP experiences a memory leak or crash, the system will automatically spin it back up after 5 seconds.

Activating and Managing ASSP

Once your service file is saved, run the following sequence to notify the system manager, enable the boot hook, and launch the proxy:

# 1. Reload systemd to detect the new service file
sudo systemctl daemon-reload

# 2. Enable ASSP to start automatically on system boot
sudo systemctl enable assp

# 3. Start the ASSP proxy service immediately
sudo systemctl start assp

Verifying the Service Status

To ensure that systemd is accurately tracking the background process tree, check the service status:

sudo systemctl status assp

You should see an active (running) status, and the CGroup section at the bottom will display the active Perl processes running under the nobody user.

Monitoring Real-Time Logs

Because systemd manages the execution environment, all standard output from ASSP is sent straight to the system journal. You can follow live spam-filtering logs using journalctl:

sudo journalctl -u assp -f

Why Tailscale + Firewalld Is My Ultimate Remote Access Stack

Setting up secure remote access usually feels like a balancing act between security and sheer frustration. For a long time, my server setup relied on Fail2ban throwing up dynamic walls via traditional iptables. It worked, but it was messy, cluttered, and stopping the service felt like waiting for water to boil as thousands of rules were torn down one by one.

Recently, I made two major upgrades to my network architecture: migrating my firewall backend entirely to Firewalld (using optimized ipsets), and rolling out Tailscale for zero-config mesh networking.

Here is why this combination has completely transformed how I manage my servers and devices.


Clean Infrastructure: The Firewalld Advantage

If you are still managing raw iptables chains for services like Fail2ban, do yourself a favor and migrate to Firewalld. Moving my setup to Firewalld made overall server configuration infinitely easier.

Instead of dealing with an unreadable wall of text when checking active blocks, Firewalld handles everything through structured zones and clean, dynamic kernel ipsets. Fail2ban now behaves itself flawlessly in the background, keeping my main firewall rules clean and freeing up system resources.


Tailscale: Secure Networking That Just Clicks

Tailscale is built on top of WireGuard®, creating a secure, encrypted mesh network (a “Tailnet”) across your devices, no matter where they are in the world. You can download the client directly from the Tailscale Website.

There is no port forwarding required on your home router, and Firewalld makes it incredibly simple to handle your security permissions. To keep your network segmented, Firewalld allows you to isolate your virtual private network interface into its own strict, custom security perimeter.

Rather than dumping your mesh traffic into a generic, open “trusted” zone, you can create a custom zone that explicitly allows only the services you choose. Assuming your virtual mesh network interface is named vpn-mesh0, here are the three commands to lock it down:

bash

# 1. Create a dedicated firewall zone for your secure network mesh
sudo firewall-cmd --permanent --new-zone=tailmesh

# 2. Assign your virtual network interface directly to this new zone
sudo firewall-cmd --permanent --zone=tailmesh --add-interface=vpn-mesh0

# 3. Permit ONLY explicit services (e.g., SSH) through the mesh
sudo firewall-cmd --permanent --zone=tailmesh --add-service=ssh

# 4. Reload firewalld to activate the changes
sudo firewall-cmd --reload

Use code with caution.

By explicitly isolating the mesh traffic to its own zone, you enforce zero-trust security. Even if a client device on your network is compromised, the attacker cannot scan or access any unapproved ports on your server, keeping your environment perfectly locked down.


Smarter Connectivity: VPN On-Demand & Local WiFi Exceptions

A common issue with traditional VPNs is the “always-on” headache. You want your traffic encrypted when you are sitting in a sketchy coffee shop, but keeping the VPN tunnel active when you are at home or work can break local casting, slow down file transfers, or create routing loops.

Tailscale solves this beautifully by allowing you to configure VPN On-Demand with smart exceptions.

Using Tailscale’s client configurations (which you can dive deep into via the official Tailscale Documentation), you can set up smart triggers based on the Wi-Fi network you are currently connected to:

  • The Untrusted Network Trigger: The moment your phone or laptop connects to an open public Wi-Fi network, Tailscale automatically spins up your connection and routes your traffic through your home “Exit Node” for complete encryption.
  • The Trusted Local Exception: When you walk through your front door and connect to your home Wi-Fi, the client recognizes the SSID. It immediately disables the heavy routing or exit-node tunneling, granting you seamless, full-speed access to local network storage, smart home devices, and local media servers without unnecessary overhead.

The Verdict

By pairing Firewalld on the backend with Tailscale on the frontend, I’ve achieved the holy grail of homelabbing and system administration: total security without sacrificing convenience. The server stays tightly locked down and easy to manage, while my client devices adapt intelligently to whatever network environment I throw them into.

If you’re looking to simplify your remote access without cutting corners on your firewall, this is the blueprint to follow.


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.

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.

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.

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.

Global Emergency Resources LLC – 57th Presidential Inauguration.

Over the last couple of weeks I was very privileged to have some of the software that I have worked on being used for the 57th Presidential Inauguration. It is probably my biggest career moment ever – so far !

My employer Global Emergency Resources LLC landed a contract to supply the first aid locations and command centers with their product – HC Standard.

Ex·cerpt from Global Emergency Resource’s website:

WASHINGTON, D.C. – The 2013 Presidential Inauguration brought landmark changes in emergency management and spectator safety. For the first time, inaugural personnel used a powerful situational awareness software suite to track medical emergencies; reunite lost family members; and provide real time information to event organizers. Emergency personnel from The District of Columbia, Maryland, Virginia, and the United States military integrated emergency data using HC Standard® – a patient tracking and critical asset software solution developed by Global Emergency Resources, LLC based in Augusta, Georgia.

HC Standard® allowed local, state and federal agencies, including the National Parks Service, US Secret Service, the Red Cross, and Homeland Security officials to have a common operating picture of major events during the Inauguration, including the Presidential Candlelight Reception; the Inaugural Parade; activities along the National Mall; the Commander in Chief Ball; the Inaugural Ball; and the Inaugural Prayer Service.

The DC Department of Health partnered with the Maryland Institute for Emergency Medical Service Systems (MIEMSS), the Northern Virginia Emergency Response System (NVERS), and the Maryland Department of Human Resources (MD DHS) to provide patient care and tracking throughout the event. Each partner used its own installation of HC Standard® to enter patient data with Motorola MC65 handheld devices. The data was aggregated and shared in all systems so that EMTs, first responders, and command center leaders could see the full picture of Inaugural events as they occurred.

During the Inauguration, HC Standard® tracked every emergency or first aid case and plotted it in each of the three emergency operations centers used for the event tracking and management. Additionally, family members who were lost, and those who were looking for them, had their information uploaded to a multijurisdictional database so they could be more easily reunited. Even the 100+ horses that carried the mounted police were part of the HC Standard® operating picture.

“Interoperability was key,” says Stan Kuzia, CEO and founder of Global Emergency Resources. “The EMS and Healthcare partners in the National Capital Region (NCR) have worked diligently over the years to eliminate information silos and enhance communication. This Presidential Inauguration demonstrated their hard work is paying off”. The various civilian agencies in the NCR also worked closely with their military counterparts to share a combined picture of patients and missing persons being treated and handled during the entire event. HC Standard® helped to bridge the interoperability gaps on Inauguration Day as near real-time data was available to military responders just as fast as their civilian counterparts.

Original document can be found here: http://www.ger911.com/news-and-events/17-news/133-inauguration2013