Skip to main content
Updated Feb 16, 2026

Networking Fundamentals & SSH Remote Access

In Lesson 8, you locked down your server with dedicated users, restrictive permissions, and SSH keys. Your agent is secure -- but secure from everyone, including you, if you cannot reach it across a network. Security without connectivity is a locked room with no door.

Here is a scenario that every agent deployer encounters: You deploy an AI agent on a cloud server. It starts successfully. You check the logs -- everything looks clean. Then you open your browser to test the health endpoint and get "connection refused." You try from the command line -- same result. The agent is running. The server is on. But nothing can reach it. What went wrong?

The answer is almost always one of three things: the agent is bound to the wrong address, a firewall is blocking the port, or you are connecting to the wrong port entirely. This lesson gives you the diagnostic toolkit to identify and fix each of these problems, establish secure remote connections with SSH, and protect your agent ports with a firewall.


Ports and Services: What Port 8000 Means

A port is a numbered endpoint on a machine that identifies a specific service. When your agent listens on port 8000, it is saying: "Send requests to this machine on door number 8000, and I will answer." Ports range from 0 to 65535, and some numbers are reserved by convention.

Well-Known Ports

The Internet Assigned Numbers Authority (IANA) maintains a registry of port assignments. The most important ones for agent deployment:

PortServiceWhy You Care
22SSHRemote access to your server
80HTTPUnencrypted web traffic
443HTTPSEncrypted web traffic
5432PostgreSQLDatabase your agent may use
8000ConventionCommon for Python/FastAPI agents
8080ConventionAlternative HTTP port

Ports 0-1023 are "well-known" and require root privileges to bind. Ports 1024-49151 are "registered" and available to any user. This is why your agent uses port 8000 instead of port 80 -- binding to 8000 does not require root access, which aligns with the least-privilege principle from Lesson 8.

Checking What is Listening

Use ss (socket statistics) to see which ports have active listeners:

ss -tlnp

Output:

State    Recv-Q   Send-Q     Local Address:Port     Peer Address:Port  Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=892,fd=3))
LISTEN 0 5 127.0.0.1:8000 0.0.0.0:* users:(("python3",pid=1234,fd=5))

Each line shows a listening service. The flags mean:

FlagPurpose
-tTCP connections only
-lListening sockets only (not established connections)
-nShow port numbers instead of service names
-pShow the process using each port

The output above reveals two services: SSH listening on port 22 (accessible from anywhere, 0.0.0.0) and a Python agent on port 8000 (accessible only from localhost, 127.0.0.1). That distinction is the next concept.


Localhost vs 0.0.0.0: The Binding Address

The binding address determines who can reach your service. This single configuration choice is the most common reason agents are unreachable from other machines.

127.0.0.1 (localhost): Loopback Only

When a service binds to 127.0.0.1 (also called localhost), it only accepts connections from the same machine:

# This agent is ONLY reachable from the server itself
python3 -m http.server 8000 --bind 127.0.0.1 &
curl http://127.0.0.1:8000/

Output:

Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...
<!DOCTYPE HTML>
<html lang="en">
<head>...

The request succeeds because you are on the same machine. But from your laptop or any other machine, this agent is invisible.

0.0.0.0: All Network Interfaces

When a service binds to 0.0.0.0, it accepts connections from every network interface -- localhost, LAN, and the internet:

# Stop the previous server
kill %1 2>/dev/null

# This agent is reachable from anywhere
python3 -m http.server 8000 --bind 0.0.0.0 &
curl http://127.0.0.1:8000/

Output:

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
<!DOCTYPE HTML>
<html lang="en">
<head>...

Now the same agent is reachable from other machines on the network (assuming no firewall blocks it).

When to Use Each

BindingUse WhenSecurity
127.0.0.1Agent should only be accessed locally (development, internal-only services)Safest -- nothing external can reach it
0.0.0.0Agent must be accessible from other machines (production, remote clients)Requires firewall protection
Agent Framework Defaults

Many web frameworks default to 127.0.0.1 for safety. When deploying to production, you must explicitly set the binding address. For FastAPI with uvicorn: uvicorn main:app --host 0.0.0.0 --port 8000. For Flask: flask run --host 0.0.0.0.

WSL2 Users

