diff options
Diffstat (limited to 'roles/cgit')
| -rw-r--r-- | roles/cgit/handlers/main.yml | 5 | ||||
| -rw-r--r-- | roles/cgit/tasks/main.yml | 124 | ||||
| -rw-r--r-- | roles/cgit/templates/cgitrc.j2 | 11 | ||||
| -rw-r--r-- | roles/cgit/templates/site-cgit.conf.j2 | 60 |
4 files changed, 196 insertions, 4 deletions
diff --git a/roles/cgit/handlers/main.yml b/roles/cgit/handlers/main.yml new file mode 100644 index 0000000..e4af55c --- /dev/null +++ b/roles/cgit/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart fcgiwrap + ansible.builtin.systemd: + name: fcgiwrap.service + state: restarted diff --git a/roles/cgit/tasks/main.yml b/roles/cgit/tasks/main.yml index 44f9459..f454d93 100644 --- a/roles/cgit/tasks/main.yml +++ b/roles/cgit/tasks/main.yml @@ -4,17 +4,125 @@ name: - cgit - fcgiwrap - - python3-pygments + - git + - python3-docutils - python3-markdown + - python3-pygments + state: present + +- name: Ensure dedicated git system group exists + ansible.builtin.group: + name: "{{ cgit_group }}" + system: true + state: present + +- name: Ensure dedicated git system user exists + ansible.builtin.user: + name: "{{ cgit_user }}" + group: "{{ cgit_group }}" + home: "{{ cgit_repos_path }}" + shell: /usr/bin/git-shell + comment: "Git version control user" + system: true + create_home: true state: present -- name: Ensure Git repositories root directory exists +- name: Add www-data to git group so Nginx/fcgiwrap can read repositories + ansible.builtin.user: + name: www-data + groups: "{{ cgit_group }}" + append: true + notify: Restart fcgiwrap + +- name: Ensure Git repositories root directory permissions (setgid for group inheritance) ansible.builtin.file: path: "{{ cgit_repos_path }}" state: directory + owner: "{{ cgit_user }}" + group: "{{ cgit_group }}" + mode: "02750" + +- name: Configure git user safe.directory for cgit (www-data) + ansible.builtin.copy: + dest: /var/www/.gitconfig owner: www-data group: www-data + mode: "0644" + content: | + [safe] + directory = * + +- name: Ensure git-shell-commands directory exists (disables interactive shell login and provides helpers) + ansible.builtin.file: + path: "{{ cgit_repos_path }}/git-shell-commands" + state: directory + owner: root + group: "{{ cgit_group }}" + mode: "0755" + +- name: Deploy git-shell helper commands (no-interactive-login, init, list) + ansible.builtin.copy: + dest: "{{ cgit_repos_path }}/git-shell-commands/{{ item.name }}" + owner: root + group: "{{ cgit_group }}" mode: "0755" + content: "{{ item.content }}" + loop: + - name: no-interactive-login + content: | + #!/bin/sh + printf '%s\n' "Hi! You've successfully authenticated as '{{ cgit_user }}', but interactive shell access is disabled." + printf '%s\n' "Tip: Run 'ssh {{ cgit_user }}@{{ primary_domain }} init <repo>.git [description]' to create a new repository." + exit 128 + - name: list + content: | + #!/bin/sh + find "{{ cgit_repos_path }}" -maxdepth 2 -name "*.git" -type d -printf "%P\n" | sort + - name: init + content: | + #!/bin/sh + set -e + REPO="$1" + DESC="$2" + if [ -z "$REPO" ]; then + echo "Usage: ssh {{ cgit_user }}@{{ primary_domain }} init <repo-name>.git [description]" + exit 1 + fi + case "$REPO" in + *.git) ;; + *) REPO="${REPO}.git" ;; + esac + TARGET="{{ cgit_repos_path }}/${REPO}" + if [ -d "$TARGET" ]; then + echo "Error: Repository ${REPO} already exists." + exit 1 + fi + git init --bare --shared=group "$TARGET" + if [ -n "$DESC" ]; then + echo "$DESC" > "$TARGET/description" + else + echo "${REPO%.git}" > "$TARGET/description" + fi + echo "Initialized bare repository at {{ cgit_user }}@{{ primary_domain }}:${REPO}" + +- name: Ensure .ssh directory exists for git user + ansible.builtin.file: + path: "{{ cgit_repos_path }}/.ssh" + state: directory + owner: "{{ cgit_user }}" + group: "{{ cgit_group }}" + mode: "0700" + +- name: Configure authorized SSH keys for git user + ansible.builtin.copy: + dest: "{{ cgit_repos_path }}/.ssh/authorized_keys" + owner: "{{ cgit_user }}" + group: "{{ cgit_group }}" + mode: "0600" + content: | + {% for key in cgit_ssh_authorized_keys %} + {{ key }} + {% endfor %} - name: Deploy /etc/cgitrc configuration ansible.builtin.template: @@ -24,11 +132,19 @@ group: root mode: "0644" -- name: Ensure fcgiwrap socket/service is enabled and running +- name: Ensure fcgiwrap socket and service are enabled and running ansible.builtin.systemd: - name: fcgiwrap.socket + name: "{{ item }}" enabled: true state: started + loop: + - fcgiwrap.socket + - fcgiwrap.service + +- name: Check if TLS certificate exists for {{ cgit_domain }} + ansible.builtin.stat: + path: "/etc/letsencrypt/live/{{ cgit_domain }}/fullchain.pem" + register: cgit_tls_cert - name: Deploy Nginx virtual host for cgit ansible.builtin.template: diff --git a/roles/cgit/templates/cgitrc.j2 b/roles/cgit/templates/cgitrc.j2 index 0cae2ab..6e9cbd0 100644 --- a/roles/cgit/templates/cgitrc.j2 +++ b/roles/cgit/templates/cgitrc.j2 @@ -7,11 +7,21 @@ root-title={{ cgit_title }} root-desc={{ cgit_description }} virtual-root=/ +{% if cgit_enable_http_clone | default(true) %} +enable-http-clone=1 +clone-url=https://{{ cgit_domain }}/$CGIT_REPO_URL git@{{ primary_domain }}:$CGIT_REPO_URL +{% else %} +enable-http-clone=0 +clone-url=git@{{ primary_domain }}:$CGIT_REPO_URL +{% endif %} + enable-index-links=1 enable-index-owner=0 enable-commit-graph=1 enable-log-filecount=1 enable-log-linecount=1 +enable-git-config=1 +remove-suffix=1 snapshots=tar.gz zip about-filter=/usr/lib/cgit/filters/about-formatting.sh @@ -23,4 +33,5 @@ readme=:README.rst readme=:README.txt readme=:README +# Scan path must be placed after global repository defaults scan-path={{ cgit_repos_path }} diff --git a/roles/cgit/templates/site-cgit.conf.j2 b/roles/cgit/templates/site-cgit.conf.j2 index 557c364..36753d3 100644 --- a/roles/cgit/templates/site-cgit.conf.j2 +++ b/roles/cgit/templates/site-cgit.conf.j2 @@ -1,3 +1,55 @@ +{% if cgit_tls_cert.stat.exists | default(false) %} +server { + server_name {{ cgit_domain }}; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + root /usr/share/cgit; + try_files $uri @cgit; + + # Serve static cgit assets directly + location ~* ^/cgit-css/(.*)$ { + alias /usr/share/cgit/$1; + expires 30d; + } + + # Block git-Receive-pack over HTTP (pushing is strictly via SSH as git@) + location ~ ^/.*/git-receive-pack$ { + return 403; + } + + location @cgit { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi; + fastcgi_param PATH_INFO $uri; + fastcgi_param QUERY_STRING $args; + fastcgi_param HTTP_HOST $server_name; + fastcgi_pass unix:/run/fcgiwrap.socket; + } + + listen 443 ssl; # managed by Certbot / Ansible + listen [::]:443 ssl; # managed by Certbot / Ansible + ssl_certificate /etc/letsencrypt/live/{{ cgit_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ cgit_domain }}/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +} + +server { + if ($host = {{ cgit_domain }}) { + return 301 https://$host$request_uri; + } + + listen 80; + listen [::]:80; + server_name {{ cgit_domain }}; + return 404; +} +{% else %} server { listen 80; listen [::]:80; @@ -6,8 +58,15 @@ server { root /usr/share/cgit; try_files $uri @cgit; + # Serve static cgit assets directly location ~* ^/cgit-css/(.*)$ { alias /usr/share/cgit/$1; + expires 30d; + } + + # Block git-receive-pack over HTTP (pushing is strictly via SSH as git@) + location ~ ^/.*/git-receive-pack$ { + return 403; } location @cgit { @@ -19,3 +78,4 @@ server { fastcgi_pass unix:/run/fcgiwrap.socket; } } +{% endif %} |
