--- # The host half of Gitea's git-over-SSH support. # # Port 22 belongs to the host's sshd and stays that way: clients keep using # git@git.tanshu.com:user/repo.git, and no remote URL changes. Four host-side # pieces make that work: # # 1. a `{{ git_user }}` login user with no interactive access — every key # Gitea accepts carries a forced command, so sshd runs the shim and # nothing else; # 2. {{ shim_path }}, the shim those forced commands invoke; # 3. {{ podman_exec_wrapper }} plus a one-command sudoers rule — how the shim, # running as `{{ git_user }}`, reaches a container owned by # {{ podman_user }}; # 4. the sshd and sudoers drop-ins that tie the above together. # # What differs from the Docker-era role is the second hop. It used to be an ssh # into an sshd running *inside* the container; under keep-id that sshd runs as a # subuid and cannot read the authorized_keys Gitea writes as {{ podman_user }}, # so the hop is a `podman exec` instead. readme.md, "How git over SSH works # now", is where the reasoning lives. # # Every task here is root-only (users, /usr/local/bin, /etc/ssh, /etc/sudoers), # so each elevates on its own. The play carries no become, because the Quadlet # units have to belong to the connection user. - name: Ensure the git group exists ansible.builtin.group: name: "{{ git_user }}" become: true # The shell matters even though nobody ever gets one: sshd runs a forced command # through the login user's shell (`/bin/bash -c '...'`), so a nologin shell would # break git-over-SSH outright. # # The uid is deliberately left to the system. On monoco the host git account was # uid 1003 because the container's git account was 1003 and the two shared one # uid space; under keep-id the container's 1003 is mapped onto {{ podman_user }} # instead, so nothing on this host depends on this number any more — not the # key file (owned by name), not the sudoers rule (matched by name), not the # authorized_keys hook (run as {{ podman_user }}). - name: Add the git login user ansible.builtin.user: name: "{{ git_user }}" comment: Gitea SSH login user group: "{{ git_user }}" home: "{{ git_home }}" shell: /bin/bash create_home: true become: true # There is deliberately no ~/.ssh for this user any more. Its old contents were # the shim's client key and the authorized_keys Gitea maintains; the key is # obsolete (nothing authenticates over ssh in this design) and the file lives in # the container's data tree now, where Gitea writes it. The migration notes in # readme.md move the file into place. - name: Install the git-over-SSH passthrough shim ansible.builtin.template: src: gitea.j2 dest: "{{ shim_path }}" owner: root group: root mode: '0755' become: true - name: Install the container exec wrapper ansible.builtin.template: src: gitea-container-exec.j2 dest: "{{ podman_exec_wrapper }}" owner: root group: root mode: '0755' become: true - name: Install the authorized_keys hook sshd runs for the git user ansible.builtin.template: src: gitea-authorized-keys.j2 dest: "{{ authorized_keys_command }}" owner: root group: root mode: '0755' become: true # valid with `visudo -cf %s`: a syntax error in /etc/sudoers.d does not fail one # rule, it fails *every* sudo invocation on the host, which on a server reached # by ssh is a nasty way to find out. - name: Let the git user reach the container as {{ podman_user }} ansible.builtin.template: src: sudoers-gitea.j2 dest: "{{ sudoers_path }}" owner: root group: root mode: '0440' validate: /usr/sbin/visudo -cf %s become: true - name: Point sshd at Gitea's authorized_keys block: - name: Install the sshd drop-in for the git user ansible.builtin.template: src: sshd-gitea.conf.j2 dest: "{{ sshd_dropin_path }}" owner: root group: root mode: '0644' register: gitea_sshd_dropin become: true # A drop-in that does not parse is not a failed play, it is a host nobody can # log in to after the next reboot: sshd reads the whole directory at start-up. # Validate the resulting configuration before it is ever loaded, and take the # file away again if it does not parse. - name: Validate the resulting sshd configuration ansible.builtin.command: /usr/sbin/sshd -t changed_when: false become: true when: gitea_sshd_dropin.changed rescue: - name: Take the sshd drop-in back out ansible.builtin.file: path: "{{ sshd_dropin_path }}" state: absent become: true - name: Fail ansible.builtin.fail: msg: >- {{ sshd_dropin_path }} did not validate, so it has been removed and sshd is still running — and still restarting — on its previous configuration. Run `/usr/sbin/sshd -t` on the host to see why. # `reload`, not `restart`: a reload re-execs sshd without dropping the connection # this play is running over. - name: Reload sshd ansible.builtin.systemd_service: name: "{{ sshd_service }}" state: reloaded become: true when: gitea_sshd_dropin.changed