# Git over SSH (OpenGit)

**Overview of HTTP + SSH together (architecture, URLs, auth):** [git-transport-http-and-ssh.md](git-transport-http-and-ssh.md).

OpenGit can serve Git over **HTTPS** (smart HTTP, existing) and optionally over **SSH** using a small **AsyncSSH** process (`scripts/daemons/opengit_git_ssh.py`). SSH uses the **same access rules** as HTTP (`routes/git/git_transport_auth.py`): public repos readable by any authenticated SSH key holder; private repos only for owner/contributors; push only for owner/contributors.

## Requirements

- Python dependency: `asyncssh` (see `requirements.txt`).
- A registered **SSH public key** per user (User Settings → SSH keys).
- **Git over SSH** enabled in Admin → System Settings, plus **SSH listen port** and **SSH hostname** (for clone URLs in the UI).

## What the SSH service does

- Listens on `git_ssh_listen_port` (default **2222**).
- **Public-key auth only** (no passwords).
- Allows **only** `git-upload-pack` and `git-receive-pack` with a single argument **`Owner/repo.git`** or **`/Owner/repo.git`** (Git’s `ssh://` form sends a leading slash; both normalize to `Owner/repo`). No shell, no arbitrary commands.
- Runs `git upload-pack` / `git receive-pack` against the same on-disk layout as HTTP (`storage/git/<owner_user_id>/<repo>`).
- Injects `core.hooksPath` on **push**, matching `routes/git_http_backend.py`.

Host keys are stored on disk (not in the database), default directory:

`$OPENGIT_APP_DIR/data/git_ssh_host_keys/`

An Ed25519 host key is generated automatically on first start if missing. **Back up this directory.** Changing or deleting host keys will prompt clients to update `known_hosts`.

## systemd

1. Edit paths in `scripts/opengit-git-ssh.service` (`WorkingDirectory`, `User`, `ExecStart`, `EnvironmentFile`).
2. Install and start:

```bash
sudo cp /opt/OpenGit/scripts/opengit-git-ssh.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now opengit-git-ssh.service
sudo systemctl status opengit-git-ssh.service
```

If **Git over SSH** is disabled in admin settings, the process logs and **exits cleanly**; enable SSH in the UI, then start the unit again (or use `Restart=on-failure` and `systemctl restart` after enabling).

### `scripts/ensure_git_ssh_systemd.sh` (Linux)

**`update_local.sh`** and **`scripts/install_opengit.sh`** (full install) both call this helper. It:

1. If **`opengit-git-ssh.service` is already running** → **`sudo systemctl restart`** so updated `scripts/daemons/opengit_git_ssh.py` is loaded.
2. If a unit file **exists** but the service is **disabled** → **`sudo systemctl enable --now`**.
3. If a unit file **exists**, is **enabled**, but **stopped** → **`sudo systemctl start`**.
4. If **no unit file** → writes `/etc/systemd/system/opengit-git-ssh.service` for your install dir (venv Python + daemon script), then **`daemon-reload`** and **`enable --now`** (requires **sudo**).

Set **`OPENGIT_SKIP_SSH_SERVICE=1`** to skip (supported by both update and install). On **macOS** or without `systemctl`, the helper exits quietly.

Run manually: `bash scripts/ensure_git_ssh_systemd.sh ~/.local/share/OpenGit`

If the service **starts then immediately stops**, turn **Git over SSH** on in Admin first, then: `sudo systemctl restart opengit-git-ssh.service`.

### Upgrading an existing unit (path change)

If your unit still runs `…/scripts/opengit_git_ssh.py`, change **`ExecStart`** to **`…/scripts/daemons/opengit_git_ssh.py`**, then:

`sudo systemctl daemon-reload && sudo systemctl restart opengit-git-ssh.service`

## How to run the SSH service (two ways)

