Forgejo on Railway with 2FA
I run my own Forgejo on Railway. It just works. The web UI is fast, deploys are easy, and most days I forget it is even there.
Then I turned on 2FA. Seemed like the obvious thing to do, and the UI made it a two-click job. What it did not say is that Git over HTTPS has no clue about MFA. Once 2FA is on, Forgejo stops taking your account password for pushes, same as Gitea. So my next push just failed.
Here is the error I got:
remote: Credentials are incorrect or have expired. Retry your command or see
https://codeberg.org/forgejo/forgejo/issues/2809 for more informationThat thread is full of people hitting the same wall. The fix is to use a Personal Access Token (PAT) instead of your password.
The HTTPS detour: Personal Access Tokens
A PAT is straightforward. Generate one in Forgejo under
Settings → Applications, give it write:repository scope, and paste it as
your password when Git prompts. For a moment, it worked. I configured Git to
cache the credential, and life moved on.
But it bugged me. I had a token sitting in a credential store. I could revoke it, but it was still one more secret to manage. SSH felt cleaner, a key pair, no passwords, no tokens, same setup as every other Git host.
The catch is that Railway only exposes the HTTP port for a service by default.
Nothing outside can reach the SSH daemon in the container. The way around it is
Railway’s TCP Proxy feature. Heads up, I replaced my real proxy address
with <tcp-host> and <tcp-port> below, yours will look different.
The TCP proxy and the OpenSSH impostor
A TCP proxy in Railway forwards a public port to an internal container port.
The first attempt was obvious: point the proxy at port 22, where the system’s
SSH daemon is listening. I created the proxy, pointed it at :22, and
connected.
It connected, but auth failed.
ssh -T forgejo
pyk@<tcp-host>: Permission denied (publickey).Verbose output gave it away:
ssh -vT forgejo 2>&1 | grep "remote software version"
debug1: Remote protocol version 2.0, remote software version OpenSSH_10.2That is the system OpenSSH daemon, not Forgejo’s built-in SSH server. Forgejo’s
own server identifies as SSH-2.0-Go. The system daemon knows nothing about
the SSH keys stored in Forgejo’s database. I was knocking on the wrong door.
So I stopped fighting the system daemon and turned on Forgejo’s own SSH server on its own internal port.
Turning on Forgejo’s built-in SSH server
Forgejo, like Gitea, can run its own SSH server. This is how you are supposed
to do it in containers when you want key-based Git access. The settings live
under the server section.
I added these to my Railway environment:
FORGEJO__server__START_SSH_SERVER="true"
FORGEJO__server__SSH_LISTEN_PORT="2222"
FORGEJO__server__SSH_LISTEN_HOST="0.0.0.0"
FORGEJO__server__SSH_DOMAIN="<tcp-host>"
FORGEJO__server__SSH_PORT="<tcp-port>"
FORGEJO__server__SSH_ROOT_PATH="/data/gitea/ssh"The important one is START_SSH_SERVER. Without it, Forgejo never opens a
listening socket. The SSH_LISTEN_PORT is the internal port. I picked 2222 so
it does not clash with the system daemon on 22. The SSH_DOMAIN and SSH_PORT
are just for display, they set the clone URLs shown in the web UI.
After redeploying, the logs showed it was up:
modules/ssh/init.go:30:Init() [I] SSH server started on 0.0.0.0:2222.Next, I updated the Railway TCP proxy to forward the public port to :2222
instead of :22.
The git user gotcha
With the proxy fixed, I tried again. The banner looked right this time:
ssh -vT forgejo 2>&1 | grep "remote software version"
debug1: Remote protocol version 2.0, remote software version GoBut auth still failed. My ~/.ssh/config had User pyk. The server rejected
the key before it ever looked it up.
Normally the Forgejo SSH server runs as one system user, usually git. Your
Forgejo username is not the SSH login. Forgejo just matches the SSH key you
offer against the keys in its database. The SSH layer only sees the git user,
then the app maps it to your account.
The fix was one line:
Host forgejo
HostName <tcp-host>
Port <tcp-port>
User git
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yesWith that, it worked:
ssh -T forgejo
Enter passphrase for key '/home/pyk/.ssh/id_ed25519':
Hi there, pyk! You've successfully authenticated with the key named ubuntu, but
Forgejo does not provide shell access.The message says Hi there, pyk! even though I logged in as git. That is
exactly how it works: the git user is the system account, and Forgejo maps
the key to your identity.
The final remote
With SSH working, I set the remote to:
git remote set-url origin git@forgejo:pyk/<repo>.gitThe git@forgejo is the SSH connection. The pyk/<repo> is the repository
path resolved by Forgejo. No tokens, no passwords, no credential caching. The
push just works.
Fun detour. The HTTPS and PAT path works, but it always felt like a hack around 2FA. SSH is the natural fit for Forgejo, and with Railway’s TCP proxy it is reachable from the outside. The lesson is that the SSH user is not your username, and the system OpenSSH daemon is not Forgejo’s SSH server. Pushes just work now, no tokens involved.
| Tags | forgejo , git , ssh , railway , self-hosting , 2fa |
|---|