August 20, 2026
Creating a virtual machine in Oracle Cloud is fairly straightforward. Things usually get complicated afterward.
Your first SSH connection, a new default port, firewall rules in both Oracle Cloud and Linux, and a web server or development environment all introduce their own potential points of failure. From the outside, though, every one of them can look exactly the same: the connection simply does not work.
An SSH problem rarely comes down to sshd_config alone. The full connection path looks more like this:
My computer | Home or office network | The internet | Oracle Cloud network | Firewall inside the VM | SSH or web service
A failure anywhere along that path produces the same result from the outside: the connection fails.
I learned this while rebuilding my own VMs. Once, I changed the SSH port and lost both the new connection and the original one, so I had to recreate the VM. Another time, every server-side setting looked correct—and for good reason: the real problem was my home router.
This guide skips machine-specific details such as a particular public IP address or private-key path. Instead, it collects the steps and checks I reuse—and the mistakes I try not to repeat—whenever I set up a new Oracle Cloud Infrastructure (OCI) VM.
1. Decide What the VM Is For
Before opening ports one by one, decide what the VM is going to do. It might be a web server, API server, data-processing machine, automation server, database host, or development machine.
Once that role is clear, it becomes much easier to map the services and ports you actually need. A web VM might look like this:
SSH administration - 22 or a separate SSH port HTTP - 80 HTTPS - 443 Backend application - 3000, 8000, or 8080
The key distinction is that a service can use a port without that port being exposed to the internet.
Internet | Ports 80 and 443 | Nginx | 127.0.0.1:8000 | FastAPI, Django, or Flask
With this setup, only ports 80 and 443 need to be public. Port 8000 can stay on the loopback interface, where Nginx can reach it but the internet cannot.
A simple planning sequence is:
Choose the VM's role | List the services it will run | Assign the service ports | Separate private ports from public ports | Allow only what is required
2. Record Roles, Not Just Port Numbers
Port numbers get hard to keep straight once more than one VM is involved. I keep a small inventory that explains why each port exists.
VM-A Role: Web server SSH administration - 10022 HTTP - 80 HTTPS - 443 Backend app - 8000, private only Database - not publicly exposed
VM-B Role: Automation server SSH administration - 10022 Web service - none Automation jobs - run internally Public ports - SSH only
For each VM, I note its name, operating system, public IP address, default SSH user, private key, current SSH port, and purpose. That record becomes much more valuable once several keys and machines start to look alike.
3. Confirm the Image, Username, Address, and Key
The default SSH username depends on the operating-system image. Common examples are:
Ubuntu images - ubuntu Oracle Linux images - opc
A wrong username can produce the same authentication error as a wrong key:
Permission denied (publickey)
Before generating a replacement key, check the basics in this order:
- Is the public IP address correct?
- Is the SSH username correct for the selected image?
- Is this the private key used when the VM was created?
- Does the private key match the public key registered on the VM?
- Did I accidentally select a key belonging to another VM?
An authentication error does not necessarily mean the key itself is broken. Check the selected image and its default account first.
4. Test the Default Connection Before Changing Anything
Right after creating a VM, make sure the default SSH connection works. Do this before changing ports or firewall rules.
Create the VM | Connect with the default SSH settings | Confirm that the shell works | Begin configuration changes
If you change several settings at once, a failed connection gives you no clear way to tell which change caused it. A safer rhythm is:
Check the current state. Change one thing. Test the result. Then move on to the next change.
5. Change the SSH Port Without Locking Yourself Out
If you are replacing port 22 or adding another SSH port, the most important rule is simple: keep the existing SSH session open until a completely new connection succeeds on the new port.
A careful sequence looks like this:
Keep the current port 22 session open | Add the new SSH port | Allow it in the VM firewall | Allow it in the OCI ingress rules | Apply the SSH configuration | Confirm the new port is listening | Open a second terminal | Connect through the new port | Confirm the new shell works | Only then close the original session
I once closed the original session immediately after changing the port. The new port failed, and port 22 would not accept a connection either. At the time, I did not know the available recovery options well enough, so I deleted the VM and started over.
Since then, I have always used two terminals:
Terminal A Existing SSH connection Keep it open for recovery Terminal B Test a completely new SSH connection through the new port
Editing a configuration file is not proof that the change worked. The real test is whether a new, independent connection succeeds.
A nonstandard port may cut down on background scanning noise, but it is no substitute for key authentication, sensible access restrictions, and a properly maintained SSH service.
6. Treat OCI and the VM Firewall as Two Separate Layers
Opening a port in the OCI console does not automatically open it inside Linux. Traffic generally has to clear three separate checks:
OCI Security List or NSG allows TCP 10022
+
The VM firewall allows TCP 10022
+
SSH is actually listening on TCP 10022
All three must be correct. The same principle applies to HTTP and HTTPS.
Security Lists and Network Security Groups
OCI can control access through Security Lists, Network Security Groups (NSGs), or both, depending on the VNIC and subnet configuration. When a port does not work, check which policies actually apply to that instance.
The fields that matter are:
- Source CIDR
- IP protocol
- Destination port
It is easy to confuse the source and destination ports. The port the server listens on is the destination port. The client normally uses a temporary source port, so there is usually no reason to restrict it to the server's port number.
Do Not Expose SSH More Widely Than Necessary
An ingress source of 0.0.0.0/0 allows connection attempts from any IPv4 address. That can be convenient while testing, but it is better to restrict SSH to a trusted public IP or network when practical. If your home IP changes often, you may need to balance security against manageability.
7. Check the Host Firewall Carefully
On a system using iptables, rules can be listed with line numbers:
sudo iptables -L INPUT --line-numbers
Simply seeing an ACCEPT rule is not enough. iptables evaluates rules from top to bottom. If an earlier REJECT or DROP matches the packet, the later allow rule is never reached.
If it fits the current ruleset, you can insert an allow rule for the new SSH port:
sudo iptables -I INPUT -p tcp --dport 10022 -j ACCEPT
A web server may need corresponding rules for HTTP and HTTPS:
sudo iptables -I INPUT -p tcp --dport 80 -j ACCEPT sudo iptables -I INPUT -p tcp --dport 443 -j ACCEPT
These are examples, not commands to paste blindly. Advice like “delete rule 5” is dangerous because every server can have a different rule order and policy. Inspect the current firewall before inserting or deleting anything.
Also find out whether the VM is managed through iptables, nftables, UFW, or another firewall layer. Mixing tools without understanding how they relate can make the effective policy harder to see.
Test Persistence After Reboot
A firewall rule added by hand may disappear after a reboot. If you need persistent rules, use the method intended for your distribution, such as its firewall service or netfilter-persistent, and then test the result instead of assuming it was saved.
Working now | Reboot the VM | Still working
8. Check systemd Socket Activation on Ubuntu
Many SSH port-change guides begin and end with:
/etc/ssh/sshd_config
On some Ubuntu 24.04 installations, however, OpenSSH may use systemd socket activation. In that setup, ssh.socket can also control the port that actually listens for connections.
systemctl status ssh.socket systemctl status ssh
If socket activation is enabled, think of the configuration as two related layers:
sshd_config
+
ssh.socket
Use an Override When the Socket Owns the Listener
A systemd socket override may live at:
/etc/systemd/system/ssh.socket.d/override.conf
For example, an override that listens on ports 22 and 10022 over IPv4 can look like this:
[Socket] ListenStream= ListenStream=0.0.0.0:22 ListenStream=0.0.0.0:10022
The empty ListenStream= clears any inherited values before the desired listeners are declared. After changing the configuration, reload systemd:
sudo systemctl daemon-reload
Then apply the socket or service change in whatever way fits the current setup, while keeping the recovery session open.
Running systemctl edit does not prove that the intended override was saved. Check that the file exists, then confirm that systemd loaded its contents.
Validate Syntax, Binding, and the Real Listener
Before applying an OpenSSH change, validate its syntax:
sudo sshd -t
No output usually means the syntax check passed. Next, inspect the actual listening sockets:
sudo ss -tlnp
A line containing 0.0.0.0:10022 shows that something is listening on port 10022 across the IPv4 interfaces. If the service is bound only to an unexpected address or only to IPv6, a connection to the public IPv4 address may still fail.
That distinction matters:
systemctl - Is the process or unit active? ss - Is the expected address and port listening?
An active service does not guarantee that the listener you need actually exists.
9. Let the Error Message Narrow the Search
| Error | First area to investigate |
|---|---|
Permission denied (publickey) | Username, private key, registered public key, target VM, and authorized_keys |
Connection refused | SSH service, ssh.socket, actual listener, and address binding |
Connection timed out | Public IP, OCI rules, VM firewall, routing, local network, and provider restrictions |
These categories are not absolute, but they give you a useful place to start.
Other Security Layers
When the service and firewall both look correct, remember that other controls may still be involved:
Service configuration Firewall AppArmor or SELinux systemd socket activation Service-specific security policy
Ubuntu commonly uses AppArmor, while SELinux is central to some other distributions. The key is to check each layer separately.
10. Investigate the Network Outside the VM
One of my longest troubleshooting sessions turned out to have nothing to do with Oracle Cloud. Ethernet and Wi-Fi took different paths through my home network, and the Wi-Fi side ran through an extra router. Once I checked and adjusted that router's firewall or access-control policy, SSH started working.
If every server-side check passes, look at the rest of the route:
- Is the computer using Ethernet or Wi-Fi?
- Do wired and wireless clients use the same network path?
- Are multiple routers connected?
- Are the devices operating in router, access-point, or bridge mode?
- Does the router have a firewall, ACL, IP restriction, or MAC-based control?
- Could a security or parental-control feature be restricting outbound traffic?
If Ethernet works but Wi-Fi fails, or vice versa, investigate the local network before making more changes in OCI.
This Is Usually Not a Port-Forwarding Problem
An SSH connection from a home computer to an Oracle Cloud VM is outbound:
My computer -> Internet -> Oracle Cloud VM
Home-router port forwarding is normally used for the opposite direction:
Internet -> Home router -> Server inside the home
If a router change fixes outbound SSH, the cause is more likely a firewall, ACL, network separation, access policy, or outbound-port restriction than port forwarding.
Use Phone Tethering as a Quick Comparison Test
Trying a different network is one of the quickest ways to separate a server problem from a local-network problem.
Home Wi-Fi: SSH fails Phone tethering: SSH succeeds Likely investigation area: home router, local firewall, or internet connection
If the connection also fails through tethering and other networks, go back to OCI and the VM. I wish I had tried this earlier; it would have saved me a lot of time.
Check the Client Computer, VPN, and Network Provider
The client computer can also block or redirect traffic. On Windows, possible culprits include Windows Defender Firewall, endpoint-security software, antivirus products, VPNs, and proxies. If a connection works only when a VPN is enabled or disabled, that is an important clue because the network route has changed.
Some corporate, school, or internet-provider networks may restrict certain outbound ports. If the VM listener, OCI rules, and VM firewall are all correct, but tethering works while the usual network does not, look closely at the router or upstream network policy.
11. Plan the Public Address for a Web Server
You can reach a new web service through its public IP address, but a domain is easier to use and maintain. The two basic options are an independently registered domain or a free dynamic-DNS hostname such as DuckDNS.
DuckDNS Is Enough for Many Personal Projects
myproject.duckdns.org | Oracle VM public IP
For experiments, learning projects, and personal web applications, a DuckDNS hostname with HTTPS can work perfectly well.
It is not, however, a domain you own independently. The DNS hierarchy still looks like this:
duckdns.org `-- myproject.duckdns.org
That means you do not control the DNS zone for duckdns.org the way the owner of example.com controls that domain.
Analytics Can Work on a DuckDNS Hostname
Google Analytics collects data through a tag embedded in the page. As long as the site loads normally and sends that data, a DuckDNS hostname does not prevent basic page-view and visitor tracking on its own.
A Traditional Cloudflare Setup Requires More Control
A conventional Cloudflare setup expects control or delegation of the domain's DNS:
My registered domain | Cloudflare DNS, proxy, CDN, and WAF | Oracle Cloud VM
Because a DuckDNS hostname is a subdomain within a zone managed by DuckDNS, I generally cannot move it into the same full-zone Cloudflare setup as a domain I own. If I plan to use several subdomains, Cloudflare proxying, mail, APIs, Search Console, or more elaborate analytics and marketing services, an independent domain is much easier to manage.
example.com |-- www.example.com |-- api.example.com |-- admin.example.com `-- status.example.com
Expose the Reverse Proxy, Not Every Application
For a conventional web application, the public and private layers can stay simple:
Visitors | Ports 80 and 443 | Nginx | 127.0.0.1:8000 | Application
If the application port does not need direct internet access, leave it closed in the OCI Security List or NSG.
12. Finish the Operating-System and Development Setup
Reboot After Updates When Required
A new VM usually needs operating-system updates. Kernel and core library updates may require a reboot before they fully take effect. After rebooting, recheck the assumptions you made during setup:
- The VM boots normally.
- SSH starts correctly.
- The chosen SSH port is still listening.
- Firewall rules remain in effect.
- A new external SSH connection succeeds.
I do not consider the initial setup finished until the machine has rebooted and accepted a fresh connection.
Separate System Python from Project Python
A Linux distribution may rely on its system Python. Keep project packages isolated instead of installing them indiscriminately into that environment.
Operating system
`-- System Python
workspace/
|-- backend/
| `-- virtual environment
|-- crawler/
| `-- virtual environment
`-- automation/
`-- virtual environment
You can use venv, uv, Conda, or another tool. What matters is keeping this boundary:
Operating-system environment ≠ project environment.
Consistent names help too. A project named crawler might use crawler_env, while a project named vm-cluster might use vm_cluster_env. The exact style matters less than sticking to one convention.
Add a Local SSH Config Entry
Once the connection is stable, save its public address, user, port, and identity file in the SSH config on the client computer:
Host oracle-vm
HostName 203.0.113.10
User ubuntu
Port 10022
IdentityFile /path/to/private-key
The address and path above are placeholders. Once you replace them with real values, connecting becomes as simple as:
ssh oracle-vm
Tools such as VS Code Remote SSH can use the same config.
13. A Practical SSH Troubleshooting Order
After plenty of trial and error, I settled on this troubleshooting order:
- Connection details: public IP, SSH username, and correct key.
- SSH service:
sshorsshdstatus,ssh.socket, configuration syntax, and real listening port. - Security inside the VM: iptables, nftables, UFW, AppArmor, or SELinux.
- Oracle Cloud: Security List, NSG, destination port, route, and network attachment.
- Client computer: local firewall, VPN, proxy, and security software.
- Home or office network: Ethernet versus Wi-Fi, router firewall, ACLs, device mode, and multiple-router topology.
- Control test: try phone tethering or another independent network.
The short version is:
Permission denied - user, key, or authentication Connection refused - service, socket, or listener Connection timed out - firewall, OCI, router, or network
14. Recommended Order for a New Oracle Cloud VM
If I were setting up a fresh instance again, I would follow this order:
Choose the VM's role | List required services and ports | Create the VM | Confirm the OS image and default SSH user | Record the IP address and SSH key | Connect successfully through default port 22 | Update the operating system | Inspect the current SSH configuration | Add the new SSH port | Allow it in the VM firewall | Allow it in the OCI Security List or NSG | Validate SSH configuration syntax | Apply any systemd or ssh.socket changes | Confirm the real listening socket | Keep the original session open | Connect from a second terminal through the new port | Confirm the new session works | Close the original session | Reboot the VM | Reconnect through the new SSH port | For a web server, configure ports 80 and 443 | Connect a domain or DuckDNS hostname | Configure HTTPS and the reverse proxy | Create isolated development environments | Add the host to the local SSH config | Connect through tools such as VS Code Remote SSH
15. The Mistakes I Remember Most
- I used the wrong SSH username. What looked like a bad key was actually a mismatch between the OS image and its default account.
- I thought opening a port in OCI was enough. OCI networking and the firewall inside the VM both had to allow it.
- I ignored iptables rule order. An allow rule did nothing when an earlier reject rule matched first.
- I looked only at
sshd_config. On an Ubuntu setup using socket activation,ssh.socketmattered too. - I treated a configured port as an open port. What mattered was the actual listener reported by
ss. - I closed the old SSH session too early. Without a recovery shell, I eventually rebuilt the VM.
- I assumed every failure was on the server. One turned out to be caused by the different wired and wireless paths through my home network.
- I waited too long to test tethering. A phone hotspot immediately showed that the server was healthy.
- I confused service ports with public ports. Not every internal application port needed to be exposed to the internet.
- I treated free DDNS like an owned domain. DuckDNS is useful, but an independent domain is easier when full DNS control and Cloudflare services are required.
The Principle That Matters Most
The most valuable lesson was not any particular command. It was the process:
Inspect the current state | Change one thing | Confirm the result | Test the real behavior | Move to the next step
On a remote server, a value in a configuration file is not necessarily the server's effective state. I now verify the entire chain:
Configuration file | Service state | Listening address and port | Firewall inside the VM | Oracle Cloud network policy | Real connection from outside
If every server setting looks correct and the connection still fails, I stop changing the server and inspect the whole route:
My computer | Local firewall or VPN | Ethernet or Wi-Fi | Router | Internet provider | Oracle Cloud | VM firewall | SSH or web service
A block at any one of these layers looks like the same connection failure to the user. The quickest way through a complicated SSH or network problem is not to change settings at random. It is to identify the failing layer and narrow the search one step at a time.