DEV Community

MilkyWay008
MilkyWay008

Posted on

Why your CLI says you're not logged in on a headless Linux server

You finish the OAuth flow on a fresh VPS. The browser says success. The CLI writes a token file with an expiry three weeks out. Then it hangs on "Verifying your account", or tells you that you are not logged in.

If the token file is right there and still valid, you are probably not fighting the login. You are fighting the credential store the CLI looks in first, which on Linux is the desktop secret service. On a headless box that service is not running.

What the CLI is actually asking for

Most cross-platform CLIs (Gemini CLI, gh, gcloud, the agent CLIs, anything built on keytar or libsecret) do not keep OAuth tokens in a plain file. They hand them to the OS secret store:

  • Linux: libsecret, which talks to org.freedesktop.secrets over D-Bus, implemented by gnome-keyring or KWallet
  • macOS: Keychain
  • Windows: Credential Manager

On a desktop this is invisible. Your login session starts a session bus and unlocks the keyring, so reads and writes just work.

Over SSH as root on a cloud image, none of that exists. No session bus means DBUS_SESSION_BUS_ADDRESS is empty. No daemon means nothing is registered on the bus. And even when gnome-keyring is installed, it is usually locked, because unlocking happens through PAM at a graphical login that never took place.

The part that wastes an evening: when the keyring call throws, most CLIs catch the exception and print the same message they print for a missing token. "You are not logged in." You are logged in. The file is there. The process just cannot reach its credential store. I have seen that exact line in agent CLI logs sitting next to a token file with a valid future expiry.

Three commands to confirm it

echo "$DBUS_SESSION_BUS_ADDRESS"     # empty means no session bus
secret-tool lookup service test      # not found / cannot connect means no secret service
timedatectl status                   # check the clock while you are here
Enter fullscreen mode Exit fullscreen mode

If the first is empty and the second fails, you have found your problem.

The fix

Install the pieces, then give the CLI a session bus with an unlocked keyring:

sudo apt update
sudo apt install -y dbus-x11 gnome-keyring libsecret-tools
Enter fullscreen mode Exit fullscreen mode

Package names vary by distro, so a quick apt-cache search secret-tool (or your distro's equivalent) is worth doing before you trust that list. On Fedora and RHEL the packages are dbus-x11, gnome-keyring and libsecret.

Then run the login inside a private session bus, with the keyring unlocked from stdin:

dbus-run-session -- bash -c '
  printf %s "$KEYRING_PW" | gnome-keyring-daemon --unlock --components=secrets
  exec your-cli login
'
Enter fullscreen mode Exit fullscreen mode

dbus-run-session starts a bus for that one command tree. gnome-keyring-daemon --unlock reads the password from stdin and exports GNOME_KEYRING_CONTROL and GNOME_KEYRING_PID for its children. The CLI then finds a working secret service, stores the token, and stops pretending you never logged in.

Confirm the store is reachable:

printf 'hunter2' | secret-tool store --label=test svc key
secret-tool lookup svc key
Enter fullscreen mode Exit fullscreen mode

If lookup prints the value back, you are good.

Two caveats I would rather say out loud. Running as root is the worst case: root's session does not get a PAM keyring unlock, /run/user/0 may have no bus at all, and some backends refuse root outright. Where you can, create a normal user, run loginctl enable-linger <user>, and do the login as that user, so a systemd user session keeps a bus alive between SSH sessions. On a long-lived server you want that bus to outlive your SSH connection, otherwise the next cron run starts from scratch.

If the host is disposable and you only need it working now, a keyring created with an empty password is an option. It is still a secret store, but it is one that anyone with the file can open. I would treat it as a last resort.

The other thing that breaks login right after a login: the clock

Different cause, same symptom family. Refresh tokens are validated against server time. A VM or container whose clock is minutes off gets invalid_grant on refresh even with a perfectly good keyring, which looks exactly like a successful OAuth login followed by an immediate auth failure. There is a gemini-cli issue on Ubuntu reporting invalid_grant with exit code 41, where clock skew and stale credentials are among the usual suspects.

timedatectl status
sudo timedatectl set-ntp true
Enter fullscreen mode Exit fullscreen mode

Sometimes there is a token env var and you can skip all of this

Headless auth is common enough that plenty of tools ship an escape hatch:

  • gh: GH_TOKEN bypasses keyring storage entirely
  • gcloud: --no-launch-browser, with CLOUDSDK_CONFIG controlling where credentials live
  • docker: credential helpers, or a plain config file

Thirty seconds with your-cli --help | grep -i token is worth it before you install a keyring stack. If the tool documents a file-based credential store, use it instead.

What not to do

  • Do not export an invented DBUS_SESSION_BUS_ADDRESS. The bus has to exist. Pointing at a socket that is not there gives you a different error and a longer afternoon.
  • Do not put the token or the keyring password in argv or a shell env var on a shared box. /proc and CI logs are not a secret store.
  • Do not assume a token file means an authenticated session. That assumption is the whole bug.

None of this is exotic. It is a desktop assumption leaking into server tooling, and it costs people an evening because the error message points at the wrong thing. Worth a try: read the CLI's own log for the word keyring, secret, or token source before you change anything. If a keyring error is in there, the steps above are the path. If it is not, it may genuinely be a bad credential, and the auth logs are the next stop.

Top comments (0)