name: rclone-oauth-headless-vps
description: Authorize rclone Google Drive OAuth on a headless VPS using cloudflared tunnel + pexpect — no browser needed on server
triggers:
- rclone config reconnect headless
- google drive oauth no browser vps
- rclone token expired headless server
Rclone OAuth on Headless VPS via Cloudflared Tunnel
Problem
Rclone needs OAuth authorization via browser, but the VPS has no browser and no X server. OAuth device flow (rclone config reconnect) requires user approval on a machine with browser access.
Solution
1. Start rclone config reconnect with pexpect to answer interactive prompts
2. Rclone creates a local OAuth server on http://127.0.0.1:53682
3. Expose that local port to the public internet using cloudflared tunnel --url http://localhost:53682
4. User visits the resulting public URL in their browser, approves, and rclone receives the token
Prerequisites
- -
cloudflaredinstalled - -
pexpectinstalled:pip3 install pexpect - - rclone remote already configured (just needs token refresh)
- -
printf 'y\n' | rclone config reconnectdoes NOT work — rclone reads from TTY, not stdin pipe. Must usepexpect. - - Cloudflared quick tunnels (no auth needed) work fine for temporary OAuth exposure.
- - Token is saved directly into
/root/.config/rclone/rclone.confby rclone after successful OAuth.
Script
`python
#!/usr/bin/env python3
"""Start rclone OAuth and expose via cloudflared tunnel."""
import subprocess, threading, time, sys, pexpect
oauth_url = None
def run_rclone():
global oauth_url
child = pexpect.spawn('rclone config reconnect gdrive:', encoding='utf-8')
child.logfile = sys.stdout
child.expect('Already have a token', timeout=10); child.sendline('y')
child.expect('web browser', timeout=10); child.sendline('y')
child.expect('http://127.0.0.1:53682/auth', timeout=15)
for line in child.before.split('\n'):
if 'http://127.0.0.1:53682/auth' in line:
oauth_url = line.strip().split()[-1]
break
child.expect(pexpect.EOF, timeout=300)
t = threading.Thread(target=run_rclone); t.start()
for i in range(30):
if oauth_url: break
time.sleep(1)
print(f"OAuth URL: {oauth_url}")
cf = subprocess.Popen(['cloudflared', 'tunnel', '--url', 'http://localhost:53682'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
tunnel_url = None
for line in cf.stdout:
print(line, end='')
for word in line.split():
if 'trycloudflare.com' in word:
tunnel_url = word.strip().rstrip('/')
break
if tunnel_url: break
print(f"\n=== OPEN THIS URL IN YOUR BROWSER ===\n{tunnel_url}\n")
t.join()
`
Key Lessons
Verification
After user approves in browser:
`bash
rclone lsd gdrive: --max-depth 1
`