diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..d2dc43b --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,5 @@ +--- +warn_list: + - experimental +exclude_paths: + - .github/workflows/ diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 0000000..e69de29 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..aa96617 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-added-large-files + - id: check-json + - id: detect-private-key + - id: check-case-conflict + - id: requirements-txt-fixer + - id: check-ast + - id: check-shebang-scripts-are-executable + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-xml + # - id: detect-aws-credentials + - id: check-docstring-first + - repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: [-I, .codespellignore] + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] + # exclude: .*/tests/.* + - repo: https://github.com/aristanetworks/j2lint.git + rev: v1.1.0 + hooks: + - id: j2lint + - repo: https://github.com/ansible-community/ansible-lint.git + rev: v24.10.0 + hooks: + - id: ansible-lint + files: \.(yaml|yml)$ + additional_dependencies: + - jmespath diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..bc4110b --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,160 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + "defaults/main.yml": [ + { + "type": "Hex High Entropy String", + "filename": "defaults/main.yml", + "hashed_secret": "6ffda8dbbef8536f790b2cf5a1dece3b6555d1ce", + "is_verified": false, + "line_number": 9 + }, + { + "type": "Hex High Entropy String", + "filename": "defaults/main.yml", + "hashed_secret": "a60b9a4c47a1fcc036df3710885f4f03c4ee9d39", + "is_verified": false, + "line_number": 10 + }, + { + "type": "Secret Keyword", + "filename": "defaults/main.yml", + "hashed_secret": "73de13cf365ff2b5ddfbc96078305cef8fdc2990", + "is_verified": false, + "line_number": 138 + } + ], + "tasks/set_forgejo_version.yml": [ + { + "type": "Hex High Entropy String", + "filename": "tasks/set_forgejo_version.yml", + "hashed_secret": "085e34c422eef6cf478a8fd627bf67f8f0e94cd8", + "is_verified": false, + "line_number": 97 + } + ] + }, + "generated_at": "2025-01-18T19:52:12Z" +} diff --git a/README.md b/README.md index cc67100..ac57d4d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Ansible Galaxy](https://ansible.l3d.space/svg/roles-ansible.gitea.svg)](https://galaxy.ansible.com/ui/standalone/roles/roles-ansible/gitea/) [![BSD-3 Clause](https://ansible.l3d.space/svg/roles-ansible.gitea_license.svg)](LICENSE) -[![Maintainance](https://ansible.l3d.space/svg/roles-ansible.gitea_maintainance.svg)](https://ansible.l3d.space/#roles-ansible.gitea) +[![Maintenance](https://ansible.l3d.space/svg/roles-ansible.gitea_maintainance.svg)](https://ansible.l3d.space/#roles-ansible.gitea) ansible role gitea/forgejo ============================ @@ -87,7 +87,7 @@ This is because the Forgejo project maintains both `stable` and `old stable` rel | `gitea_gpg_server` | `hkps://keys.openpgp.org` | A gpg key server where this role can download the gpg key | | `gitea_backup_on_upgrade` | `false` | Optionally a backup can be created with every update of gitea. | | `gitea_backup_location` | `{{ gitea_home }}/backups/` | Where to store the gitea backup if one is created with this role. | -| `submodules_versioncheck` | `false` | a simple version check that can prevent you from accidentally running an older version of this role. *(recomended)* | +| `submodules_versioncheck` | `false` | a simple version check that can prevent you from accidentally running an older version of this role. *(recommended)* | ### gitea in the linux world | variable name | default value | description | @@ -347,7 +347,7 @@ This is because the Forgejo project maintains both `stable` and `old stable` rel ### Actions ([actions](https://docs.gitea.com/administration/config-cheat-sheet#actions-actions)) | variable name | default value | description | | ------------- | ------------- | ----------- | -| `gitea_actions_enabled` | `false` | Enable/Disable actions capabilities globaly. You may want to add `repo.actions` to `gitea_default_repo_units` to enable actions on all new repositories | +| `gitea_actions_enabled` | `false` | Enable/Disable actions capabilities globally. You may want to add `repo.actions` to `gitea_default_repo_units` to enable actions on all new repositories | | `gitea_actions_default_actions_url` | `github` | Default address to get action plugins, e.g. the default value means downloading from `https://github.com/actions/checkout` for `uses: actions/checkout@v3` | | `gitea_actions_extra` | | you can use this variable to pass additional config parameters in the `[actions]` section of the config. | diff --git a/defaults/main.yml b/defaults/main.yml index 961598f..699b5cc 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -284,3 +284,7 @@ gitea_custom: "{{ gitea_home }}/custom" gitea_customize_footer: false gitea_customize_files: false gitea_customize_files_path: "{{ gitea_custom_search }}/gitea_files/" + +# systemd hardening +gitea_systemd_hardening_enable: true +gitea_cgroups_restriction_enable: true diff --git a/files/dj3498u4hyyarh35rkjfnghbjxug6b19.asc b/files/dj3498u4hyyarh35rkjfnghbjxug6b19.asc new file mode 100644 index 0000000..0464688 Binary files /dev/null and b/files/dj3498u4hyyarh35rkjfnghbjxug6b19.asc differ diff --git a/tasks/backup.yml b/tasks/backup.yml index 1766be7..7534693 100644 --- a/tasks/backup.yml +++ b/tasks/backup.yml @@ -38,6 +38,6 @@ state: 'started' when: ansible_service_mgr == "systemd" - - name: Print updateing error and cancel + - name: Print updating error and cancel ansible.builtin.fail: msg: "failed to backup gitea" diff --git a/tasks/install_forgejo.yml b/tasks/install_forgejo.yml index 3f2bac7..fff905d 100644 --- a/tasks/install_forgejo.yml +++ b/tasks/install_forgejo.yml @@ -59,12 +59,24 @@ msg: "{{ _gitea_gpg_key_status.stdout }}" verbosity: 1 - - name: Import forgejo gpg key - ansible.builtin.command: "gpg --keyserver {{ gitea_gpg_server }} --recv {{ gitea_forgejo_gpg_key }}" - register: _gitea_import_key - become: false - changed_when: '"imported: 1" in _gitea_import_key.stderr' - # when: '_gitea_gpg_key_status.rc != 0 or "expired" in _gitea_gpg_key_status.stdout' + - name: Gpg key + block: + - name: Import forgejo gpg key + ansible.builtin.command: "gpg --keyserver {{ gitea_gpg_server }} --recv {{ gitea_forgejo_gpg_key }}" + register: _gitea_import_key + become: false + changed_when: '"imported: 1" in _gitea_import_key.stderr' + # when: '_gitea_gpg_key_status.rc != 0 or "expired" in _gitea_gpg_key_status.stdout' + rescue: + - name: Load local forgejo gpg key + ansible.builtin.copy: + src: dj3498u4hyyarh35rkjfnghbjxug6b19.asc + dest: /tmp/ + mode: '0400' + - name: Import local forgejo gpg key + ansible.builtin.command: "gpg --import /tmp/dj3498u4hyyarh35rkjfnghbjxug6b19.asc" + register: import0 + changed_when: "'imported: [1-9]+' in import0.stdout" - name: Check archive signature become: false diff --git a/tasks/local_git_users.yml b/tasks/local_git_users.yml index 9fb1597..9493e5d 100644 --- a/tasks/local_git_users.yml +++ b/tasks/local_git_users.yml @@ -1,18 +1,21 @@ --- - name: Identify gitea users - ansible.builtin.command: su - {{ gitea_user }} -c '{{ gitea_full_executable_path }} -c {{ gitea_configuration_path }}/gitea.ini admin user list' + ansible.builtin.command: + cmd: "{{ gitea_full_executable_path }} -c {{ gitea_configuration_path }}/gitea.ini admin user list" become: true + become_user: "{{ gitea_user }}" register: _giteausers changed_when: false - name: Use gitea cli to create user become: true - ansible.builtin.command: | - su - {{ gitea_user }} -c \ - '{{ gitea_full_executable_path }} -c {{ gitea_configuration_path }}/gitea.ini \ - admin user create --username "{{ user.name }}" \ - --password "{{ user.password }}" --email "{{ user.email }}" \ - --must-change-password={{ user.must_change_password }} --admin={{ user.admin }}' + become_user: "{{ gitea_user }}" + ansible.builtin.command: + cmd: > + {{ gitea_full_executable_path }} -c {{ gitea_configuration_path }}/gitea.ini + admin user create --username "{{ user.name }}" + --password "{{ user.password }}" --email "{{ user.email }}" + --must-change-password={{ user.must_change_password }} --admin={{ user.admin }} register: _gitearesult failed_when: - '"successfully created" not in _gitearesult.stdout' @@ -26,10 +29,11 @@ - name: Use gitea cli to delete user become: true - ansible.builtin.command: | - su - {{ gitea_user }} -c \ - '{{ gitea_full_executable_path }} -c {{ gitea_configuration_path }}/gitea.ini \ - admin user delete --username "{{ user.name }}"' + become_user: "{{ gitea_user }}" + ansible.builtin.command: + cmd: > + {{ gitea_full_executable_path }} -c {{ gitea_configuration_path }}/gitea.ini + admin user delete --username "{{ user.name }}" register: _giteadelresult failed_when: - '"error" in _giteadelresult.stdout' diff --git a/templates/gitea.ini.j2 b/templates/gitea.ini.j2 index c536051..6798805 100644 --- a/templates/gitea.ini.j2 +++ b/templates/gitea.ini.j2 @@ -1,5 +1,5 @@ ; this file is the configuration of your local Gitea instance -; {{ ansible_managed }} +{{ ansible_managed | comment(decoration="; ") }} ; ; This file overwrites the default values from Gitea. ; undefined variables will use the default value from Gitea. diff --git a/templates/gitea.service.j2 b/templates/gitea.service.j2 index 053a5cf..d7cc6dc 100644 --- a/templates/gitea.service.j2 +++ b/templates/gitea.service.j2 @@ -9,9 +9,106 @@ Group={{ gitea_group }} ExecStart={{ gitea_full_executable_path }} web --config {{ gitea_configuration_path }}/gitea.ini --custom-path {{ gitea_custom }}/ --work-path {{ gitea_home }} Restart=on-failure WorkingDirectory={{ gitea_home }} -{% if gitea_systemd_cap_net_bind_service %} + +{% if gitea_systemd_hardening_enable | bool %} +# Reduce Attack Surface +# Exposure level 9.2 -> 2.0 +NoNewPrivileges=yes +PrivateTmp=true +ProtectHome=yes +ProtectSystem=yes +# ProtectSystem=strict +{% if ansible_distribution == 'Ubuntu' and ansible_distribution_major_version | int >= 21 %} +# ProtectProc=noaccess +{% endif %} + +PrivateDevices=yes +DeviceAllow= + +# PrivateUsers=yes + +UMask=077 + +# ERROR: /proc not mounted - LibreOffice is unlikely to work well if at all +# InaccessiblePaths=/proc + +ProtectKernelTunables=true +ProtectKernelModules=yes +{% if (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version | int >= 20) or + (ansible_os_family == 'RedHat' and ansible_distribution_major_version | int > 8 ) +%} +ProtectKernelLogs=yes +ProtectHostname=yes +ProtectClock=yes +{% endif %} + +ProtectControlGroups=true +LockPersonality=true +RestrictRealtime=true +RestrictNamespaces=yes +# RestrictNamespaces=~CLONE_NEWCGROUP CLONE_NEWIPC CLONE_NEWNET CLONE_NEWPID +RestrictSUIDSGID=yes +MemoryDenyWriteExecute=yes +RemoveIPC=Yes + +# PrivateNetwork=yes +RestrictAddressFamilies=AF_INET AF_INET6 +RestrictAddressFamilies=~AF_UNIX + +IPAccounting=yes +# IPAddressAllow=localhost link-local multicast 10.0.0.0/8 192.168.0.0/16 +# IPAddressDeny= + +{% if gitea_systemd_cap_net_bind_service %} AmbientCapabilities=CAP_NET_BIND_SERVICE -{% endif %} +{% endif %} +CapabilityBoundingSet=~CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_DAC_READ_SEARCH +CapabilityBoundingSet=~CAP_SYS_RAWIO +CapabilityBoundingSet=~CAP_SYS_PTRACE +CapabilityBoundingSet=~CAP_DAC_* CAP_FOWNER CAP_IPC_OWNER +CapabilityBoundingSet=~CAP_BPF +CapabilityBoundingSet=~CAP_NET_ADMIN +CapabilityBoundingSet=~CAP_KILL +CapabilityBoundingSet=~CAP_NET_BROADCAST +CapabilityBoundingSet=~CAP_SYS_NICE CAP_SYS_RESOURCE +CapabilityBoundingSet=~CAP_SYS_BOOT +CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE +CapabilityBoundingSet=~CAP_SYS_CHROOT +CapabilityBoundingSet=~CAP_BLOCK_SUSPEND +CapabilityBoundingSet=~CAP_LEASE +CapabilityBoundingSet=~CAP_SYS_PACCT +CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG +CapabilityBoundingSet=~CAP_SYS_ADMIN +CapabilityBoundingSet=~CAP_SETUID CAP_SETGID +CapabilityBoundingSet=~CAP_SETPCAP +CapabilityBoundingSet=~CAP_CHOWN +CapabilityBoundingSet=~CAP_FSETID CAP_SETFCAP +CapabilityBoundingSet=~CAP_NET_RAW +CapabilityBoundingSet=~CAP_IPC_LOCK +{% if not (ansible_virtualization_type is defined and + ansible_virtualization_type == "docker" + ) +%} +{% if (ansible_os_family == 'RedHat' and ansible_distribution_major_version|int >= 8) or + (ansible_distribution == "Ubuntu" and ansible_distribution_major_version|int > 18) +%} +SystemCallFilter=@system-service +{% endif %} +# SystemCallFilter=~@debug @mount @cpu-emulation @obsolete @privileged @resources @reboot @swap @raw-io @module +SystemCallFilter=~@debug @mount @cpu-emulation @obsolete @resources @reboot @swap @raw-io @module +# When system call is disallowed, return error code instead of killing process +SystemCallErrorNumber=EPERM +{% endif %} +SystemCallArchitectures=native + +{% if gitea_cgroups_restriction_enable | bool %} +CPUWeight={{ gitea_cgroups_cpushares | default('1024') }} +CPUQuota={{ gitea_cgroups_cpuquota | default('80%') }} +MemoryMax={{ gitea_cgroups_memorylimit | default('4G') }} +IOWeight={{ gitea_cgroups_ioweight | default('80') }} +{% endif %} + +{% endif %} [Install] WantedBy=multi-user.target