WSL2 networking is more complex than native Linux. WSL2 runs in a virtual machine with its own network interface. To access a service running inside WSL2 from Windows, you typically use localhost -- WSL2 handles the forwarding automatically on recent Windows versions. If that does not work, check your WSL2 IP with ip addr show eth0 and use that address instead.

Cleaning Up the Test Server

kill %1 2>/dev/null

Output:

[1]+  Terminated              python3 -m http.server 8000 --bind 0.0.0.0

Testing Endpoints with curl

curl is the command-line tool for making HTTP requests. Think of it as a browser that runs in the terminal and shows you exactly what the server returns -- no rendering, no JavaScript, just raw HTTP.

Basic GET Request

curl http://localhost:8000/

Output:

<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Directory listing for /</title>
</head>
...

If the agent is not running, you get a clear error:

curl http://localhost:8000/

Output:

curl: (7) Failed to connect to localhost port 8000 after 0 ms: Connection refused

"Connection refused" means nothing is listening on that port. This is different from a timeout (something is blocking the connection) or a 404 (something is listening but does not have the requested resource).

Checking HTTP Status Codes

The -o /dev/null -s -w flags let you extract just the status code:

curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/

Output:

200
Status CodeMeaningAction
200SuccessAgent is healthy
404Not foundEndpoint path is wrong
500Server errorAgent crashed or has a bug
000Connection failedNothing is listening on that port

Sending POST Requests

Agent APIs often accept POST requests with JSON data:

curl -X POST http://localhost:8000/api/process \
-H "Content-Type: application/json" \
-d '{"task": "analyze", "input": "test data"}'

Output:

{"status": "processed", "result": "analysis complete"}

The flags:

FlagPurpose
-X POSTUse POST method instead of GET
-H "..."Set a request header
-d '...'Send data in the request body

Verbose Mode for Debugging

When something is not working and you need to see the full HTTP conversation:

curl -v http://localhost:8000/health

Output:

*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /health HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.81.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 15
<
{"status":"ok"}

Lines starting with > are what you sent. Lines starting with < are what the server returned. Lines starting with * are curl's own connection status.


SSH Connections: Reaching Your Server

SSH (Secure Shell) is the standard protocol for connecting to remote Linux servers. In Lesson 8, you generated an SSH key pair. Now you will use those keys to connect to a server.

Basic SSH Connection

ssh yourname@192.168.1.100

Output:

The authenticity of host '192.168.1.100 (192.168.1.100)' can't be established.
ED25519 key fingerprint is SHA256:abc123def456...
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.100' (ED25519) to the list of known hosts.
yourname@server:~$

The first time you connect to a server, SSH asks you to verify its fingerprint. This prevents man-in-the-middle attacks. After accepting, the fingerprint is saved in ~/.ssh/known_hosts and SSH will not ask again.

Key-Based Authentication

If your public key is already on the server (in ~/.ssh/authorized_keys), SSH authenticates automatically without a password. To copy your key to a server:

ssh-copy-id yourname@192.168.1.100

Output:

/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/yourname/.ssh/id_ed25519.pub"
Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'yourname@192.168.1.100'"
and check to make sure that only the key(s) you wanted were added.

After this, future connections use your key instead of a password:

ssh yourname@192.168.1.100

Output:

yourname@server:~$

No password prompt -- the key handled authentication silently.

Specifying a Port or Key File

If SSH runs on a non-standard port or you have multiple keys:

ssh -p 2222 -i ~/.ssh/agent-deploy-key yourname@192.168.1.100

Output:

yourname@server:~$
FlagPurpose
-p 2222Connect to port 2222 instead of default 22
-i ~/.ssh/agent-deploy-keyUse a specific private key file

SSH Config: Managing Multiple Servers

Typing ssh -p 2222 -i ~/.ssh/agent-deploy-key yourname@192.168.1.100 every time is tedious and error-prone. The SSH config file lets you create aliases.

Creating Your SSH Config

nano ~/.ssh/config

Add entries for each server:

Host agent-prod
HostName 192.168.1.100
User deploy
Port 2222
IdentityFile ~/.ssh/agent-deploy-key

Host agent-staging
HostName 10.0.0.50
User deploy
Port 22
IdentityFile ~/.ssh/id_ed25519

Host db-server
HostName 10.0.0.51
User dbadmin
IdentityFile ~/.ssh/db-key

