If you’ve spent any time in Haskell or FP circles, you’ll have run into the terms Functor, Applicative, and
Monad. They can sound mysterious, but at their core they’re just design patterns for sequencing computations.
Python isn’t a purely functional language, but we can still capture these ideas in code. In this post, we’ll build a
full Maybe type in Python: a safe container that represents either a value (Some) or no value (Nothing). We’ll
compare it with the Haskell version along the way.
A full runnable demo of the code presented here is available as a gist up on GitHub.
Maybe
We start of with our box or context. In our case today, we might have a value in the box (Some) or the box
maybe empty (Nothing). Because both of these are derivatives of the same thing, we create a base class of Maybe.
Our Maybe class here defines all of the operations we want to be able to perform on this datatype, but does not
implement any of them; leaving the implementation to be filled in my the derivatives. You can expect the implementations
between these derived classes to be quite different to each other.
We should end up with something like this:
classDiagram
class Maybe {
+map(f)
+ap(mb)
+bind(f)
}
class Some {
+value: T
}
class Nothing
Maybe <|-- Some
Maybe <|-- Nothing
Functor: Mapping over values
A Functor is anything you can map a function over. In Haskell, the generic Functor version of this is called
fmap:
fmap(+1)(Just10)-- Just 11fmap(+1)Nothing-- Nothing
The flow of values through map (or fmap) looks like this:
A Monad takes things further: it lets us chain together computations that themselves return a Maybe.
In Haskell, this is the >>= operator (bind):
halfIfEven::Int->MaybeInthalfIfEvenx=ifevenxthenJust(x`div`2)elseNothingJust10>>=halfIfEven-- Just 5Just3>>=halfIfEven-- Nothing
Here we’re chaining a computation that itself returns a Maybe. If the starting point is Nothing, or if the
function returns Nothing, the whole chain collapses.
flowchart LR
S[Some x] --bind f--> FOUT[Some y]
S --bind g--> GOUT[Nothing]
N[Nothing] --bind f--> NRES[Nothing]
Notice how the “empty box” propagates: if at any point we hit Nothing, the rest of the chain is skipped.
You’ll also see a common pattern emerging with all of the implementations for Nothing. There’s no computation. It’s
simply just returning itself. As soon as you hit Nothing, you’re short-circuited to nothing.
Do Notation (Syntactic Sugar)
Haskell makes monadic code look imperative with do notation:
doa<-Just4b<-halfIfEvenareturn(a+b)
In Python, we can approximate this style using a generator-based decorator. Each yield unwraps a Maybe, and the
whole computation short-circuits if we ever see Nothing.
This isn’t strictly necessary, but it makes larger chains of monadic code read like straight-line Python.
Wrapping Up
By porting Maybe into Python and implementing map, ap, and bind, we’ve seen how Functors, Applicatives, and
Monads aren’t magic at all — just structured patterns for working with values in context.
Functor: apply a function inside the box.
Applicative: apply a function that’s also in a box.
Monad: chain computations that each return a box.
Haskell bakes these ideas into the language; in Python, we can experiment with them explicitly. The result is safer,
more composable code — and maybe even a little functional fun.
Kerberos is one of those protocols that sounds mysterious until you see it in action. The moment you type kinit, run
klist, and watch a ticket pop up, it clicks: this is Single Sign-On in its rawest form. In this post we’ll set up a
tiny realm on a Debian test box (koffing.local), get a ticket-granting ticket (TGT), and then use it for SSH without
typing a password.
What is Kerberos?
Born at MIT’s Project Athena in the 1980s, Kerberos solved campus-wide single sign-on over untrusted networks. It
matured through v4 to Kerberos 5 (the standard you use today). It underpins enterprise SSO in Windows domains
(Active Directory) and many UNIX shops.
Kerberos authenticates clients to services without sending reusable secrets. You authenticate once to the KDC, get
a TGT (Ticket Granting Ticket), then use it to obtain per-service tickets from the TGS
(Ticket Granting Service).
Services trust the KDC, not your password.
Core terms
Realm: Admin boundary (e.g., LOCAL).
Principal: Identity in the realm, like michael@LOCAL (user) or host/koffing.local@LOCAL (service).
KDC: The authentication authority. Runs on koffing.local as krb5kdc and kadmind.
TGT: Your “hall pass.” Lets you ask the KDC for service tickets.
Service ticket: What you present to a service (e.g., SSHD on koffing.local) to prove identity.
Keytab: File holding long-term service keys (like for sshd). Lets the service authenticate without storing a password.
Here’s a visual representation of how the Kerberos flow operates:
sequenceDiagram
participant U as User
participant AS as KDC/AS
participant TGS as KDC/TGS
participant S as Service (e.g., SSHD)
U->>AS: AS-REQ (I am michael)
AS-->>U: AS-REP (TGT + session key)
U->>TGS: TGS-REQ (I want ticket for host/koffing.local)
TGS-->>U: TGS-REP (service ticket)
U->>S: AP-REQ (here's my service ticket)
S-->>U: AP-REP (optional) + access granted
Ok, with all of that out of the way we can get to setting up.
Setup
There’s a few packages to install and a little bit of configuration. All of these instructions are written for a
Debian/Ubuntu flavour of Linux. I’m sure that the instructions aren’t too far off for other distributions.
Install the packages
We install the Key Distribution Servicekrb5-kdc, Administration Serverkrb5-admin-server, and some Client
Utilitieskrb5-user.
The fully qualified name of my virtual machine that I’m testing all of this out on is called koffing.local. These
values would change to suit your environment.
Edit /etc/krb5.conf and make sure it looks like this:
[libdefaults]
default_realm = LOCAL
rdns = false
dns_lookup_kdc = false
forwardable = true
[realms]
LOCAL = {
kdc = koffing.local
admin_server = koffing.local
}
[domain_realm]
.local = LOCAL
koffing.local = LOCAL
Make sure your host resolves correctly:
hostname-f# should print: koffing.local (for me)
getent hosts koffing.local
# If needed, add to /etc/hosts:# 127.0.1.1 koffing.local koffing
Create the KDC database
Now we initialize the database that will hold all of your principals, policies, realms, etc.
sudo mkdir -p /var/lib/krb5kdc
sudo kdb5_util create -s -r LOCAL
# set the KDC master password when prompted
This will show you which hostnames it resolves, which tickets it requests, and where it fails.
List all principals in the KDC database:
sudo kadmin.local -q"listprincs"
Clear your credential cache if tickets get stale:
kdestroy
The two most common pitfalls are:
Hostname mismatch
Realm mismatch (default realm not set in /etc/krb5.conf).
SSO
So, we’ve got the proof of concept going, but it would be good to see this in action. What we’ll cover in this next
section is getting the sshd service to trust our Kerberos tickets. This will allow for passwordless SSH for the
user.
Add the host service principal and keytab
In order to get KDC to vouch for services, those services need principal definitions. A principal is any Kerberos
identity. Users get user principals (as we saw above), services also need principals.
A keytab is a file that stores one or more Kerberos keys (like passwords, but in cryptographic form). Unlike users
(who can type passwords into kinit), services can’t type passwords interactively. So the KDC generates a random key
for host/koffing.local@LOCAL (-randkey) and you export it into /etc/krb5.keytab with ktadd.
Now sshd can silently use that keytab to decrypt tickets clients send it.
Enable GSSAPI in sshd
The global /etc/ssh/sshd_config needs a couple of flags flicked. The SSH daemon doesn’t implement Kerberos directly,
so it uses the GSSAPI library functions provided by MIT Kerberos (or Heimdal) to handle ticket validation. GSSAPI
isn’t a protocol itself; it’s an API or abstraction layer.
Once we’ve flipped these switches we are telling sshd“Accept authentication from any GSSAPI mechanism. In practice, this means Kerberos tickets.”.
This setup is obviously done on any server that you want to do this SSO style login with. It’s a bit confusing in my
example here, because everything is on the one machine.
Configure your SSH client
Conversely, we have configuration to do on the client side. For clients that want to connect with this type of
authentication, the following settings are required in their ~/.ssh/config:
If everything lines up, ssh should not prompt for a password. Your Kerberos TGT has been used to authenticate silently.
Where Kerberos Fits
Kerberos is ideal for LAN-based authentication: it provides fast, passwordless single sign-on for services like SSH,
Postgres, and intranet HTTP apps. But it isn’t designed for cross-organization web or mobile use.
Modern protocols like OIDC (OpenID Connect) build on OAuth 2.0 to provide authentication and federation across the
public internet. They use signed tokens, redirect flows, and JSON-based metadata — making them better suited for SaaS,
cloud apps, and mobile clients.
In short: Kerberos is the right tool inside the castle walls; OIDC is the right tool when your users are everywhere.
Wrap-up
We’ve stood up a Kerberos realm (LOCAL), issued a TGT for a user (michael), and used it for passwordless SSH into
the same box. That’s enough to demystify Kerberos: no secrets flying across the network, just short-lived tickets
granted by a trusted KDC.
There’s plenty more that we can accomplish here as we could create service principals for HTTP, Postgres, or
cross-realm trust.
FreeBSD Jails are one of the earliest implementations of operating
system-level virtualization—dating back to the early 2000s, long before Docker popularized the idea of lightweight
containers. Despite their age, jails remain a powerful, flexible, and minimal way to isolate services and processes on
FreeBSD systems.
This post walks through a minimal “Hello World” setup using Jails, with just enough commentary to orient new users and
show where jails shine in the modern world of virtualization.
Why Jails?
A FreeBSD jail is a chroot-like environment with its own file system, users,
network interfaces, and process table. But unlike chroot, jails extend control to include process isolation, network
access, and fine-grained permission control. They’re more secure, more flexible, and more deeply integrated into the
FreeBSD base system.
Here’s how jails compare with some familiar alternatives:
Versus VMs: Jails don’t emulate hardware or run separate kernels. They’re faster to start, lighter on resources, and simpler to manage. But they’re limited to the same FreeBSD kernel as the host.
Versus Docker: Docker containers typically run on a Linux host and rely on a container runtime, layered filesystems, and extensive tooling. Jails are simpler, arguably more robust, and don’t require external daemons. However, they lack some of the ecosystem and portability benefits that Docker brings.
If you’re already running FreeBSD and want to isolate services or test systems with minimal overhead, jails are a
perfect fit.
Setup
Let’s build a bare-bones jail. The goal here is simplicity: get a jail running with minimal commands. This is the BSD
jail equivalent of “Hello, World.”
# Make a directory to hold the jailmkdir hw
# Install a minimal FreeBSD userland into that directorysudo bsdinstall jail /home/michael/src/jails/hw
# Start the jail with a name, IP address, and a shellsudo jail -cname=hw host.hostname=hw.example.org \
ip4.addr=192.168.1.190 \path=/home/michael/src/jails/hw \command=/bin/sh
You now have a running jail named hw, with a hostname and IP, running a shell isolated from the host system.
192.168.1.190 is just a static address picked arbitrarily by me. For you, you’ll want to pick an address that is
reachable on your local network.
Poking Around
With your jail up and running, that means you can start working with it. To enter the jail, you can use the following:
sudo jexec hw /bin/sh
jexec allows you to send any command that you need to into the jail to execute.
sudo jexec hw ls /
Querying
You can list running jails with:
jls
You should see something like this:
JID IP Address Hostname Path
2 192.168.1.190 hw.example.org /home/michael/src/jails/hw
You can also look at what’s currently running in the jail:
ps -J hw
You should see the /bin/sh process:
PID TT STAT TIME COMMAND
2390 5 I+J 0:00.01 /bin/sh
Finishing up
To terminate the jail:
sudo jail -r hw
This is a minimal setup with no automated networking, no jail management frameworks, and no persistent configuration.
And that’s exactly the point: you can get a working jail in three commands and tear it down just as easily.
When to Use Jails
Jails make sense when:
You want process and network isolation on FreeBSD without the overhead of full VMs.
You want to run multiple versions of a service (e.g., Postgres 13 and 15) on the same host.
You want stronger guarantees than chroot provides for service containment.
You’re building or testing FreeBSD-based systems and want a reproducible sandbox.
For more complex jail setups, FreeBSD offers tools like ezjail, iocage, and bastille that add automation and
persistence. But it’s worth knowing how the pieces fit together at the core.
Conclusion
FreeBSD jails offer a uniquely minimal, powerful, and mature alternative to both VMs and containers. With just a few
commands, you can create a secure, isolated environment for experimentation, testing, or even production workloads.
This post only scratched the surface, but hopefully it’s enough to get you curious. If you’re already on FreeBSD, jails
are just sitting there, waiting to be used—no extra software required.
Modern Linux systems provide a fascinating feature for overriding shared library behavior at runtime: LD_PRELOAD.
This environment variable lets you inject a custom shared library before anything else is loaded — meaning you can
intercept and modify calls to common functions like open, read, connect, and more.
In this post, we’ll walk through hooking the open() function using LD_PRELOAD and a simple shared object. No extra
tooling required — just a few lines of C, and the ability to compile a .so file.
Intercepting open()
Let’s write a tiny library that intercepts calls to open() and prints the file path being accessed. We’ll also
forward the call to the real open() so the program behaves normally.
Create a file named hook_open.c with the following:
#define _GNU_SOURCE
#include<stdio.h>
#include<stdarg.h>
#include<dlfcn.h>
#include<fcntl.h>intopen(constchar*pathname,intflags,...){staticint(*real_open)(constchar*,int,...)=NULL;if(!real_open)real_open=dlsym(RTLD_NEXT,"open");va_listargs;va_start(args,flags);mode_tmode=va_arg(args,int);va_end(args);fprintf(stderr,"[HOOK] open() called with path: %s\n",pathname);returnreal_open(pathname,flags,mode);}
This function matches the signature of open, grabs the “real” function using dlsym(RTLD_NEXT, ...), and then
forwards the call after logging it.
Note We use va_list to handle the optional mode argument safely.
Compiling the Hook
Compile your code into a shared object:
gcc -fPIC-shared-o hook_open.so hook_open.c -ldl
Now you can use this library with any dynamically linked program that calls open.
Testing with a Simple Program
Try running a standard tool like cat to confirm that it’s using open():
LD_PRELOAD=./hook_open.so cat hook_open.c
You should see:
[HOOK] open() called with path: hook_open.c
#define _GNU_SOURCE
...
Each time the program calls open(), your hook intercepts it, logs the call, and passes control along.
Notes and Gotchas
This only works with dynamically linked binaries — statically linked programs don’t go through the dynamic linker.
Some programs (like ls) may use openat() instead of open(). You can hook that too, using the same method.
If your hook causes a crash or hangs, it’s often due to incorrect use of va_arg or missing dlsym resolution.
Where to Go From Here
You can expand this basic example to:
Block access to specific files
Redirect file paths
Inject fake contents
Hook other syscalls like connect(), write(), execve()
LD_PRELOAD is a powerful mechanism for debugging, sandboxing, and learning how programs interact with the system.
Just don’t forget — you’re rewriting the behavior of fundamental APIs at runtime.
Hexagonal Architecture, also known as Ports and
Adapters, is a compelling design pattern that encourages the decoupling of domain logic from infrastructure concerns.
In this post, I’ll walk through a Rust project called banker that adopts this architecture, showing how it helps
keep domain logic clean, composable, and well-tested.
You can follow along with the full code up in my GitHub Repository to get this
running locally.
Project Structure
The banker project is organized as a set of crates:
crates/
├── banker-core # The domain and business logic
├── banker-adapters # Infrastructure adapters (e.g. in-memory repo)
├── banker-fixtures # Helpers and test data
└── banker-http # Web interface via Axum
Each crate plays a role in isolating logic boundaries:
banker-core defines the domain entities, business rules, and traits (ports).
banker-adapters implements the ports with concrete infrastructure (like an in-memory repository).
banker-fixtures provides test helpers and mock repositories.
banker-http exposes an HTTP API with axum, calling into the domain via ports.
Structurally, the project flows as follows:
graph TD
subgraph Core
BankService
AccountRepo[AccountRepo trait]
end
subgraph Adapters
HTTP[HTTP Handler]
InMemory[InMemoryAccountRepo]
Fixtures[Fixture Test Repo]
end
HTTP -->|calls| BankService
BankService -->|trait| AccountRepo
InMemory -->|implements| AccountRepo
Fixtures -->|implements| AccountRepo
Defining the Domain (banker-core)
In Hexagonal Architecture, the domain represents the core of your application—the rules, behaviors, and models that
define what your system actually does. It’s intentionally isolated from infrastructure concerns like databases or HTTP.
This separation ensures the business logic remains testable, reusable, and resilient to changes in external technology
choices.
The banker-core crate contains the central business model:
pubstructBank<R:AccountRepo>{repo:R,}impl<R:AccountRepo>Bank<R>{pubfndeposit(&self,cmd:Deposit)->Result<Account,BankError>{letmutacct=self.repo.get(&cmd.id)?.ok_or(BankError::NotFound)?;acct.balance_cents+=cmd.amount_cents;self.repo.upsert(&acct)?;Ok(acct)}// ... open and withdraw omitted for brevity}
The Bank struct acts as the use-case layer, coordinating logic between domain entities and ports.
Implementing Adapters
In Hexagonal Architecture, adapters are the glue between your domain and the outside world. They translate external
inputs (like HTTP requests or database queries) into something your domain understands—and vice versa. Adapters
implement the domain’s ports (traits), allowing your application core to remain oblivious to how and where the data
comes from.
The in-memory repository implements the AccountRepo trait and lives in banker-adapters:
The outermost layer of a hexagonal architecture typically handles transport—the mechanism through which external actors
interact with the system. In our case, that’s HTTP, implemented using the axum framework. This layer invokes domain
services via the ports defined in banker-core, ensuring the business logic remains insulated from the specifics of web
handling.
In banker-http, we wire up the application for HTTP access using axum:
Each handler invokes domain logic through the Bank service, returning simple JSON responses.
This is one example of a primary adapter—other adapters (e.g., CLI, gRPC) could be swapped in without changing the core.
Takeaways
Traits in Rust are a perfect match for defining ports.
Structs implementing those traits become adapters—testable and swappable.
The core domain crate (banker-core) has no dependencies on infrastructure or axum.
Tests can exercise the domain logic via fixtures and in-memory mocks.
Hexagonal Architecture in Rust isn’t just theoretical—it’s ergonomic. With traits, lifetimes, and ownership semantics,
you can cleanly separate concerns while still writing expressive, high-performance code.