For years my Gitea instance (in Docker) did have proper HTTPS clone URLs using a Reverse Proxy that handles domain and SSL termination, but I could never get SSH to work properly and had to expose the Gitea built-in SSH server on a different port.

This post describes my setup step by step in case you (or I) need to recreate it.

Prerequisites

  • a working Gitea instance
  • with its User Accounts set up
  • at least one user with a Public Key set up
  • your Synology has its SSH service properly configured, secured, exposed through the firewall

SSH fundamentals

But first a few things to understand, which I fully grasped way too late into the process:

  • There will be just one SSH server running (on the host)

  • During authentication the SSH protocol only allows authentication. And exactly once.

    That means you cannot during the authentication forward the connection to another server, only after authentication. But any authentication required by the second server would fail because your (git) client thinks auth has already completed (which in a way it has)

  • Agent-Forwarding might work in theory, but we want regular ssh clones without any host config modifications required, that matters doubly on CI builds.

The setup

Gitea comes batteries-included with little helpers for that. But the tricky part is getting this to work reliably and securely. We’ll mount the git’s .ssh dir read-write into the Gitea container and Gitea will then keep it updated with all its valid users public keys and an additional tagged command that lets Gitea to detect the user and allows further processing.

Setting up the git user

We need a system user that all git clones then are funnelled through (The git@ part in git@$domain.tld).

Synology’s Web Interface will block the creation of a git user for security reasons. Terminal it then is:

sudo synouser --add git '' "" 0 "" 0

Then set an invalid password for the git user to prevent password authentication:

sudo synouser --setpw git '*'

While you’re at it, go to the Web Interface and tick the “Prevent user from changing password” checkbox.

Setting up the git user’s SSH directory

sudo mkdir -p /var/services/homes/git/.ssh
sudo touch /var/services/homes/git/.ssh/authorized_keys
sudo chown -R 1000 /var/services/homes/git/.ssh
sudo chmod 700 /var/services/homes/git/.ssh
sudo chmod 600 /var/services/homes/git/.ssh/authorized_keys
sudo chmod 755 /var/services/homes/git

Next we remove Synology’s extended ACLs from the git’s home directory, as sshd’s StrictModes rejects directories with ACL entries:

sudo /usr/syno/bin/synoacltool -del /var/services/homes/git

Preparing the git user account settings

The user in my Gitea Docker container has the user id 1000 and because SSH PubKey auth is set to be picky when it comes to file permissions of its key files, I had to match the uid on the host.

First let’s check if the user id 1000 is free:

id 1000

In my case it said “no such user”, which is exactly what we need.

Open the passwd file and change the git user id entry to 1000.

Please take utmost care when editing this file, as it potentially could break your Synology install and lock you out of it!

sudo vi /etc/passwd

Find the git line and change it to:

git:x:1000:100::/var/services/homes/git:/bin/sh

This a changes the git user id to 1000 and the login shell to a valid value.

Now is a good time to restart the ssh service using

sudo synoservicectl --restart sshd

Forwarding to Gitea

This is the tricky part I didn’t understand at first: An entry in the authorized_keys file can have additional parameters, like restricting the user to certain commands after auth.

We’ll create our own wrapper to forward to Gitea and restrict the git user to only be able to execute this one command that connects through to Gitea as /usr/local/bin/gitea-serv:

sudo vi /usr/local/bin/gitea-serv

And put the following content there:

#!/bin/sh
exec /volume1/@appstore/ContainerManager/usr/bin/docker exec -i -u git -e SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea /app/gitea/gitea serv "$@" --config /data/gitea/conf/app.ini

Make it executable:

sudo chmod 755 /usr/local/bin/gitea-serv

Allow gitea-serv in sudoers

By default no user can access Gitea (or any other Docker container for that matter). We could add the git user to the docker group, but that would give it full access to any Container. We don’t want that. But we can give it sudo access without password check to the gitea-serv command:

sudo sh -c 'cat > /etc/sudoers.d/git-gitea << EOF
git ALL=(root) NOPASSWD: /usr/local/bin/gitea-serv
Defaults>root env_keep+=SSH_ORIGINAL_COMMAND
EOF'

Verify it works by switching to the git user and testing:

sudo su -s /bin/sh git -c 'sudo /usr/local/bin/gitea-serv serv key-1'

If it works the expected output should be something like Hi there, <username>! You've successfully authenticated.... The actual name doesn’t matter, as key-1 is just the first key that Gitea has stored.

If it didn’t work yet, it’ll probably sort itself out with the next step.

Mount the git users’ .ssh directory into the Gitea container

I use docker-compose for the Gitea install, so I had to add this in the volumes section:

volumes:
  - other volumes
  - /var/services/homes/git/.ssh:/data/git/.ssh:rw

But if you use plain old Docker, just add the directory in there.

Please note: You have to mount the entire .ssh dir, as with file-based mounts Gitea has issues writing the keys.

Update Gitea’s configuration

As already mentioned earlier, we have to tell Gitea to write the correct command (our gitea-serv) to the authorized_keys file, and we have to disable the built-in ssh server.

In my case I updated the docker-compose.yaml again:

environment:
  - SSH_DOMAIN=${ssh domain}
  - SSH_PORT=22 # change this to whatever your Synology SSH daemon listens to
  - START_SSH_SERVER=false # Gitea's own SSH server not needed

The first two env variables do not have any effect other than to show up in the UI as clone URL. The second disables the built-in SSH server.

The other part of the Gitea config lives in the app.ini file:

[server]
SSH_CREATE_AUTHORIZED_KEYS_FILE = true
SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE = sudo /usr/local/bin/gitea-serv key-{{.Key.ID}}

Now it is time to restart (or in case of docker-compose redeploy) Gitea:

cd {location of your docker-compose.yaml} && docker compose restart

Populate authorized_keys

Whenever a key is removed or added in the Gitea webinterface the authorized_keys will be regenerated. But the regeneration can also be invoked from the CLI:

docker exec -u git gitea /app/gitea/gitea admin regenerate keys --config /data/gitea/conf/app.ini

Verify the file was written correctly:

docker exec gitea cat /data/git/.ssh/authorized_keys | head -3

Expected format should be:

# gitea public key
command="sudo /usr/local/bin/gitea-serv key-1",no-port-forwarding,...,restrict ssh-ed25519 AAAA... user-1

Test the connection

ssh -T git@${your-server}

Expected output:

Hi there, jan! You've successfully authenticated with the key named ${your-key-name},
but Gitea does not provide shell access.

And your regular users should also still be able to connect:

ssh {non-git-user}@{your-server}

I am so happy works! I now can ssh into the machine using any other user and the HTTPS/SSH clone URLs finally work as expected. Didn’t have to do weird redirects, the git user and the sudo part is restricted to this one functionality. And Gitea is now also part of Synology’s fail2ban mechanism.

Troubleshooting

These are the steps I took to troubleshoot issues with the setup.

Permission denied on connect

Check what the sshd logs say:

sudo journalctl -u sshd.service --since "5 minutes ago"

bad ACL permission

Try removing the Extended ACLs again: synoacltool -del /var/services/homes/git

Could not open authorized keys”

Check ownership: sudo chown -R 1000 /var/services/homes/git/.ssh

Key not found

Regenerate keys: docker exec -u git gitea /app/gitea/gitea admin regenerate keys --config /data/gitea/conf/app.ini

Git push/pull shows Gitea welcome message instead of working

SSH_ORIGINAL_COMMAND is not being passed. Verify sudoers contains:

Defaults>root env_keep+=SSH_ORIGINAL_COMMAND

Setup broken after DSM update

Re-do the steps in Preparing the git user account settings

Wrapper script: docker not found

Verify the Docker binary path:

readlink -f /usr/local/bin/docker

Update the path in /usr/local/bin/gitea-serv accordingly. On Synology with Container Manager it is typically:

/volume1/@appstore/ContainerManager/usr/bin/docker