Save and exit (Ctrl+O, Enter, Ctrl+X).

Set Correct Permissions

SSH refuses to use a config file with loose permissions:

chmod 600 ~/.ssh/config

Output:

(no output on success)

Connect Using Aliases

Now instead of the full command, use the alias:

ssh agent-prod

Output:

deploy@agent-prod:~$

SSH reads the config file, finds the agent-prod entry, and fills in the hostname, user, port, and key automatically.

Verify Your Config

grep -c "Host " ~/.ssh/config

Output:

3

Three server entries configured. You can add as many as you need -- production servers, staging environments, database hosts, CI/CD runners.


Hardening SSH on Your Server

Once key authentication works, disable password login to prevent brute-force attacks. This is a safety-critical change that requires careful sequencing.

SSH Lockout Prevention Protocol

ALWAYS keep a backup session open when modifying sshd_config. If you misconfigure SSH and close your only connection, you are locked out of the server. Follow these steps in exact order:

  1. Open two SSH sessions to the server
  2. In session 1, make sshd_config changes
  3. In session 1, restart SSH and test
  4. In session 2, verify you can still connect with a new connection
  5. Only after session 2 confirms access, close session 1

If session 2 cannot connect, use session 1 (which is still open) to revert your changes.

Step 1: Verify Key Authentication Works First

Before disabling passwords, confirm key login succeeds:

ssh -o PasswordAuthentication=no yourname@your-server

Output:

yourname@server:~$

If this fails with "Permission denied (publickey)", your key is not properly configured. Fix key authentication before proceeding -- disabling passwords now would lock you out.

Step 2: Edit sshd_config (In Session 1)

sudo nano /etc/ssh/sshd_config

Find and change these settings:

PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes

Output:

(nano editor with changes applied)
SettingValueWhy
PasswordAuthentication noDisables password loginPrevents brute-force attacks
PermitRootLogin noBlocks direct root SSHForces use of sudo from regular accounts
PubkeyAuthentication yesEnables key-based authShould already be yes by default

Step 3: Test Configuration Before Restarting

sudo sshd -t

Output:

(no output means configuration is valid)

If there is a syntax error, sshd -t will report it. Fix any errors before restarting.

Step 4: Restart SSH Service

sudo systemctl restart sshd

Output:

(no output on success)

Step 5: Verify from Session 2

In your second SSH session (the backup), open a new connection:

ssh yourname@your-server

Output:

yourname@server:~$

If this works, your configuration is correct. If it fails, use session 1 to revert the sshd_config changes and restart sshd again.


Basic Firewall with ufw

ufw (Uncomplicated Firewall) is Ubuntu's front-end for the kernel's netfilter firewall. It follows a simple model: set a default policy, then add exceptions.

Install and Check Status

sudo apt install -y ufw
sudo ufw status

Output:

Status: inactive

The firewall is installed but not active. Before enabling it, you must allow SSH -- otherwise enabling the firewall will immediately cut off your remote session.

Allow SSH Before Enabling

sudo ufw allow 22/tcp

Output:

Rules updated
Rules updated (v6)

Allow Your Agent Port

sudo ufw allow 8000/tcp

Output:

Rules updated
Rules updated (v6)

Set Default Deny and Enable

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable

Output:

Default incoming policy changed to 'deny'
Default outgoing policy changed to 'allow'
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

The default policy is deny incoming -- all ports are blocked unless explicitly allowed. Outgoing connections (your server reaching out to APIs, package repos) are allowed.

Check Your Rules

sudo ufw status numbered

Output:

Status: active

To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 8000/tcp ALLOW IN Anywhere
[ 3] 22/tcp (v6) ALLOW IN Anywhere (v6)
[ 4] 8000/tcp (v6) ALLOW IN Anywhere (v6)

Two ports are open: 22 (SSH) and 8000 (your agent). Everything else is blocked.

Removing a Rule

If you need to close a port:

sudo ufw delete allow 8000/tcp

Output:

Rule deleted
Rule deleted (v6)

Denying a Specific IP

If you see suspicious access attempts:

sudo ufw deny from 203.0.113.50

Output:

Rule added
Always Allow SSH First

If you enable ufw without allowing port 22, you will immediately lose SSH access to your server. Always run sudo ufw allow 22/tcp before sudo ufw enable. If you make this mistake on a cloud server, you may need to use your provider's console access to fix it.


