Home / OCI

OCIHow-toBeginner

Reaching private OCI instances with the Bastion service: managed SSH and port forwarding

Create an OCI bastion, open managed SSH and port forwarding sessions to private instances and databases, and fix the usual connection failures.

Deepak Bhardwaj7 min read

Applies toOCI Bastion service, OCI Compute (Oracle Linux 8/9), Oracle Cloud Agent Bastion plugin, OCI CLI 3.x

Goal

You need a shell on a compute instance, or a SQL client connection to a database, in a private subnet. There is no public IP, no VPN, and you don't want to run a jump host. The OCI Bastion service gives you short-lived, audited SSH sessions into that subnet instead.

There are two session types:

Session type What you get Needs on the target
Managed SSH An SSH shell on a Linux instance OpenSSH server, Oracle Cloud Agent running, Bastion plugin enabled
Port forwarding A local port tunnelled to any private IP and port (SSH, RDP, 1521, 5432, 443) Only that the port is listening and reachable from the bastion

If the target isn't a Linux compute instance (a DB system, an Autonomous Database private endpoint, a Windows host), use port forwarding.

1. Check the prerequisites

  • An IAM policy for the people who will open sessions (see below).
  • The target subnet's security list or NSG allows ingress from the bastion's private endpoint IP on the target port (22 for managed SSH).
  • For managed SSH: the target subnet has a route to a service gateway (All region Services in Oracle Services Network), NAT gateway or internet gateway, so the Oracle Cloud Agent can reach OCI.
  • For managed SSH: the Bastion plugin is enabled on the instance. It is disabled by default.
  • An SSH key pair on your workstation. The public key goes into the session; the private key stays with you.

A minimal policy for an operations group:

Allow group OpsAdmins to use bastion in compartment Ops
Allow group OpsAdmins to manage bastion-session in compartment Ops
Allow group OpsAdmins to read instances in compartment Ops
Allow group OpsAdmins to read instance-agent-plugins in compartment Ops
Allow group OpsAdmins to read virtual-network-family in compartment Ops

Only the people who create and delete bastions need manage bastion.

Tip

You can limit which OS user a group may log in as by adding a condition to the session statement, for example where target.bastion-session.username = 'appadmin'. The default opc user has passwordless sudo on platform images, so restrict it where you can.

2. Create the bastion

Create one bastion per target subnet or VCN, in the same region as the targets. The bastion is regional.

oci bastion bastion create \
  --bastion-type standard \
  --compartment-id ocid1.compartment.oc1..xxxx \
  --target-subnet-id ocid1.subnet.oc1..xxxx \
  --name ops-bastion \
  --client-cidr-list '["203.0.113.10/32"]' \
  --max-session-ttl 10800 \
  --wait-for-state SUCCEEDED
  • --client-cidr-list is the allowlist of client IPs that may connect. Keep it as narrow as possible and don't use 0.0.0.0/0.
  • --max-session-ttl caps every session on this bastion. Allowed values run from 30 minutes to 3 hours; check the current limits in the Bastion docs.
  • --dns-proxy-status ENABLED lets port forwarding sessions target an FQDN instead of an IP.

Get the bastion's private IP, then add it as the ingress source on the target subnet's security list or NSG:

oci bastion bastion get --bastion-id ocid1.bastion.oc1..xxxx \
  --query 'data."private-endpoint-ip-address"' --raw-output

If your public IP changes (home broadband, a new office egress), update the allowlist:

oci bastion bastion update --bastion-id ocid1.bastion.oc1..xxxx \
  --client-cidr-list '["198.51.100.25/32"]'

3. Enable the Bastion plugin (managed SSH only)

Console: Compute → Instances → your instance → Oracle Cloud Agent tab, set Bastion to Enabled. The change can take up to 10 minutes to apply.

Check it from the CLI:

oci instance-agent plugin list \
  --compartment-id ocid1.compartment.oc1..xxxx \
  --instanceagent-id ocid1.instance.oc1..xxxx \
  --name Bastion

