Hi, I'm a Java developer working on an open-source contribution to the Debezium Platform — a project under the Red Hat/JBoss umbrella. The feature I'm building is called Host-Based Pipeline Deployment. The idea is to allow the platform to deploy Debezium Server containers on bare-metal servers and cloud VMs — not just Kubernetes — using SSH and Ansible as the automation backbone.
I had never gone deep into how SSH actually works, what the config file does, or why file permissions like 0600 and 0700 exist. And I definitely had no separate server lying around to test against.
This post is the story of how I went from zero to fully understanding SSH, entirely on my MacBook M1, using Docker containers as fake servers. No cloud account needed. No separate machine needed. Just one MacBook and some curiosity.
This will be a great and very beginner-friendly ride, so Developers, Assemble! 🥷
What Is SSH, and Why Does It Matter for This Feature?
SSH stands for Secure Shell. It provides a cryptographically secured environment (the "Secure" part) to access a computer's command-line interface (the "Shell" part) over an untrusted network.
My feature is all about deploying pipeline containers to remote host servers from a central Conductor service. To do that, we need secure connections over the network so that operations on the remote server happen safely and reliably.
Here's how the feature works: A sysadmin creates a standard OpenSSH ~/.ssh/config file listing all the target hosts they want Debezium to deploy to. The Conductor service (a Quarkus Java app) watches that file for changes. When a new host appears, it triggers an Ansible playbook to provision that host — install Docker, deploy the Host Agent (a lightweight HTTP service), pre-pull the Debezium Server Docker image, and so on.
The critical design decision is: Java never opens SSH sessions directly. No JSch, no Apache MINA-SSHD. Instead, Java calls Ansible via ProcessBuilder, and Ansible handles all SSH connectivity by reading ~/.ssh/config natively through OpenSSH.
So if I don't understand SSH — the key pairs, the config file, the permissions — I can't understand why any of the Java code is written the way it is.
Step 1: Realizing macOS Already Had SSH (and It Was Fine)
ssh -V
# OpenSSH_9.9p1, LibreSSL 3.3.6
macOS ships with OpenSSH pre-installed. Nothing to install. I then checked if the .ssh directory already existed — it didn't, so I created one:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ls -la ~/.ssh/
Step 2: How SSH Actually Works — The Padlock Analogy
I always thought SSH just meant "type a password over an encrypted connection." I was wrong. SSH with key-based authentication works like this:
Think of a padlock. You have a padlock (your public key) and a key to open it (your private key). You give the padlock to every server you want to connect to — they lock a challenge with it and send it back. Only your key can open it. If you can open it, you prove you own the key — without ever sending the key to anyone.
YOU (your Mac):
-------------------------------
private key ← stays on your Mac
(~/.ssh/ddd41_practice)
SERVER (remote host):
-------------------------------
public key ← you put this here
(~/.ssh/authorized_keys on server)
What happens at connection time:
Server → encrypts a challenge using your public key → sends it
You → decrypt it with your private key → send proof back
Server → "Correct! You're in." — no password ever sent.
This is why losing your private key is such a big deal — anyone who has it can impersonate you to any server that has your public key.
Step 3: Generating a Project-Specific SSH Key
First, I made sure the .ssh directory existed with the correct permissions:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
Then I generated an ED25519 key. Why ED25519 and not RSA? It's modern, faster, and produces smaller keys — it's the recommended type in 2025+.
ssh-keygen -t ed25519 -C "ddd41-practice" -f ~/.ssh/ddd41_practice
When it asked for a passphrase, I pressed Enter twice to skip it. For local development testing, a passphrase gets in the way. In production, you'd always set one.
This created two files:
-
~/.ssh/ddd41_practice— the private key (no extension). NEVER share this. -
~/.ssh/ddd41_practice.pub— the public key. Safe to share.
Notice the permissions right away: 600 on the private key, 644 on the public key. I didn't set those manually — ssh-keygen set them automatically. That detail matters a lot, and I'll explain why next.
What Lives in ~/.ssh/ Now
| File | Purpose |
|---|---|
config |
The SSH config file — aliases and settings for each host |
known_hosts |
Fingerprints of servers you've connected to before |
ddd41_practice |
Your private key (NEVER share this) |
ddd41_practice.pub |
Your public key (safe to share) |
Step 4: The Permission Rabbit Hole — Why 0600, 0700, 0400 Are Non-Negotiable
This was the part that genuinely surprised me. SSH is paranoid about file permissions. If your private key is readable by other users on the system, SSH refuses to use it at all — it won't even try.
Here's the breakdown (r=4, w=2, x=1):
| Path | Required Permission | Meaning | What Happens If Wrong |
|---|---|---|---|
~/.ssh/ directory |
700 (rwx------) |
Only you can read, write, or enter | SSH ignores all config inside it |
~/.ssh/config |
600 (rw-------) |
Only you can read and write | SSH ignores the config file |
| Private key file |
600 or 400
|
Only you can read (and optionally write) | SSH refuses: "Permissions are too open"
|
| Public key file | 644 |
Others can read (it's meant to be shared) | Fine either way |
This is not a "best practice suggestion." SSH will literally refuse to use a key that is world-readable. This is a security guarantee baked into the protocol.
Step 5: The SSH Config File — The Most Important Part
Without a config file, connecting to a server looks like this every single time:
ssh -i ~/.ssh/ddd41_practice -p 2222 -l ubuntu 192.168.1.10
That is tedious and error-prone. The SSH config file at ~/.ssh/config lets you write all of that once:
Host db-server-1
HostName 192.168.1.10
User ubuntu
Port 2222
IdentityFile ~/.ssh/ddd41_practice
And then just type:
ssh db-server-1
SSH reads the config, finds the db-server-1 block, and automatically uses the right IP, port, user, and key. This is exactly why the DDD-41 design uses ~/.ssh/config as the source of truth for host discovery — the sysadmin already knows how to write SSH configs. No new UI, no new API, no new format to learn.
Step 6: Building Fake SSH Servers on My Mac (Docker)
I don't have a separate Linux server. But I have Docker. The solution: build a custom Ubuntu Docker image with an SSH server installed and my public key pre-authorized as a build argument.
Prerequisites
- Install Docker Desktop
- Install Ansible via Homebrew:
brew install ansible - Install the Docker Ansible collection:
ansible-galaxy collection install community.docker
6.1 — Create a Project Directory
mkdir -p ~/ddd41-lab/docker
cd ~/ddd41-lab/docker
6.2 — Create the Dockerfile
cat > ~/ddd41-lab/docker/Dockerfile << 'EOF'
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y \
openssh-server \
sudo \
python3 \
python3-pip \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash deploy && \
echo "deploy ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN mkdir -p /home/deploy/.ssh && \
chmod 700 /home/deploy/.ssh && \
chown deploy:deploy /home/deploy/.ssh
ARG SSH_PUB_KEY
RUN echo "${SSH_PUB_KEY}" > /home/deploy/.ssh/authorized_keys && \
chmod 600 /home/deploy/.ssh/authorized_keys && \
chown deploy:deploy /home/deploy/.ssh/authorized_keys
RUN mkdir /run/sshd && \
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config && \
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config && \
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config && \
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin no/' /etc/ssh/sshd_config
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
EOF
6.3 — Build the Image, Injecting Your Public Key
cd ~/ddd41-lab/docker
docker build \
--build-arg SSH_PUB_KEY="$(cat ~/.ssh/ddd41_practice.pub)" \
-t ddd41-fake-server:latest \
.
What just happened?
- Docker built an Ubuntu image with an OpenSSH server installed
- The
deployuser was created with passwordless sudo - Your public key was embedded into the image's
authorized_keys - Now, any container from this image will accept connections with your
ddd41_practiceprivate key
6.4 — Start the Fake Servers
We'll start two containers to simulate db-server-1 and db-server-2:
docker run -d \
--name db-server-1 \
-p 2201:22 \
ddd41-fake-server:latest
docker run -d \
--name db-server-2 \
-p 2202:22 \
ddd41-fake-server:latest
Verify they're running:
docker ps
6.5 — Configure the SSH Config File
cat >> ~/.ssh/config << 'EOF'
Host db-server-1
HostName 127.0.0.1
User deploy
Port 2201
IdentityFile ~/.ssh/ddd41_practice
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Host db-server-2
HostName 127.0.0.1
User deploy
Port 2202
IdentityFile ~/.ssh/ddd41_practice
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
EOF
chmod 600 ~/.ssh/config
6.6 — Test SSH Connections
# Test connecting to db-server-1
ssh db-server-1 "hostname"
# Test connecting to db-server-2
ssh db-server-2 "hostname"
Yaiiii, it worked! 🎉 No IP. No port flag. No -i flag. Just the alias. SSH read the config, found the db-server-1 block, used 127.0.0.1:2201, authenticated with ~/.ssh/ddd41_practice, and connected as deploy.
6.7 — The ssh -G Command: Verify What SSH Actually Resolved
This command is incredibly useful for debugging. It shows you exactly what SSH resolved for a given host alias — without actually connecting:
ssh -G db-server-1
It prints every resolved setting: the hostname, port, user, identity file, and more. If something isn't working, always run ssh -G before blaming anything else.
What I Took Away From All This
After going through every step above, I finally understood things I had taken for granted for years:
Why key-based auth is better than passwords — the private key never leaves your machine. There is nothing to intercept in transit.
Why file permissions are enforced by SSH itself — it's not optional or a best-practice suggestion. SSH will literally refuse to use a key that is world-readable. This is a security guarantee baked into the protocol.
Why
~/.ssh/configis so powerful — and why my project uses it as the source of truth for host discovery. The sysadmin already knows how to write SSH configs. The platform reads it directly — no new UI, no new API, no new format to learn.Why
ssh -Gis the most useful debugging command — always run it before blaming anything else.
Let me know in the comments: what was your Best moment with SSH? 💬
Let's recap what we accomplished:
- ✅ Generated an SSH key pair (ED25519)
- ✅ Built fake SSH servers with Docker (public key pre-authorized)
- ✅ Configured
~/.ssh/configwith host aliases - ✅ Connected to both servers using just an alias name
- ✅ Understood why permissions like
0600and0700are non-negotiable
I'm building this feature as part of GSoC 2026. If you're interested in the full design, check out the DDD-41 design document. Feedback and questions are always welcome!



Top comments (1)
Impressive work !!!