Migrate Gitea to rootless Podman / SELinux

- Front git-over-SSH with host sshd and live key lookup
- Build and load custom SELinux policies oci_log and gitea_ssh
This commit is contained in:
2026-09-01 01:42:33 -04:00
parent 601a889172
commit a1ef6e76a5
22 changed files with 463 additions and 129 deletions
+11
View File
@@ -8,6 +8,17 @@
name: fail2ban
state: present
- name: Ensure Fail2ban's log file is correctly labelled
ansible.builtin.file:
path: /var/log/fail2ban.log
state: touch
mode: "0640"
access_time: preserve
modification_time: preserve
setype: fail2ban_log_t
when: selinux is defined and selinux is not false
notify: restart_fail2ban
- name: Deny incoming traffic by default
community.general.ufw:
default: deny
+8 -1
View File
@@ -16,6 +16,13 @@
policy: "{{ selinux.policy | default('default') }}"
when: selinux is defined and selinux is not false
- name: Set SELinux permissive domains
community.general.selinux_permissive:
domain: "{{ item }}"
permissive: true
loop: "{{ selinux.permissive_domains | default([]) }}"
when: selinux is defined and selinux is not false
- name: Check for GRUB
ansible.builtin.stat:
path: /etc/default/grub
@@ -100,7 +107,7 @@
community.crypto.openssh_keypair:
path: "{{ base_scripts }}/.keys/id_ed25519"
type: ed25519
comment: "{{ ansible_hostname }}-deploy-key"
comment: "{{ ansible_facts['hostname'] }}-deploy-key"
mode: "400"
state: present
when: scripts is defined
+1 -1
View File
@@ -1,7 +1,7 @@
[global]
workgroup = WORKGROUP
server string = Samba Server %v
netbios name = {{ ansible_hostname }}
netbios name = {{ ansible_facts['hostname'] }}
security = user
map to guest = bad user
dns proxy = no
+1 -1
View File
@@ -1,6 +1,6 @@
# {{ ansible_managed }}
[Interface] # {{ ansible_hostname }}
[Interface] # {{ ansible_facts['hostname'] }}
PrivateKey = {{ wgkey['content'] | b64decode | trim }}
Address = {{ wireguard.address }}
{% if wireguard.listenport is defined %}
+1 -1
View File
@@ -77,7 +77,7 @@
community.crypto.openssh_keypair:
path: "{{ docker_repos_keys }}/id_{{ docker_repos_keytype }}"
type: "{{ docker_repos_keytype }}"
comment: "{{ ansible_hostname }}-deploy-key"
comment: "{{ ansible_facts['hostname'] }}-deploy-key"
mode: "400"
state: present
when: docker_compose_deploy is defined
+8 -3
View File
@@ -1,6 +1,6 @@
# container settings
gitea_name: gitea
gitea_sshport: "222"
gitea_sshport: "2222"
gitea_webport: "3000"
gitea_ssh: "127.0.0.1:{{ gitea_sshport }}"
gitea_web: "127.0.0.1:{{ gitea_webport }}"
@@ -16,7 +16,12 @@ gitea_dbuser: "{{ gitea_name }}"
# proxy settings
gitea_proxy_limit: "1"
gitea_trusted_proxies: "172.16.0.0/12"
gitea_trusted_proxies: "10.89.0.0/16"
# host
gitea_root: "{{ docker_compose_root }}/{{ gitea_name }}"
gitea_root: "{{ podman_compose.git.root }}/{{ gitea_name }}"
gitea_logs: "{{ podman_log_root }}/{{ gitea_name }}"
gitea_data: /srv/gitea
gitea_se_level: "s0:c22,c222"
gitea_uid: 1000
gitea_oci_uid: "{{ podman_subuid_base['git'] | int + gitea_uid | int - 1 }}"
+9
View File
@@ -0,0 +1,9 @@
module gitea_ssh 1.0;
require {
type sshd_t;
type ssh_exec_t;
class file { getattr read open map execute execute_no_trans };
}
allow sshd_t ssh_exec_t:file { getattr read open map execute execute_no_trans };
+5 -5
View File
@@ -1,5 +1,5 @@
- name: Restart Gitea
ansible.builtin.service:
name: "{{ docker_compose_service }}@{{ gitea_name }}"
state: restarted
listen: restart_gitea
- name: Reload ssh
ansible.builtin.systemd_service:
name: ssh
state: reloaded
listen: restart_ssh
+159 -22
View File
@@ -13,7 +13,7 @@
community.mysql.mysql_user:
name: "{{ gitea.DB_USER }}"
password: "{{ gitea.DB_PASSWD }}"
host: '%'
host: "%"
state: present
priv: "{{ gitea.DB_NAME }}.*:ALL"
login_unix_socket: /var/run/mysqld/mysqld.sock
@@ -21,35 +21,36 @@
- name: Create git's .ssh directory
ansible.builtin.file:
path: /home/git/.ssh
owner: git
group: git
mode: "700"
state: directory
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('ssh_home_t', omit) }}
- name: Generate git's SSH keys
community.crypto.openssh_keypair:
path: /home/git/.ssh/id_rsa
owner: git
group: git
mode: "600"
register: gitea_keypair
- name: Label git's SSH keys for sshd access
ansible.builtin.file:
path: "/home/git/.ssh/{{ item }}"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('ssh_home_t', omit) }}
loop:
- id_rsa
- id_rsa.pub
- name: Find git's public SSH key
ansible.builtin.slurp:
src: /home/git/.ssh/id_rsa.pub
register: git_rsapub
- name: Get stats on git's authorized_keys file
ansible.builtin.stat:
path: /home/git/.ssh/authorized_keys
register: git_authkeys
- name: Create git's authorized_keys file
ansible.builtin.file:
path: /home/git/.ssh/authorized_keys
mode: "600"
state: touch
when: not git_authkeys.stat.exists
- name: Add git's public SSH key to authorized_keys
ansible.builtin.lineinfile:
path: /home/git/.ssh/authorized_keys
regex: "^ssh-rsa"
line: "{{ git_rsapub['content'] | b64decode }}"
register: gitea_rsapub
- name: Create Gitea host script for SSH
ansible.builtin.template:
@@ -57,11 +58,147 @@
dest: /usr/local/bin/gitea
mode: "755"
- name: Install Gitea SSH SELinux policy source
ansible.builtin.copy:
src: gitea_ssh.te
dest: "{{ podman_selinux_dir }}/gitea_ssh.te"
owner: root
group: root
mode: "0644"
register: gitea_selinux_src
when: selinux is defined and selinux is not false
- name: Clear stale Gitea SSH SELinux artifacts
ansible.builtin.file:
path: "{{ podman_selinux_dir }}/gitea_ssh.{{ item }}"
state: absent
loop: [mod, pp]
when:
- selinux is defined and selinux is not false
- gitea_selinux_src is changed
- name: Compile Gitea SSH SELinux policy module
ansible.builtin.command:
cmd: >-
checkmodule -M -m -o {{ podman_selinux_dir }}/gitea_ssh.mod
{{ podman_selinux_dir }}/gitea_ssh.te
creates: "{{ podman_selinux_dir }}/gitea_ssh.mod"
when: selinux is defined and selinux is not false
- name: Package Gitea SSH SELinux policy module
ansible.builtin.command:
cmd: >-
semodule_package -o {{ podman_selinux_dir }}/gitea_ssh.pp
-m {{ podman_selinux_dir }}/gitea_ssh.mod
creates: "{{ podman_selinux_dir }}/gitea_ssh.pp"
when: selinux is defined and selinux is not false
- name: List loaded SELinux policy modules for Gitea
ansible.builtin.command:
cmd: semodule -l
register: gitea_semodule_list
changed_when: false
when: selinux is defined and selinux is not false
- name: Load Gitea SSH SELinux policy module
ansible.builtin.command:
cmd: semodule -i {{ podman_selinux_dir }}/gitea_ssh.pp
register: gitea_semodule
changed_when: gitea_semodule.rc == 0
when:
- selinux is defined and selinux is not false
- gitea_selinux_src is changed or "gitea_ssh" not in gitea_semodule_list.stdout_lines
- name: Configure sshd for Gitea AuthorizedKeysCommand
ansible.builtin.template:
src: gitea_sshd.conf.j2
dest: /etc/ssh/sshd_config.d/gitea.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: restart_ssh
- name: Set SELinux context on Gitea's data directory
community.general.sefcontext:
target: "{{ gitea_data }}(/.*)?"
setype: container_file_t
selevel: "{{ gitea_se_level }}"
state: present
when: selinux is defined and selinux is not false
- name: Create Gitea's data directory
ansible.builtin.file:
path: "{{ gitea_data }}"
state: directory
owner: "{{ gitea_oci_uid }}"
group: "{{ gitea_oci_uid }}"
mode: "0750"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('container_file_t', omit) }}
selevel: "{{ gitea_se_level }}"
- name: Create Gitea's container-side SSH directories
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ gitea_oci_uid }}"
group: "{{ gitea_oci_uid }}"
mode: "{{ item.mode }}"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('container_file_t', omit) }}
selevel: "{{ gitea_se_level }}"
loop:
- { path: "{{ gitea_data }}/git", mode: "0755" }
- { path: "{{ gitea_data }}/git/.ssh", mode: "0700" }
- name: Authorise git's public SSH key inside the container
ansible.builtin.copy:
content: "{{ gitea_rsapub['content'] | b64decode }}"
dest: "{{ gitea_data }}/git/.ssh/authorized_keys"
owner: "{{ gitea_oci_uid }}"
group: "{{ gitea_oci_uid }}"
mode: "0600"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('container_file_t', omit) }}
selevel: "{{ gitea_se_level }}"
- name: Set SELinux context on Gitea's logging directory
community.general.sefcontext:
target: "{{ gitea_logs }}(/.*)?"
setype: oci_log_t
selevel: "{{ gitea_se_level }}"
state: present
when: selinux is defined and selinux is not false
- name: Create Gitea's logging directory
ansible.builtin.file:
name: /var/log/gitea
name: "{{ gitea_logs }}"
state: directory
mode: "755"
owner: "{{ gitea_oci_uid }}"
group: "{{ gitea_oci_uid }}"
mode: "0755"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('oci_log_t', omit) }}
selevel: "{{ gitea_se_level }}"
- name: Create Gitea's log file for Fail2ban
ansible.builtin.file:
path: /var/log/oci/gitea/gitea.log
state: touch
owner: "{{ gitea_oci_uid }}"
group: "{{ gitea_oci_uid }}"
mode: "0640"
modification_time: preserve
access_time: preserve
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('oci_log_t', omit) }}
selevel: "{{ gitea_se_level }}"
- name: Install Gitea's Fail2ban filter
ansible.builtin.template:
-19
View File
@@ -1,19 +0,0 @@
# {{ ansible_managed }}
gitea_version={{ gitea_version }}
gitea_name={{ gitea_name }}
gitea_domain={{ gitea_domain }}
gitea_rooturl={{ gitea_rooturl }}
gitea_web={{ gitea_web }}
gitea_ssh={{ gitea_ssh }}
gitea_dbtype={{ gitea_dbtype }}
gitea_dbhost={{ gitea_dbhost }}
gitea_dbname={{ gitea_dbname }}
gitea_dbuser={{ gitea_dbuser }}
gitea_dbpass={{ gitea_dbpass }}
gitea_proxy_limit={{ gitea_proxy_limit }}
gitea_trusted_proxies={{ gitea_trusted_proxies }}
{% if not gitea_signup %}
gitea_disable_registration=true
{% else %}
gitea_disable_registration=false
{% endif %}
@@ -1,36 +0,0 @@
version: '3.7'
services:
gitea:
image: "gitea/gitea:${gitea_version}"
container_name: "${gitea_name}"
ports:
- "${gitea_ssh}:22"
- "${gitea_web}:3000"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- USER_UID={{ getent_passwd.git[1] }}
- USER_GID={{ getent_group.git[1] }}
- GITEA__log__MODE=file
- GITEA__server__ROOT_URL=${gitea_rooturl}
- GITEA__server__DOMAIN=${gitea_domain}
- GITEA__server__SSH_DOMAIN=${gitea_domain}
- GITEA__database__DB_TYPE=${gitea_dbtype}
- GITEA__database__HOST=${gitea_dbhost}
- GITEA__database__NAME=${gitea_dbname}
- GITEA__database__USER=${gitea_dbuser}
- GITEA__database__PASSWD=${gitea_dbpass}
- GITEA__security__INSTALL_LOCK=true
- GITEA__security__REVERSE_PROXY_LIMIT=${gitea_proxy_limit}
- GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES=${gitea_trusted_proxies}
- GITEA__service__DISABLE_REGISTRATION=${gitea_disable_registration}
volumes:
- {{ gitea_volume }}:/data
- /home/git/.ssh:/data/git/.ssh
- /var/log/gitea:/data/gitea/log
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
volumes:
{{ gitea_volume }}:
+1 -10
View File
@@ -2,17 +2,8 @@
[gitea]
enabled = true
filter = gitea
logpath = /var/log/gitea/gitea.log
logpath = {{ gitea_logs }}/gitea.log
maxretry = 10
findtime = 3600
bantime = 900
action = iptables-allports
[gitea-docker]
enabled = true
filter = gitea
logpath = /var/log/gitea/gitea.log
maxretry = 10
findtime = 3600
bantime = 900
action = iptables-allports[chain="FORWARD"]
+3
View File
@@ -0,0 +1,3 @@
# {{ ansible_managed }}
AuthorizedKeysCommand /usr/local/bin/gitea keys -e git -u %u -t %t -k %k
AuthorizedKeysCommandUser git
+13 -3
View File
@@ -16,12 +16,22 @@
regex: "^bind-address"
line: "bind-address = {{ ansible_facts.docker0.ipv4.address }}"
notify: restart_mariadb
when: ansible_facts.docker0 is defined
when: ansible_facts['docker0'] is defined
- name: Change the bind-address for rootless containers
ansible.builtin.lineinfile:
path: /etc/mysql/mariadb.conf.d/50-server.cnf
regexp: "^bind-address"
line: "bind-address = 0.0.0.0"
notify: restart_mariadb
when:
- ansible_facts['docker0'] is not defined
- podman_compose is defined
- name: Flush handlers to ensure MariaDB restarts immediately
ansible.builtin.meta: flush_handlers
tags: restart_mariadb
when: ansible_facts.docker0 is defined
when: ansible_facts['docker0'] is defined or podman_compose is defined
- name: Allow database connections from Docker
community.general.ufw:
@@ -29,4 +39,4 @@
port: "3306"
proto: tcp
src: "{{ item }}"
loop: "{{ mariadb_trust | default(['172.16.0.0/12']) }}"
loop: "{{ mariadb_trust | default(['169.254.0.0/16']) }}"
+4 -2
View File
@@ -1,4 +1,6 @@
# Default configuration for podman role
podman_repos_keytype: ed25519
podman_ssh_key_path: "{{ ansible_user_dir }}/.ssh"
podman_log_root: /var/log/oci
podman_nodocker: false
podman_repos_keytype: ed25519
podman_selinux_dir: /usr/share/selinux
podman_ssh_key_path: "{{ ansible_user_dir }}/.ssh"
+19
View File
@@ -0,0 +1,19 @@
module oci_log 1.0;
require {
type container_t;
type fail2ban_t;
attribute logfile;
attribute file_type;
attribute non_security_file_type;
class dir { add_name search watch write };
class file { append create getattr open watch };
}
type oci_log_t;
typeattribute oci_log_t file_type, non_security_file_type, logfile;
allow container_t oci_log_t:dir { add_name search write };
allow container_t oci_log_t:file { append create getattr open };
allow fail2ban_t oci_log_t:dir watch;
allow fail2ban_t oci_log_t:file watch;
+1 -1
View File
@@ -71,7 +71,7 @@
community.crypto.openssh_keypair:
path: "{{ podman_homedir }}/.ssh/podman-id_{{ podman_repos_keytype }}"
type: "{{ podman_repos_keytype }}"
comment: "{{ ansible_hostname }}-{{ podman_user }}-deploy-key"
comment: "{{ ansible_facts['hostname'] }}-{{ podman_user }}-deploy-key"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: "0600"
+130 -13
View File
@@ -3,6 +3,91 @@
name: ["podman", "docker-cli", "docker-compose"]
state: present
- name: Allow rootless containers to use capabilities in their user namespace
ansible.posix.seboolean:
name: container_use_userns_all_caps
state: true
persistent: true
when: selinux is defined and selinux is not false
- name: Create SELinux policy build directory
ansible.builtin.file:
path: "{{ podman_selinux_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: selinux is defined and selinux is not false
- name: Install container log SELinux policy source
ansible.builtin.copy:
src: oci_log.te
dest: "{{ podman_selinux_dir }}/oci_log.te"
owner: root
group: root
mode: "0644"
register: podman_selinux_src
when: selinux is defined and selinux is not false
- name: Clear stale container log SELinux artifacts
ansible.builtin.file:
path: "{{ podman_selinux_dir }}/oci_log.{{ item }}"
state: absent
loop: [mod, pp]
when:
- selinux is defined and selinux is not false
- podman_selinux_src is changed
- name: Compile container log SELinux policy module
ansible.builtin.command:
cmd: >-
checkmodule -M -m -o {{ podman_selinux_dir }}/oci_log.mod
{{ podman_selinux_dir }}/oci_log.te
creates: "{{ podman_selinux_dir }}/oci_log.mod"
when: selinux is defined and selinux is not false
- name: Package container log SELinux policy module
ansible.builtin.command:
cmd: >-
semodule_package -o {{ podman_selinux_dir }}/oci_log.pp
-m {{ podman_selinux_dir }}/oci_log.mod
creates: "{{ podman_selinux_dir }}/oci_log.pp"
when: selinux is defined and selinux is not false
- name: List loaded SELinux policy modules
ansible.builtin.command:
cmd: semodule -l
register: podman_semodule_list
changed_when: false
when: selinux is defined and selinux is not false
- name: Load container log SELinux policy module
ansible.builtin.command:
cmd: semodule -i {{ podman_selinux_dir }}/oci_log.pp
register: podman_semodule
changed_when: podman_semodule.rc == 0
when:
- selinux is defined and selinux is not false
- podman_selinux_src is changed or "oci_log" not in podman_semodule_list.stdout_lines
- name: Set SELinux context on container log directory
community.general.sefcontext:
target: "{{ podman_log_root }}(/.*)?"
setype: oci_log_t
state: present
when: selinux is defined and selinux is not false
- name: Create container log directory
ansible.builtin.file:
path: "{{ podman_log_root }}"
state: directory
owner: root
group: root
mode: "0755"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('oci_log_t', omit) }}
- name: Install GnuPG tools and trusted CA bundle
ansible.builtin.apt:
name: ["gnupg", "ca-certificates"]
@@ -19,13 +104,19 @@
label: "{{ item }}"
when: podman_compose is defined
- name: Set subuid base facts for podman users
ansible.builtin.set_fact:
podman_subuid_base: "{{ podman_subuid_base | default({}) | combine({
item.item: 100000 + ((item.ansible_facts.getent_passwd[item.item][1]
| int - 1000) * 65536) }) }}"
loop: "{{ user_info.results }}"
loop_control:
label: "{{ item.item }}"
- name: Configure /etc/subuid for rootless users
ansible.builtin.lineinfile:
path: "/etc/subuid"
line:
"{{ item.item }}:{{ 100000 +
((item.ansible_facts.getent_passwd[item.item][1] | int - 1000) * 65536)
}}:65536"
line: "{{ item.item }}:{{ podman_subuid_base[item.item] }}:65536"
regexp: "^{{ item.item }}:"
create: true
backup: true
@@ -37,10 +128,7 @@
- name: Configure /etc/subgid for rootless users
ansible.builtin.lineinfile:
path: "/etc/subgid"
line:
"{{ item.item }}:{{ 100000 +
((item.ansible_facts.getent_passwd[item.item][1] | int - 1000) * 65536)
}}:65536"
line: "{{ item.item }}:{{ podman_subuid_base[item.item] }}:65536"
regexp: "^{{ item.item }}:"
create: true
backup: true
@@ -49,10 +137,39 @@
loop_control:
label: "{{ item.item }}"
- name: Enable lingering for podman compose user
ansible.builtin.command:
cmd: "loginctl enable-linger {{ item.item }}"
changed_when: false
- name: Ensure systemd linger directory exists
ansible.builtin.file:
path: /var/lib/systemd/linger
state: directory
owner: root
group: root
mode: "0755"
- name: Enable lingering for podman compose users
ansible.builtin.file:
path: "/var/lib/systemd/linger/{{ item.item }}"
state: touch
owner: root
group: root
mode: "0644"
access_time: preserve
modification_time: preserve
loop: "{{ user_info.results }}"
loop_control:
label: "{{ item.item }}"
- name: Start user manager for podman compose users
ansible.builtin.systemd_service:
name: "user@{{ item.ansible_facts.getent_passwd[item.item][1] }}.service"
state: started
loop: "{{ user_info.results }}"
loop_control:
label: "{{ item.item }}"
- name: Wait for user runtime directory
ansible.builtin.wait_for:
path: "/run/user/{{ item.ansible_facts.getent_passwd[item.item][1] }}/bus"
timeout: 30
loop: "{{ user_info.results }}"
loop_control:
label: "{{ item.item }}"
@@ -97,7 +214,7 @@
dest: /etc/profile.d/docker-host.sh
owner: root
group: root
mode: '0755'
mode: "0755"
- name: Install git for repository cloning
ansible.builtin.apt:
-10
View File
@@ -1,13 +1,3 @@
- name: Enable nginx sites configuration
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ item.item.domain }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.item.domain }}.conf"
state: link
mode: "400"
loop: "{{ nginx_sites.results }}"
when: item.changed
listen: reload_nginx
- name: Reload nginx
ansible.builtin.service:
name: nginx
+10 -1
View File
@@ -43,7 +43,16 @@
loop_control:
label: "{{ item.domain }}"
notify: reload_nginx
register: nginx_sites
- name: Enable nginx sites configuration
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ item.domain }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.domain }}.conf"
state: "{{ item.enabled | default(true) | ternary('link', 'absent') }}"
loop: "{{ proxy.servers }}"
loop_control:
label: "{{ item.domain }}"
notify: reload_nginx
- name: Generate self-signed certificate
ansible.builtin.command: