Logging in to Codex CLI on a Remote Server with SSH Port Forwarding

7 minute read

Published:

Logging in to Codex CLI on a Remote Server with SSH Port Forwarding

I recently ran into a confusing issue while trying to log in to the OpenAI Codex CLI on a remote Linux server.

Running:

codex login

worked normally at first. Codex printed an authentication URL and started a local login server:

Starting local login server on http://localhost:1455.

If your browser did not open, navigate to this URL to authenticate:
https://auth.openai.com/oauth/authorize?...

I copied the authentication URL into the browser on my laptop, signed in to my OpenAI account, and then the browser redirected me to something like:

http://localhost:1455/auth/callback?code=...

That is where things stopped working: the browser could not connect to localhost:1455.

The problem was not the OpenAI login itself. The problem was that localhost referred to two different machines.

Why this happens

My setup looked like this:

Laptop
  |-- Web browser
  |
  `-- SSH
       |
       v
Remote Linux server
  `-- codex login

Codex was running on the remote server. When it started its OAuth callback server at:

localhost:1455

that meant port 1455 on the remote server.

But my browser was running on my laptop. When the browser was redirected to:

http://localhost:1455/auth/callback

it interpreted localhost as my laptop, not the remote server.

In other words:

Codex expects:   remote-server:1455
Browser opens:   laptop:1455

Nothing was listening on port 1455 on my laptop, so the callback failed.

This is a general issue with browser-based OAuth flows when a command-line application is running on a remote or headless machine.

OpenAI documents that Codex CLI can sign in with a ChatGPT account and stores credentials locally after authentication:

The solution: SSH local port forwarding

The solution is to forward port 1455 on the laptop to port 1455 on the remote server.

SSH already provides exactly this feature through local port forwarding with the -L option.

The working setup is:

Browser on laptop
       |
       | http://localhost:1455
       v
Laptop port 1455
       |
       | SSH tunnel
       v
Remote server port 1455
       |
       v
codex login

Step 1: Start Codex login on the remote server

SSH into the remote machine as usual and run:

codex login

You should see something similar to:

Starting local login server on http://localhost:1455.

If your browser did not open, navigate to this URL to authenticate:
https://auth.openai.com/oauth/authorize?...

Leave this process running. Codex is now waiting for the OAuth callback.

Step 2: Create an SSH tunnel from your laptop

Open a second terminal on your laptop, not on the remote server.

Run:

ssh -N -L 1455:localhost:1455 username@remote-server

For example:

ssh -N -L 1455:localhost:1455 myuser@login.example.edu

The options mean:

-N
    Do not start an interactive remote shell.
    Keep the SSH connection open only for forwarding.

-L 1455:localhost:1455
    Forward port 1455 on my laptop to localhost:1455
    as seen from the remote server.

I prefer the slightly more explicit form:

ssh -N \
    -L 127.0.0.1:1455:127.0.0.1:1455 \
    username@remote-server

This explicitly binds the local forwarded port to the loopback interface.

Keep this SSH connection running while you complete authentication.

OpenSSH documents -L as local TCP port forwarding:

Step 3: Open the authentication URL in your local browser

Return to the terminal where codex login is running.

Copy the long URL beginning with:

https://auth.openai.com/oauth/authorize?...

and open it in the browser on your laptop.

Complete the normal OpenAI sign-in process.

Eventually, the browser should redirect to something like:

http://localhost:1455/auth/callback?code=...

This time the request reaches Codex because the SSH tunnel forwards it:

localhost:1455 in the browser
        |
        v
SSH local port forwarding
        |
        v
localhost:1455 on the remote server
        |
        v
Codex OAuth callback server

The original terminal running codex login should then report that authentication succeeded.

You do not need to manually copy the code= value

This was the part that initially confused me.

When I first saw a URL like:

http://localhost:1455/auth/callback?code=...

I assumed that I needed to extract the value after code= and paste it back into the terminal.

That is not necessary when the callback flow is working correctly.

The authorization code is intended to be delivered directly to the temporary callback server started by codex login. The SSH tunnel simply makes that callback server reachable from the browser on your laptop.

So instead of manually handling the OAuth code, let the browser callback reach Codex through the SSH tunnel.

What if Codex suggests --device-auth?

On a remote or headless machine, Codex may suggest:

codex login --device-auth

That may be the simplest option in environments where device authentication is allowed.

However, some organizations or managed environments may not permit the device-auth flow. In that situation, SSH port forwarding is a practical way to use the regular browser login flow with:

codex login

If you connect through a jump host

Many research clusters, HPC systems, and corporate environments require a bastion or jump host.

If your normal connection looks like:

ssh -J jump.example.edu user@compute.example.edu

add the local forwarding to the destination connection:

ssh -N \
    -J jump.example.edu \
    -L 1455:localhost:1455 \
    user@compute.example.edu

If your SSH configuration is already defined in ~/.ssh/config, it may be as simple as:

ssh -N -L 1455:localhost:1455 my-server

The important point is that the SSH connection containing -L must ultimately reach the same machine where codex login is listening.

Troubleshooting

The browser still says localhost:1455 refused the connection

First, confirm that codex login is still running on the remote server.

You can also check whether something is listening on port 1455:

ss -ltn | grep 1455

Then confirm that the SSH tunnel is still running on your laptop.

Port 1455 is already in use on my laptop

Check which process is using it:

lsof -iTCP:1455 -sTCP:LISTEN

If an old SSH tunnel is still running, stop it before starting a new one.

I created the tunnel after the browser login already failed

Cancel the old login attempt on the remote server:

Ctrl+C

Then start again:

codex login

Use the newly generated authentication URL.

OAuth login attempts contain temporary state, so it is better not to mix an old browser callback with a newly started Codex login process.

Codex uses a different port

Do not assume that 1455 will always be the callback port.

If Codex prints a different port in the future, use that port in the SSH forwarding command. For example, if it prints localhost:9999, use:

ssh -N -L 9999:localhost:9999 username@remote-server

Security note

The authentication URL and callback URL contain temporary OAuth parameters. Avoid posting complete versions of those URLs in screenshots, GitHub issues, Slack messages, or blog posts.

In particular, redact values associated with fields such as:

code=
state=
code_challenge=

You should not need to manually copy or share the callback authorization code when using the SSH tunnel approach.

The short version

The entire problem can be summarized in one sentence:

Codex was listening on localhost:1455 on the remote server, while my browser interpreted localhost:1455 as my laptop.

SSH local port forwarding connects those two localhost environments.

On the remote server:

codex login

On the local computer:

ssh -N -L 1455:localhost:1455 username@remote-server

Then open the authentication URL printed by Codex in your local browser.

After login, the browser redirects to localhost:1455; SSH forwards that callback to the remote server; and Codex completes authentication.

Once you understand which machine each localhost refers to, the issue becomes straightforward to diagnose.

References