DEV Community

Cover image for How I Finally Understood SSH Before Building a Real Open-Source Feature?
divyanshu_Kumar
divyanshu_Kumar

Posted on

How I Finally Understood SSH Before Building a Real Open-Source Feature?

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Contents of the .ssh directory

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And then just type:

ssh db-server-1
Enter fullscreen mode Exit fullscreen mode

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

  1. Install Docker Desktop
  2. Install Ansible via Homebrew: brew install ansible
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 \
  .
Enter fullscreen mode Exit fullscreen mode

What just happened?

  • Docker built an Ubuntu image with an OpenSSH server installed
  • The deploy user 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_practice private 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
Enter fullscreen mode Exit fullscreen mode

Verify they're running:

docker ps
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

SSH connection test results

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
Enter fullscreen mode Exit fullscreen mode

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/config is 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 -G is 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/config with host aliases
  • ✅ Connected to both servers using just an alias name
  • ✅ Understood why permissions like 0600 and 0700 are 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)

Collapse
 
aarti_panchal_5118dc7a282 profile image
Aarti Panchal

Impressive work !!!