# Live Debugging: SSH into a Running GitLab CI or GitHub Action Job I've always like the idea of being able to ssh into a running job to debug CI-specific things like firewall rules. Don't forget to set up an SSH key in GitHub and GitLab in order to allow yourself to SSH into the job. **Security implications:** on GitHub, `limit-access-to-actor: true` makes sure only the pub keys of the person who triggered the workflow can SSH into the Action. On GitLab, `--gitlab-user=$GITLAB_USER_LOGIN` does the same thing. Note that the entire TCP traffic is going through [owenthereal/upterm](https://github.com/owenthereal/upterm)'s servers, but since it is being encrypted (SSH), so all good. You could improve the situation by running your own upterm server. ## Live Debugging in GitHub Actions In GitHub Actions, the solution is super simple. Just use the action [`owenthereal/action-upterm`](https://github.com/owenthereal/action-upterm): ```yaml name: CI jobs: build: runs-on: ubuntu-latest steps: - name: Setup upterm session uses: owenthereal/action-upterm@v1 with: limit-access-to-actor: true ``` ## Live Debugging in GitLab CI With GitLab CI, Actions don't exist and you have to craft your own. To make things worse, you can't find examples of people using `upterm` in their `.gitlab-ci.yml` files since GitLab code search isn't available on the public gitlab.com instance... (one of the many reasons one should prefer GitHub over GitLab). To mimick the GitHub action, you can use one of these snippets: > [!NOTE] > > **28 Nov, 2025:** I have updated this script to fix [a breaking change](https://github.com/owenthereal/upterm/issues/371#issuecomment-3588801715) introduced in upterm v0.18.0: the socket is no longer in `~/.upterm/`, it is now under `/run/user/$(id -u)/upterm`. ### Alpine ```bash # Alpine version. test: image: alpine:3.19 script: - apk add --no-cache curl tmux openssh-client - curl -sSfL https://github.com/owenthereal/upterm/releases/download/v0.19.0/upterm_linux_amd64.tar.gz | tar xz -C /usr/local/bin - mkdir -p ~/.ssh - ssh-keyscan uptermd.upterm.dev 2>/dev/null | awk '{ print "@cert-authority * " $2 " " $3 }' | tee /dev/stderr >> ~/.ssh/known_hosts - ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 - mkdir -p ~/.local/state/upterm && touch ~/.local/state/upterm/upterm.log && tail -F ~/.local/state/upterm/upterm.log & - tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --gitlab-user $GITLAB_USER_LOGIN --accept --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43" - tmux set -t upterm-wrapper window-size largest; tmux set -t upterm window-size largest - until upterm session current --admin-socket /run/user/$(id -u)/upterm/*.sock; do sleep 5; done - until ! ls /run/user/$(id -u)/upterm/ | grep -q '\.sock$'; do sleep 5; done ``` ### Debian and Ubuntu ```bash # Debian and Ubuntu version. test: image: ubuntu:latest script: - apt update && apt install -y tmux openssh-client - curl -sSfL https://github.com/owenthereal/upterm/releases/download/v0.19.0/upterm_linux_amd64.tar.gz | tar xz -C /usr/local/bin - mkdir -p ~/.ssh - ssh-keyscan uptermd.upterm.dev 2>/dev/null | awk '{ print "@cert-authority * " $2 " " $3 }' | tee /dev/stderr >> ~/.ssh/known_hosts - ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 - mkdir -p ~/.local/state/upterm && touch ~/.local/state/upterm/upterm.log && tail -F ~/.local/state/upterm/upterm.log & - tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --gitlab-user $GITLAB_USER_LOGIN --accept --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43" - tmux set -t upterm-wrapper window-size largest; tmux set -t upterm window-size largest - until upterm session current --admin-socket /run/user/$(id -u)/upterm/*.sock; do sleep 5; done - until ! ls /run/user/$(id -u)/upterm/ | grep -q '\.sock$'; do sleep 5; done ``` ### CentOS, REHL, and Red Hat UBI ```bash # CentOS, REHL, Red Hat UBI. test: image: registry.access.redhat.com/ubi9/ubi script: - yum install -y curl tmux openssh-clients tar gzip - curl -sSfL https://github.com/owenthereal/upterm/releases/download/v0.19.0/upterm_linux_amd64.tar.gz | tar xz -C /usr/local/bin - mkdir -p ~/.ssh - ssh-keyscan uptermd.upterm.dev 2>/dev/null | awk '{ print "@cert-authority * " $2 " " $3 }' | tee /dev/stderr >> ~/.ssh/known_hosts - ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 - mkdir -p ~/.local/state/upterm && touch ~/.local/state/upterm/upterm.log && tail -F ~/.local/state/upterm/upterm.log & - tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --gitlab-user $GITLAB_USER_LOGIN --accept --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43" - tmux set -t upterm-wrapper window-size largest; tmux set -t upterm window-size largest - until upterm session current --admin-socket /run/user/$(id -u)/upterm/*.sock; do sleep 5; done - until ! ls /run/user/$(id -u)/upterm/ | grep -q '\.sock$'; do sleep 5; done ``` ### Why `tmux`? I've learned that tmux is the only way; you can't do this: ```yaml test: image: alpine:3.19 script: - upterm host --force-command 'tmux attach -t upterm' --accept -- tmux new-session -d upterm ``` Not sure why... but when trying to connect, you would get "Connection closed": ```console $ ssh psxScav2LZnOJugeSE8A:ZTc4NGUxMWRiMjI5MDgudm0udXB0ZXJtLmludGVybmFsOjIyMjI=@uptermd.upterm.dev Connection closed by 37.16.25.99 port 22 ```