You want status RUNNING. INVALID or a missing plugin usually means the agent can't reach OCI. Fix the route to the service gateway first.

4. Open a managed SSH session

oci bastion session create-managed-ssh \
  --bastion-id ocid1.bastion.oc1..xxxx \
  --target-resource-id ocid1.instance.oc1..xxxx \
  --target-os-username opc \
  --ssh-public-key-file ~/.ssh/id_ed25519.pub \
  --session-ttl 3600 \
  --display-name "inc-4821-opc" \
  --wait-for-state SUCCEEDED

When the session is ACTIVE, get the connection command OCI generates for it:

oci bastion session get --session-id ocid1.bastionsession.oc1..xxxx \
  --query 'data."ssh-metadata".command' --raw-output

It has this shape. Replace <privateKey> with the path to your private key:

ssh -i ~/.ssh/id_ed25519 \
  -o ProxyCommand="ssh -i ~/.ssh/id_ed25519 -W %h:%p -p 22 ocid1.bastionsession.oc1..xxxx@host.bastion.eu-frankfurt-1.oci.oraclecloud.com" \
  -p 22 opc@10.0.2.15

5. Open a port forwarding session

Example: reach a Base Database listener on 10.0.3.20:1521 from your laptop.

oci bastion session create-port-forwarding \
  --bastion-id ocid1.bastion.oc1..xxxx \
  --target-private-ip 10.0.3.20 \
  --target-port 1521 \
  --ssh-public-key-file ~/.ssh/id_ed25519.pub \
  --session-ttl 3600 \
  --display-name "inc-4821-db" \
  --wait-for-state SUCCEEDED

Start the tunnel and leave it running in its own terminal:

ssh -i ~/.ssh/id_ed25519 -N -L 1521:10.0.3.20:1521 -p 22 \
  ocid1.bastionsession.oc1..xxxx@host.bastion.eu-frankfurt-1.oci.oraclecloud.com

Then point your client at localhost:1521. The same pattern works for RDP: forward local port 3389 (or 13389 if it's in use) to the Windows host's port 3389.

6. Clean up

Sessions expire at their TTL, but delete them when the work is done:

oci bastion session list --bastion-id ocid1.bastion.oc1..xxxx --all \
  --query 'data[?"lifecycle-state"==`ACTIVE`].{name:"display-name",id:id}' --output table
oci bastion session delete --session-id ocid1.bastionsession.oc1..xxxx --force

When it doesn't connect

Symptom Likely cause Fix
Session goes to FAILED on create (managed SSH) Bastion plugin not running on the target Enable the plugin, wait up to 10 minutes, check oci instance-agent plugin list
Connection timed out to host.bastion... Your public IP isn't in client-cidr-list, or the hostname has the wrong region Update the allowlist; use the bastion's region in the hostname
Permission denied (publickey) on the first hop Private key doesn't match the public key in the session Use the matching key; chmod 600 the private key
First hop works, second hop times out Target security list or NSG doesn't allow the bastion's private IP on the port Add an ingress rule from the bastion IP/32 on 22 (or the target port)
Permission denied on the second hop Wrong OS user, or the user can't log in with keys Check --target-os-username; check sshd_config and authorized_keys on the target
channel 0: open failed on the tunnel Nothing is listening on the target port, or a host firewall blocks it Check the service with ss -tlnp; open the port in firewalld
Tunnel drops after a while Session TTL reached Create a new session; raise --session-ttl up to the bastion's max

Note

Bastion session create and delete calls are recorded in the OCI Audit log, so you can see who opened a session, when, and to which target.

Prevent it next time

  • Create bastions as part of the network build (Terraform oci_bastion_bastion), not during an incident.
  • Add the bastion's private IP to the target NSGs when you create them, and enable the Bastion plugin in your instance image or launch template.
  • Keep client-cidr-list limited to your office and VPN egress ranges, and review it when those change.
  • Use short TTLs and include the ticket number in --display-name so the audit trail makes sense.
  • Give people manage bastion-session but not manage bastion, so they can't widen the allowlist themselves.