| Way | What it is | Good for |
|-----|------------|----------|
| **A. Foreground in a terminal** | You run `python3 scripts/daemons/opengit_git_ssh.py` yourself; that terminal stays open and prints log lines. | Quick debugging on your laptop or server. |
| **B. systemd** | `opengit-git-ssh.service` runs the same script in the background; logs go to the journal. | Production / always-on. |

They are the **same program**; you usually use **either** A or B, not both on the same port.

## Testing with `git clone` (and what “check the logs” means)

Rough flow:

1. Enable **Git over SSH** in Admin and set **port** / **SSH hostname**.
2. Add your **public key** in User Settings.
3. Start the service (**systemd** or **foreground**).
4. From another machine or terminal, run **`git clone`** using the SSH URL from the repo **Code** menu (for port ≠ 22, use the `ssh://git@host:port/...` form, or `GIT_SSH_COMMAND="ssh -p 2222 ..."`).

**5. “Check the logs”** — this only answers: *did the server see my connection and accept or reject it?*

- **If you used way A (foreground):** the **same terminal** where you started `scripts/daemons/opengit_git_ssh.py` prints lines like connections, “pubkey accepted”, or errors. **Watch that window** while you run `git clone` in another window.
- **If you used way B (systemd):** there is no foreground terminal; read the journal instead:

```bash
journalctl -u opengit-git-ssh.service -f
```

(`-f` follows new lines, like `tail -f`.)

So “step 5” is not a separate install step — it means **look at the SSH server’s output** (terminal or `journalctl`) while you test `git clone` to confirm auth and errors.

## Firewall and reverse proxy

- Open the configured **TCP port** (e.g. 2222) on your firewall/security group.
- To use **port 22**, either run the service as root with port 22 (not recommended) or use **iptables/nginx stream**/a cloud LB to forward `22 → 2222`.

## Client: `known_hosts`

On first connect, SSH will ask to trust the host key. To verify, compare the server’s public key:

```bash
ssh-keygen -lf /opt/OpenGit/data/git_ssh_host_keys/ssh_host_ed25519.pub
```

## Clone URLs

The repository **Code** menu shows HTTPS and/or SSH URLs based on:

- `git_transport_http_enabled` / `git_transport_ssh_enabled`
- `git_clone_url_preference`: `https`, `ssh`, or `both`
- `git_ssh_public_host` — leave empty so clone URLs use the same hostname the browser used (`Host` / `X-Forwarded-Host`, e.g. LAN IP or your DNS name). Set only if SSH must show a different host than the web UI.

Non-default SSH ports use the `ssh://git@host:port/owner/repo.git` form so Git does not require `~/.ssh/config`.

## HTTPS with personal access tokens

Disabling SSH does not affect **HTTPS** cloning with username + password/PAT; that remains the usual option when SSH is off or blocked.

## Environment variables

| Variable | Purpose |
|----------|---------|
| `OPENGIT_APP_DIR` | OpenGit install root (default: parent of `scripts/`, resolved from `scripts/daemons/…`) |
| `OPENGIT_STORAGE_DIR` | Same as app `storage` dir (default: `storage`) |
| `GIT_EXECUTABLE_PATH` | `git` binary (default: `/usr/bin/git`) |
| `OPENGIT_SSH_BIND` | Listen address (default: `0.0.0.0`) |
| `OPENGIT_SSH_HOST_KEY_DIR` | Override host key directory |

Load database URL / Postgres the same way as the web app via `.opengit.env`.

## Binary sessions (`encoding=None`)

The Git smart protocol over SSH is **binary**. OpenGit’s listener passes **`encoding=None`** into `asyncssh.create_server()` so stdin/stdout use **bytes**. If you fork the server and omit that, you may see `TypeError: utf_8_encode() argument 1 must be str, not bytes` when piping `git upload-pack` to the client.

## Push size limits (SSH vs HTTP)

HTTP applies a **pre-check** on pack size before invoking `git receive-pack`. SSH streams the pack to `git` directly, so that same pre-check is not applied; **hooks and storage policies** still run. For strict parity, prefer HTTP for very large pushes or extend the SSH path with a buffering check (future work).