Putting It All Together: Diagnostic Checklist

When your agent is unreachable, work through this diagnostic sequence:

StepCommandWhat It Tells You
1. Is it running?ss -tlnp | grep 8000Whether anything is listening on the port
2. What address?Check the Local Address column127.0.0.1 = local only, 0.0.0.0 = network accessible
3. Can I reach it locally?curl http://localhost:8000/healthWhether the agent responds at all
4. Is the firewall blocking?sudo ufw statusWhether the port is allowed through
5. Can I reach it remotely?curl http://server-ip:8000/healthWhether end-to-end connectivity works

This sequence moves from inside-out: first check the agent itself, then check network accessibility. Most problems are caught at step 1 or step 2.


Exercises

Exercise 1: Test Port Connectivity with curl

Task: Use curl to check whether anything is listening on port 8000 locally.

curl -s -o /dev/null -w "%{http_code}" http://localhost:8000

Expected output (if nothing is running):

000

Expected output (if an agent is listening):

200

A status code of 000 means nothing is listening. 200 means a healthy response. Any other code (404, 500) means something is listening but not responding as expected.

Exercise 2: Generate and Display SSH Keys

Task: If you already generated SSH keys in Lesson 8, verify they exist. If not, generate a new pair and display the public key.

ls ~/.ssh/id_ed25519* 2>/dev/null || ssh-keygen -t ed25519 -C "agent-deploy@mycompany.com" -N "" -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub

Verify:

ls ~/.ssh/id_ed25519*

Expected output:

/home/yourname/.ssh/id_ed25519
/home/yourname/.ssh/id_ed25519.pub

Both files present: private key (no extension) and public key (.pub).

Exercise 3: Create an SSH Config Entry

Task: Create an SSH config entry with a server alias.

mkdir -p ~/.ssh
touch ~/.ssh/config
chmod 600 ~/.ssh/config

cat >> ~/.ssh/config << 'EOF'
Host my-agent-server
HostName 192.168.1.100
User deploy
IdentityFile ~/.ssh/id_ed25519
EOF

Output:

(no output on success)

Verify:

grep -c "Host " ~/.ssh/config

Expected output:

1

One host entry confirmed. Add more entries as you set up additional servers.


Try With AI

Diagnosing a Connection Problem:

My AI agent is running on port 8000 but I can only reach it from
the server itself, not from my laptop. Walk me through the
diagnostic steps to figure out why.

What you're learning: AI walks you through a systematic networking diagnostic: first checking the binding address (is it 127.0.0.1 or 0.0.0.0?), then checking the firewall (is port 8000 allowed?), then checking routing (is there a NAT or proxy in the way?). This layered approach is how experienced system administrators diagnose connectivity problems -- starting from the service and working outward through each network layer.

Building Your SSH Config:

I have 5 servers I SSH into regularly. Here are their details:
- Production API: api.mycompany.com, user: deploy, port 2222, key: ~/.ssh/prod-key
- Staging: staging.mycompany.com, user: deploy, port 22, key: ~/.ssh/id_ed25519
- Database: db.internal.mycompany.com, user: dbadmin, key: ~/.ssh/db-key
- CI Runner: 10.0.0.200, user: ci, port 22, key: ~/.ssh/ci-key
- Dev sandbox: dev.mycompany.com, user: myname, key: ~/.ssh/id_ed25519

Generate my complete ~/.ssh/config file with short aliases.

What you're learning: Translating your specific infrastructure into SSH configuration is a task where AI collaboration saves significant time. You provide the domain knowledge (your servers, users, keys), AI handles the formatting. Review the output to verify the syntax matches what you learned in this lesson -- the Host, HostName, User, Port, and IdentityFile fields.

Locking Down SSH Safely:

I want to lock down SSH on my agent server. Walk me through the
changes to sshd_config step by step, but make sure I don't lock
myself out. I want to:
1. Disable password authentication
2. Disable root login
3. Limit SSH to specific users
Include a rollback plan for each step in case something goes wrong.

What you're learning: Safety-critical configuration changes benefit from step-by-step collaboration where each change is verified before proceeding to the next. AI provides the sequence and rollback commands, but you execute and verify each step -- this is the pattern for all security-sensitive system changes: change one thing, test, confirm, then proceed.