Remote Access Integration Guide
What is remote access?
Identity over network position: keys, least-privilege tunnels, zero-trust mesh, audit.
The design principle is identity over location — prove who you are (Ed25519 keys, MFA, an IdP) and open the minimum path. SSH `-L`/`-R` forward arbitrary TCP through an encrypted channel; `ProxyJump` chains through a bastion so only one box has a public port. ngrok / `cloudflared` expose localhost over HTTPS — the exact Daraja-callback workflow, since Safaricom only POSTs to public HTTPS URLs. Tailscale/WireGuard replace open ports with an identity-gated mesh — ideal for administering a fleet of Raspberry Pis with SSH never touching the public internet.
Six remote-access primitives
Pick by the shape of the problem: shell, expose-localhost, private mesh, or desktop.
██████╗ ███████╗███╗ ███╗ ██████╗ ████████╗███████╗ █████╗ ██████╗ ██████╗███████╗███████╗███████╗
██╔══██╗██╔════╝████╗ ████║██╔═══██╗╚══██╔══╝██╔════╝ ██╔══██╗██╔════╝██╔════╝██╔════╝██╔════╝██╔════╝
██████╔╝█████╗ ██╔████╔██║██║ ██║ ██║ █████╗ ███████║██║ ██║ █████╗ ███████╗███████╗
██╔══██╗██╔══╝ ██║╚██╔╝██║██║ ██║ ██║ ██╔══╝ ██╔══██║██║ ██║ ██╔══╝ ╚════██║╚════██║
██║ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ██║ ███████╗ ██║ ██║╚██████╗╚██████╗███████╗███████║███████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚══════╝╚══════╝Remote Access Integration Guide
Focus: securely reach a machine or service that isn't directly exposed — a box behind a firewall, a
localhostdev server Daraja needs to call, a Raspberry Pi on the office LAN. The throughline is identity over network position: prove who you are (keys, MFA, an identity provider), open the minimum path, and audit it — never poke a hole in the firewall and hope.
Overview
"Remote access" is one verb — reach a thing that's elsewhere — answered by a handful of tools that trade off in predictable ways:
- SSH — the workhorse for administering a server you can route to. Key-based auth, an encrypted shell, and — the underused superpower — port forwarding: carry an arbitrary TCP stream through the SSH connection so a database on a private subnet looks like it's on your
localhost. - Public tunnels (ngrok, Cloudflare Tunnel) — the inverse problem: you have a service on
localhostand you need the public internet to reach it over HTTPS. This is exactly the M-Pesa/Daraja callback workflow — Daraja will only POST to a public HTTPS URL, and your laptop isn't one. - VPN & zero-trust (WireGuard, Tailscale, Cloudflare Access) — instead of exposing services one port at a time, put the machines on a private encrypted network and gate entry on identity. The firewall stays shut; access is a function of who you are, not where you are.
- RDP/VNC — when you need a graphical desktop, not a shell. Almost always tunnelled, never exposed raw.
- Remote development (VS Code Remote-SSH, GitHub Codespaces) — run your editor locally but execute on the remote box, so the code lives where the CPU, GPU, or private data is.
The decision tree: need a shell on a routable box? SSH. need the public internet to hit your localhost? a tunnel. need many services reachable by your team without opening ports? zero-trust mesh. need a desktop? RDP/VNC over a tunnel. Security is the constant: keys not passwords, least privilege, MFA, and an audit trail.
Official Documentation
| Source | URL | What it covers |
|---|---|---|
OpenSSH ssh manual | https://www.man7.org/linux/man-pages/man1/ssh.1.html | -L/-R forwarding, -J jump host, -i, -N |
OpenSSH ssh_config | https://www.man7.org/linux/man-pages/man5/ssh_config.5.html | ~/.ssh/config, ProxyJump, IdentityFile |
| ngrok getting started | https://ngrok.com/docs/getting-started/ | ngrok http, authtoken, auto-HTTPS |
| Cloudflare Tunnel | https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/ | cloudflared tunnel create/route/run, outbound-only model |
| Tailscale CLI | https://tailscale.com/kb/1080/cli | tailscale up/status/ssh, exit nodes |
| What is Tailscale | https://tailscale.com/kb/1151/what-is-tailscale | WireGuard mesh, zero-trust, MagicDNS, ACLs |
SSH: keys, config, and tunnels
Key-based auth (never passwords)
Generate a modern Ed25519 key, then copy the public half to the server. The private key never leaves your machine.
# Generate an Ed25519 keypair (smaller + faster than RSA, no reason to use RSA in 2026)
ssh-keygen -t ed25519 -C "you@codeamani.com" -f ~/.ssh/id_ed25519
# Install the PUBLIC key on the server's authorized_keys
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server.example.com
# Connect with an explicit identity
ssh -i ~/.ssh/id_ed25519 user@server.example.comOn the server, harden /etc/ssh/sshd_config: PasswordAuthentication no, PermitRootLogin no, PubkeyAuthentication yes. Passwords are brute-forceable; keys are not.
~/.ssh/config — stop typing flags
A config block turns a long command into ssh prod. It's also where bastion routing lives.
# A jump/bastion host — the only box with a public SSH port
Host bastion
HostName bastion.example.com
User jumpuser
IdentityFile ~/.ssh/id_ed25519
# A private box reachable ONLY through the bastion
Host app-internal
HostName 10.0.1.50 # private IP, no public exposure
User appuser
ProxyJump bastion # SSH hops through bastion automatically
IdentityFile ~/.ssh/id_ed25519Now ssh app-internal transparently tunnels through bastion. The equivalent one-shot flag is -J:
ssh -J jumpuser@bastion.example.com appuser@10.0.1.50Port forwarding (-L / -R)
This is SSH's most useful and least-known feature. -N means "no shell, just hold the tunnel open."
# LOCAL forward (-L): pull a remote/private service onto YOUR localhost.
# Reach a Postgres on a private subnet as if it were local:5432.
ssh -N -L 5432:db.internal:5432 user@bastion.example.com
# └─local─┘ └──remote target──┘
# → psql -h localhost -p 5432 now hits db.internal through the encrypted tunnel.
# REMOTE forward (-R): push YOUR localhost out to a port on the remote box.
# Expose your laptop's :3000 on the server's :8080 (e.g. a quick demo).
ssh -N -R 8080:localhost:3000 user@server.example.comLocal vs remote, the mnemonic:
-Lbrings something to you (Local),-Rsends something out to the remote.
Public tunnels: exposing localhost for HTTPS callbacks
Daraja, Stripe, Clerk and every other webhook provider POST to a public HTTPS URL. Your dev server on http://localhost:3000 is invisible to them. A tunnel gives localhost a temporary public HTTPS front door.
ngrok — fastest path
# One-time: register your account's authtoken
ngrok config add-authtoken <YOUR_TOKEN>
# Expose localhost:3000 over public HTTPS (TLS managed for you)
ngrok http 3000
# → Forwarding https://abc123.ngrok-free.app -> http://localhost:3000Take that https://…ngrok-free.app URL and register it as your Daraja callback URL. The inspector at http://127.0.0.1:4040 shows every request/response — invaluable for debugging callback payloads.
Cloudflare Tunnel (cloudflared) — durable, no open ports
cloudflared makes an outbound-only connection to Cloudflare's edge; your firewall stays fully closed to inbound traffic, and you get a stable hostname on your own domain (great when a provider whitelists callback domains).
cloudflared tunnel login # browser auth, picks a zone
cloudflared tunnel create daraja-dev # creates a named tunnel + UUID
cloudflared tunnel route dns daraja-dev cb.codeamani.com # map a hostname
# ~/.cloudflared/config.yml
# tunnel: <UUID>
# credentials-file: ~/.cloudflared/<UUID>.json
# ingress:
# - hostname: cb.codeamani.com
# service: http://localhost:3000
# - service: http_status:404
cloudflared tunnel run daraja-dev # bring it up| ngrok | Cloudflare Tunnel | |
|---|---|---|
| Setup | seconds | a few minutes |
| URL | random (paid: reserved) | your own domain hostname |
| Best for | throwaway webhook testing | longer-lived previews, IP-restricted providers |
| Inbound ports | none | none (outbound-only) |
VPN & zero-trust
Tunnels and bastions expose one path at a time. A zero-trust mesh flips the model: machines join a private encrypted network and access is granted by identity + policy, not by where a packet originates.
Tailscale (WireGuard mesh, managed)
Tailscale builds an encrypted peer-to-peer WireGuard mesh (a "tailnet"). No central gateway to bottleneck, no inbound ports, and devices authenticate against your existing IdP (Google, GitHub, etc.).
tailscale up # auth via browser/IdP, join the tailnet
tailscale status # list peers + their tailnet IPs
tailscale ip -4 # this device's 100.x.y.z address
tailscale up --ssh # enable identity-gated Tailscale SSH
tailscale ssh user@raspberry-pi # SSH over the mesh, keys handled for youAccess between devices is governed by ACLs (a policy file), and MagicDNS lets you use names (raspberry-pi) instead of IPs. This is the cleanest way to administer a fleet of Raspberry Pis without exposing SSH to the internet at all.
WireGuard (the raw protocol)
Tailscale is WireGuard with identity + NAT-traversal bolted on. Plain WireGuard (wg, wg-quick up wg0) is the DIY option — you manage keys and peer config yourself. Reach for it when you want a single self-hosted VPN concentrator and no third party in the path.
Cloudflare Access (zero-trust for HTTP)
Pairs with Cloudflare Tunnel: put an internal app behind a Tunnel, then enforce an identity policy (email domain, IdP group, MFA) at Cloudflare's edge before any request reaches your origin. No VPN client, no open port — the app is private but reachable by exactly the right people.
RDP / VNC (graphical desktops)
When you need a screen, not a shell:
- RDP (Windows) and VNC (cross-platform) are graphical remote-desktop protocols.
- Never expose RDP/VNC directly to the internet — RDP brute-forcing is a top ransomware vector. Tunnel them: either over SSH (
ssh -L 5900:localhost:5900 user@hostthen point your VNC client atlocalhost:5900) or, better, over Tailscale/WireGuard so the desktop is only reachable inside the private mesh.
Remote development
Run the editor locally, execute remotely — so code lives next to the data, GPU, or private network it needs.
- VS Code Remote-SSH — VS Code connects over your existing
~/.ssh/configentry (soProxyJump/bastion routing just works), installs a small server on the remote, and you edit/run/debug as if local. Zero extra infrastructure. - GitHub Codespaces — a fully managed cloud dev container; no machine of your own to administer. Good for onboarding (a new dev is coding in minutes) and for heavy builds you don't want on a laptop.
Security
Remote access is the front door to your infrastructure — treat every item here as mandatory, not optional.
- Keys, never passwords.
PasswordAuthentication noon every server; Ed25519 keys; protect private keys with a passphrase + an agent. - Least privilege. One bastion with a public port; everything else private and reached via
ProxyJump. Per-user accounts, not a sharedroot. - MFA / identity. Gate SSH and apps behind an IdP + MFA (Tailscale, Cloudflare Access) rather than a bare key on a stolen laptop.
- No raw exposure. RDP/VNC/databases never face the internet — tunnel or mesh them.
- Outbound-only where possible. Cloudflare Tunnel / Tailscale need zero inbound firewall rules — nothing to scan, nothing to brute-force.
- Rotate & revoke. Remove departed users from
authorized_keys/ the tailnet ACL immediately. Short-lived certs > long-lived keys for fleets. - Audit. Log SSH sessions and access decisions; Tailscale/Cloudflare give you a who-reached-what trail out of the box.
- Disable agent forwarding unless needed (
ForwardAgentopens your keys to a compromised hop); preferProxyJump. - Tunnel auth tokens are secrets. ngrok authtoken,
cloudflaredcredentials JSON, WireGuard private keys →.env.local/ secret managers, never committed.
codeAmani notes
- Daraja callback tunneling is the everyday use case. Daraja only POSTs to public HTTPS. Locally:
ngrok http 3000, register thehttps://…ngrok-free.appURL as your callback, and test against sandbox shortcode 174379 / test phone 254708374149 before going live. For a stable hostname a provider can whitelist, usecloudflaredmapped tocb.codeamani.com → http://localhost:3000. Either way the callback lands onapp/api/mpesa/callback/route.ts— and remember Daraja callbacks are effectively single-shot, so dedupe onCheckoutRequestIDand pair with a status-query reconciliation job (see the webhooks guide). - Raspberry Pi administration. A Pi on the office LAN should not have SSH exposed to the internet. Put it on the tailnet (
tailscale up --sshon the Pi) and reach it withtailscale ssh pi@raspberry-pifrom anywhere — identity-gated, no port-forwarding on the router, no dynamic-DNS hacks. Theraspberry-piguide covers the device side. - Zero-trust over open ports. For any internal tool (a staging dashboard, an admin panel), prefer Cloudflare Tunnel + Access or Tailscale over opening a port on a Vercel-adjacent box. The firewall stays shut; access is a function of identity, which is also what our Clerk-everywhere posture wants.
- Bastion pattern for managed DBs. When a Supabase/Neon-style resource sits behind a private network, a one-line
ssh -N -L 5432:db.internal:5432 user@bastiongives you localpsql/migration access through an encrypted tunnel — no need to widen the DB's IP allowlist. - Secrets hygiene.
cloudflaredcredential JSONs, ngrok authtokens, and WireGuard keys are credentials. They live in.env.local/ the secret manager and are caught by the pre-push gitleaks gate — never paste them in chat or commit them.