├── .aar_doc.yml ├── .config └── ansible-lint.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── labeler.yml ├── version-drafter.yml └── workflows │ ├── ansible-lint.yml │ ├── codespell.yml │ ├── enforce-labels.yml │ ├── galaxy-publish.yml │ ├── mysql_hardening.yml │ ├── nginx_hardening.yml │ ├── os_hardening.yml │ ├── os_hardening_vm.yml │ ├── prettier-md.yml │ ├── release.yml │ ├── roles-readme.yml │ ├── ssh_hardening.yml │ ├── ssh_hardening_bsd.yml │ └── ssh_hardening_custom_tests.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── OS_HARDENING_CHANGELOG.md ├── README.md ├── galaxy.yml ├── meta └── runtime.yml ├── molecule ├── mysql_hardening │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ ├── prepare.yml │ ├── prepare_tasks │ │ └── mysql_users.yml │ ├── requirements.yml │ ├── verify.yml │ └── verify_tasks │ │ ├── mysql_users.yml │ │ └── service.yml ├── nginx_hardening │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ ├── official-nginx-role-debian.yml │ ├── official-nginx-role-redhat.yml │ ├── prepare.yml │ ├── requirements.yml │ └── verify.yml ├── os_hardening │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ ├── prepare.yml │ ├── prepare_tasks │ │ ├── ignore_home_folders.yml │ │ ├── netrc.yml │ │ ├── pw_ageing.yml │ │ ├── sys_account_shell.yml │ │ └── yum.yml │ ├── verify.yml │ ├── verify_tasks │ │ ├── ignore_home_folders.yml │ │ ├── netrc.yml │ │ ├── pam.yml │ │ ├── pw_ageing.yml │ │ ├── ssh_auth_locked.yml │ │ ├── sys_account_shell.yml │ │ └── yum.yml │ └── waivers.yaml ├── os_hardening_vm │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ ├── prepare.yml │ ├── prepare_tasks │ │ └── yum.yml │ ├── requirements.yml │ ├── verify.yml │ └── verify_tasks │ │ ├── pam.yml │ │ └── yum.yml ├── shared │ └── prerequisites.yml ├── ssh_hardening │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ ├── prepare.yml │ └── verify.yml ├── ssh_hardening_bsd │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ ├── prepare.yml │ ├── verify.yml │ ├── waivers_freebsd13.yaml │ ├── waivers_freebsd14.yaml │ └── waivers_openbsd7.yaml └── ssh_hardening_custom_tests │ ├── INSTALL.rst │ ├── converge.yml │ ├── molecule.yml │ └── prepare.yml ├── renovate.json ├── requirements.txt └── roles ├── mysql_hardening ├── CHANGELOG.md ├── README.md ├── defaults │ └── main.yml ├── handlers │ └── main.yml ├── meta │ ├── argument_specs.yml │ └── main.yml ├── tasks │ ├── configure.yml │ ├── main.yml │ └── mysql_secure_installation.yml ├── templates │ ├── hardening.cnf.j2 │ └── my.cnf.j2 └── vars │ ├── Debian.yml │ ├── Fedora.yml │ ├── FreeBSD.yml │ ├── Oracle Linux.yml │ ├── RedHat.yml │ ├── Suse.yml │ ├── Ubuntu.yml │ └── main.yml ├── nginx_hardening ├── CHANGELOG.md ├── README.md ├── defaults │ └── main.yml ├── handlers │ └── main.yml ├── meta │ ├── argument_specs.yml │ └── main.yml ├── tasks │ └── main.yml └── templates │ └── hardening.conf.j2 ├── os_hardening ├── CHANGELOG.md ├── README.md ├── defaults │ └── main.yml ├── handlers │ └── main.yml ├── meta │ ├── argument_specs.yml │ └── main.yml ├── tasks │ ├── apt.yml │ ├── auditd.yml │ ├── cron.yml │ ├── ctrlaltdel.yml │ ├── hardening.yml │ ├── limits.yml │ ├── login_defs.yml │ ├── main.yml │ ├── minimize_access.yml │ ├── minimize_access_fs.yml │ ├── modprobe.yml │ ├── netrc.yml │ ├── pam.yml │ ├── pam_debian.yml │ ├── pam_rhel.yml │ ├── profile.yml │ ├── rhosts.yml │ ├── securetty.yml │ ├── selinux.yml │ ├── suid_sgid.yml │ ├── sysctl.yml │ ├── user_accounts.yml │ └── yum.yml ├── templates │ ├── etc │ │ ├── audit │ │ │ └── auditd.conf.j2 │ │ ├── default │ │ │ └── ufw.j2 │ │ ├── libuser.conf.j2 │ │ ├── login.defs.j2 │ │ ├── modprobe.d │ │ │ └── modprobe.j2 │ │ ├── pam.d │ │ │ └── rhel_auth.j2 │ │ ├── profile.d │ │ │ ├── profile.conf.j2 │ │ │ └── tmout.sh.j2 │ │ ├── securetty.j2 │ │ ├── security │ │ │ └── faillock.conf.j2 │ │ ├── sysconfig │ │ │ └── rhel_sysconfig_init.j2 │ │ └── systemd │ │ │ └── coredump.conf.d │ │ │ └── coredumps.conf.j2 │ └── usr │ │ └── share │ │ └── pam-configs │ │ ├── pam_faillock.j2 │ │ ├── pam_faillock_authfail.j2 │ │ ├── pam_passwdqc.j2 │ │ └── pam_tally2.j2 └── vars │ ├── Amazon.yml │ ├── Archlinux.yml │ ├── Debian.yml │ ├── Fedora.yml │ ├── RedHat.yml │ ├── Suse.yml │ ├── Ubuntu.yml │ └── main.yml └── ssh_hardening ├── CHANGELOG.md ├── README.md ├── defaults └── main.yml ├── files ├── ssh_password └── sshd ├── handlers └── main.yml ├── meta ├── argument_specs.yml └── main.yml ├── tasks ├── ca_keys_and_principals.yml ├── crypto_ciphers.yml ├── crypto_hostkeys.yml ├── crypto_kex.yml ├── crypto_macs.yml ├── disable-systemd-socket.yml ├── hardening.yml ├── install.yml ├── main.yml └── selinux.yml ├── templates ├── authorized_principals.j2 ├── openssh.conf.j2 ├── opensshd.conf.j2 ├── revoked_keys.j2 └── trusted_user_ca_keys.j2 └── vars ├── Alpine.yml ├── Amazon_2.yml ├── Archlinux.yml ├── Debian.yml ├── Fedora.yml ├── Fedora_37.yml ├── FreeBSD.yml ├── OpenBSD.yml ├── RedHat.yml ├── RedHat_9.yml ├── SmartOS.yml ├── Suse.yml └── main.yml /.aar_doc.yml: -------------------------------------------------------------------------------- 1 | output_template: | 2 | 3 | 4 | ## Supported Operating Systems 5 | 6 | {%- for platform in metadata.galaxy_info.platforms %} 7 | - {{ platform.name }} 8 | {%- if "versions" in platform %} 9 | - {{ platform.versions | default([]) | join(', ') }} 10 | {%- endif %} 11 | {%- endfor %} 12 | 13 | ## Role Variables 14 | {% for entrypoint in argument_specs.keys() %} 15 | {%- set path, options=entrypoint_options[entrypoint][0] -%} 16 | {%- for name, details in options.items() |sort() %} 17 | - `{{ name }}` 18 | - Default: `{{ details.display_default }}` 19 | - Description: {{ details.display_description }} 20 | - Type: {{ details.display_type }} 21 | - Required: {{ details.display_required }} 22 | {%- if details.choices %} 23 | - Choices: 24 | {%- for choice in details.choices %} 25 | - {{ choice }} 26 | {%- endfor %} 27 | {%- endif %} 28 | {%- endfor %} 29 | {%- endfor %} 30 | 31 | ## Dependencies 32 | 33 | {%- if ("dependencies" in metadata) and (metadata.dependencies | length > 0) %} 34 | {%- for dependency in metadata.dependencies %} 35 | - {{ dependency }} 36 | {%- endfor %} 37 | {%- else %} 38 | 39 | None. 40 | {%- endif %} 41 | 42 | ## Example Playbook 43 | 44 | ``` 45 | - hosts: all 46 | become: true 47 | roles: 48 | - name: {{ role }} 49 | ``` 50 | 51 | 52 | -------------------------------------------------------------------------------- /.config/ansible-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # .ansible-lint 3 | # exclude_paths included in this file are parsed relative to this file's location 4 | # and not relative to the CWD of execution. CLI arguments passed to the --exclude 5 | # option will be parsed relative to the CWD of execution. 6 | exclude_paths: 7 | - .cache/ # implicit unless exclude_paths is defined in config 8 | - .ansible/ # somehow someone decided that the cache directory should be renamed 9 | # add all waivers individually, since exclude_files does not support globs 10 | - molecule/os_hardening/waivers.yaml 11 | - molecule/ssh_hardening_bsd/waivers_freebsd13.yaml 12 | - molecule/ssh_hardening_bsd/waivers_freebsd14.yaml 13 | - molecule/ssh_hardening_bsd/waivers_openbsd7.yaml 14 | 15 | mock_roles: 16 | - geerlingguy.git 17 | - nginxinc.nginx 18 | 19 | skip_list: 20 | - var-naming[no-role-prefix] 21 | - meta-runtime[unsupported-version] 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These files are text and should be normalized (Convert crlf => lf) 2 | *.php text eol=lf 3 | *.css text eol=lf 4 | *.js text eol=lf 5 | *.htm text eol=lf 6 | *.html text eol=lf 7 | *.xml text eol=lf 8 | *.txt text eol=lf 9 | *.ini text eol=lf 10 | *.inc text eol=lf 11 | .htaccess text eol=lf 12 | *.pp text eol=lf 13 | *.yml text eol=lf 14 | *.yaml text eol=lf 15 | *.sh text eol=lf 16 | 17 | # These files are binary and should be left untouched 18 | # (binary is a macro for -text -diff) 19 | *.png binary 20 | *.jpg binary 21 | *.jpeg binary 22 | *.gif binary 23 | *.ico binary 24 | *.mov binary 25 | *.mp4 binary 26 | *.mp3 binary 27 | *.flv binary 28 | *.fla binary 29 | *.swf binary 30 | *.gz binary 31 | *.zip binary 32 | *.7z binary 33 | *.ttf binary 34 | *.rpm binary 35 | 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # reference: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests 3 | name: "Bug report" 4 | description: "Create a report to help us improve." 5 | labels: [ 6 | "bug" 7 | ] 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Reporting Bugs ld follow some simple rules: 13 | 14 | - **Check**, if you can find similar bugs 15 | - **Describe** what is happening and what should happen 16 | - **Explain** how to reproduce the problem 17 | - **Add** more details and attachments 18 | - **Follow up**, if somebody is having questions or needs more details 19 | 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: "Description" 24 | description: Describe the bug 25 | placeholder: A clear and concise description of what the bug is. 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: reprod 31 | attributes: 32 | label: "Reproduction steps" 33 | render: Shell 34 | description: Paste an example playbook that can be used to reproduce the problem. This will be automatically formatted into code, no need for backticks. 35 | value: | 36 | ... 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: currentbehavior 42 | attributes: 43 | label: "Current Behavior" 44 | description: Please describe the results you received 45 | value: | 46 | ... 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | id: expectedbehavior 52 | attributes: 53 | label: "Expected Behavior" 54 | description: Please describe the results you expect 55 | value: | 56 | ... 57 | validations: 58 | required: true 59 | 60 | - type: textarea 61 | id: os 62 | attributes: 63 | label: "OS / Environment" 64 | value: | 65 | Provide all relevant information below, e.g. target OS versions, network device firmware, etc. 66 | validations: 67 | required: true 68 | 69 | - type: textarea 70 | id: ansible 71 | attributes: 72 | label: "Ansible Version" 73 | render: Shell 74 | value: | 75 | Paste verbatim output from "ansible --version" between quotes. This will be automatically formatted into code, so no need for backticks. 76 | validations: 77 | required: true 78 | 79 | - type: textarea 80 | id: collection 81 | attributes: 82 | label: "Collection Version" 83 | render: Shell 84 | value: | 85 | Paste version of the collection. This will be automatically formatted into code, so no need for backticks. 86 | validations: 87 | required: true 88 | 89 | - type: textarea 90 | id: additional 91 | attributes: 92 | label: "Additional information" 93 | description: Please add information like the used software versions, outputs of logs, OS information, screenshots, etc. to enhance the report 94 | value: | 95 | ... 96 | validations: 97 | required: false 98 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # reference: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests 3 | name: "Feature request" 4 | description: "Suggest an idea for this project." 5 | labels: [ 6 | "enhancement" 7 | ] 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Requesting a new feature or enhancement is following some simple rules: 13 | 14 | - **Check**, if the feature is already requested 15 | - **Describe** what your feature will bring to the community 16 | - **Explain** the criteria to fulfill the request 17 | - **Add** more details like mock ups, attachments, lists, screenshots 18 | - **Follow Up** in the discussion to the feature 19 | 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: "Description" 24 | description: Is your feature request related to a problem? Please describe. 25 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: solution 31 | attributes: 32 | label: "Solution" 33 | description: Describe the solution you'd like 34 | placeholder: A clear and concise description of what you want to happen. 35 | validations: 36 | required: false 37 | 38 | - type: textarea 39 | id: alternatives 40 | attributes: 41 | label: "Alternatives" 42 | description: Describe alternatives you've considered 43 | placeholder: A clear and concise description of any alternative solutions or features you've considered. 44 | validations: 45 | required: false 46 | 47 | - type: textarea 48 | id: additional 49 | attributes: 50 | label: "Additional information" 51 | description: Add any other context or screenshots about the feature request here. 52 | value: | 53 | ... 54 | validations: 55 | required: false 56 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_hardening: 3 | - changed-files: 4 | - any-glob-to-any-file: 5 | - roles/mysql_hardening/** 6 | - molecule/mysql_hardening/** 7 | - .github/workflows/mysql_hardening.yml 8 | 9 | os_hardening: 10 | - changed-files: 11 | - any-glob-to-any-file: 12 | - roles/os_hardening/** 13 | - molecule/os_hardening/** 14 | - .github/workflows/os_hardening.yml 15 | 16 | ssh_hardening: 17 | - changed-files: 18 | - any-glob-to-any-file: 19 | - roles/ssh_hardening/** 20 | - molecule/ssh_hardening/** 21 | - molecule/ssh_hardening_custom_tests/** 22 | - .github/workflows/ssh_hardening.yml 23 | - .github/workflows/ssh_hardening_custom_tests.yml 24 | 25 | nginx_hardening: 26 | - changed-files: 27 | - any-glob-to-any-file: 28 | - roles/nginx_hardening/** 29 | - molecule/nginx_hardening/** 30 | - .github/workflows/nginx_hardening.yml 31 | -------------------------------------------------------------------------------- /.github/version-drafter.yml: -------------------------------------------------------------------------------- 1 | major-labels: ['major'] 2 | minor-labels: ['minor', 'enhancement'] 3 | patch-labels: ['patch', 'bug'] 4 | -------------------------------------------------------------------------------- /.github/workflows/ansible-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Ansible Lint # feel free to pick your own name 3 | 4 | on: # yamllint disable-line rule:truthy 5 | # Run CI against all pushes (direct commits, also merged PRs), Pull Requests 6 | push: 7 | branches: [master] 8 | paths: 9 | - 'roles/**' 10 | - 'molecule/**' 11 | - 'requirements.txt' 12 | - '.github/workflows/ansible-lint.yml' 13 | - '.config/ansible-lint.yml' 14 | pull_request: 15 | # The branches below must be a subset of the branches above 16 | branches: [master] 17 | paths: 18 | - 'roles/**' 19 | - 'molecule/**' 20 | - 'requirements.txt' 21 | - '.github/workflows/ansible-lint.yml' 22 | - '.config/ansible-lint.yml' 23 | 24 | jobs: 25 | ansible-lint: 26 | runs-on: ubuntu-22.04 27 | 28 | steps: 29 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 30 | 31 | - name: Lint Ansible Playbook 32 | uses: ansible/ansible-lint@e98f9b38769d45cbe3c55a23b2eb25631babe7c4 # v25 33 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codespell - Spellcheck 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | codespell: 12 | uses: "dev-sec/.github/.github/workflows/codespell.yml@main" 13 | with: 14 | ignore_words_list: "chage,BOOTUP" 15 | -------------------------------------------------------------------------------- /.github/workflows/enforce-labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Enforce PR labels" 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request_target: 6 | types: [labeled, unlabeled, opened, edited, synchronize] 7 | 8 | permissions: 9 | contents: read # to read configuration file 10 | pull-requests: write # to label PRs 11 | 12 | jobs: 13 | enforce-label: 14 | if: github.repository == 'dev-sec/ansible-collection-hardening' 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 18 | with: 19 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 20 | -------------------------------------------------------------------------------- /.github/workflows/galaxy-publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish collection to Ansible Galaxy 3 | 4 | on: # yamllint disable-line rule:truthy 5 | release: 6 | types: 7 | - released 8 | 9 | jobs: 10 | deploy: 11 | if: github.repository == 'dev-sec/ansible-collection-hardening' 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 15 | 16 | # deploy the collection first, because if it fails, we don't want 17 | # to update the galaxy.yml 18 | - name: Deploy the collection 19 | uses: artis3n/ansible_galaxy_collection@f6110aef877db4caaa7e9a192975fb006dea61fe # v2 20 | with: 21 | api_key: ${{ secrets.GALAXY_API_KEY }} 22 | galaxy_version: ${{ github.event.release.tag_name }} 23 | 24 | # checkout master instead of the release-tag so we can push the galaxy.yml 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 26 | with: 27 | ref: master 28 | 29 | - name: update galaxy.yml with new version 30 | uses: microsoft/variable-substitution@6287962da9e5b6e68778dc51e840caa03ca84495 # v1 31 | with: 32 | files: 'galaxy.yml' 33 | env: 34 | version: "${{ github.event.release.tag_name }}" 35 | 36 | - name: push galaxy.yml 37 | uses: github-actions-x/commit@722d56b8968bf00ced78407bbe2ead81062d8baa # v2.9 38 | with: 39 | github-token: ${{ secrets.GITHUB_TOKEN }} 40 | push-branch: 'master' 41 | commit-message: 'update galaxy.yml with new version' 42 | force-add: 'true' 43 | files: galaxy.yml 44 | name: dev-sec CI 45 | email: hello@dev-sec.io 46 | -------------------------------------------------------------------------------- /.github/workflows/nginx_hardening.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "devsec.nginx_hardening" 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'roles/nginx_hardening/**' 9 | - 'molecule/nginx_hardening/**' 10 | - '.github/workflows/nginx_hardening.yml' 11 | - 'requirements.txt' 12 | pull_request: 13 | branches: [master] 14 | paths: 15 | - 'roles/nginx_hardening/**' 16 | - 'molecule/nginx_hardening/**' 17 | - '.github/workflows/nginx_hardening.yml' 18 | - 'requirements.txt' 19 | schedule: 20 | - cron: '0 6 * * 1' 21 | 22 | concurrency: 23 | group: >- 24 | ${{ github.workflow }}-${{ 25 | github.event.pull_request.number || github.sha 26 | }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | build: 31 | runs-on: ubuntu-22.04 32 | env: 33 | PY_COLORS: 1 34 | ANSIBLE_FORCE_COLOR: 1 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | molecule_distro: 39 | - centosstream9 40 | - rocky8 41 | - rocky9 42 | - ubuntu2004 43 | - ubuntu2204 44 | - ubuntu2404 45 | - debian11 46 | - debian12 47 | - amazon2023 48 | # - arch # needs to be fixed 49 | # - opensuse_tumbleweed # needs to be fixed 50 | # - fedora # no support from geerlingguy role 51 | steps: 52 | - name: Checkout repo 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 54 | with: 55 | path: ansible_collections/devsec/hardening 56 | submodules: true 57 | 58 | - name: Set up Python 59 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 60 | with: 61 | python-version: 3.12 62 | cache: 'pip' 63 | 64 | - name: Install dependencies 65 | run: pip install -r requirements.txt 66 | working-directory: ansible_collections/devsec/hardening 67 | 68 | - name: Downgrade Ansible for Rocky 8 tests 69 | run: pip install "ansible-core<2.17" 70 | working-directory: ansible_collections/devsec/hardening 71 | if: matrix.molecule_distro == 'rocky8' 72 | 73 | # Molecule has problems detecting the proper location for installing roles 74 | # https://github.com/ansible/molecule/issues/3806 75 | # we do not set a custom role path, but the automatically determined install path used is not compatible with the location molecule expects the role 76 | # see CI logs of this action "INFO Set ANSIBLE_ROLES_PATH" should not be present, since we do not set a custom path 77 | # we have to find a proper way to configure this 78 | - name: Temporary fix for roles 79 | run: | 80 | mkdir -p /home/runner/.ansible 81 | ln -s /home/runner/work/ansible-collection-hardening/ansible-collection-hardening/ansible_collections/devsec/hardening/roles \ 82 | /home/runner/.ansible/roles 83 | 84 | - name: Test with molecule 85 | run: molecule test -s nginx_hardening 86 | env: 87 | MOLECULE_DISTRO: ${{ matrix.molecule_distro }} 88 | working-directory: ansible_collections/devsec/hardening 89 | -------------------------------------------------------------------------------- /.github/workflows/os_hardening.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "devsec.os_hardening" 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'roles/os_hardening/**' 9 | - 'molecule/os_hardening/**' 10 | - '.github/workflows/os_hardening.yml' 11 | - 'requirements.txt' 12 | pull_request: 13 | branches: [master] 14 | paths: 15 | - 'roles/os_hardening/**' 16 | - 'molecule/os_hardening/**' 17 | - '.github/workflows/os_hardening.yml' 18 | - 'requirements.txt' 19 | schedule: 20 | - cron: '0 6 * * 3' 21 | 22 | concurrency: 23 | group: >- 24 | ${{ github.workflow }}-${{ 25 | github.event.pull_request.number || github.sha 26 | }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | build: 31 | runs-on: ubuntu-22.04 32 | env: 33 | PY_COLORS: 1 34 | ANSIBLE_FORCE_COLOR: 1 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | - molecule_distro: opensuse_tumbleweed 40 | molecule_docker_command: "/usr/lib/systemd/systemd" 41 | molecule_distro: 42 | - centosstream9 43 | - rocky8 44 | - rocky9 45 | - fedora39 46 | - fedora40 47 | - ubuntu2004 48 | - ubuntu2204 49 | - ubuntu2404 50 | - debian11 51 | - debian12 52 | - amazon2023 53 | - arch 54 | molecule_docker_command: 55 | - "/lib/systemd/systemd" 56 | steps: 57 | - name: Checkout repo 58 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 59 | with: 60 | path: ansible_collections/devsec/hardening 61 | submodules: true 62 | 63 | - name: Set up Python 64 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 65 | with: 66 | python-version: 3.12 67 | cache: 'pip' 68 | 69 | - name: Install dependencies 70 | run: pip install -r requirements.txt 71 | working-directory: ansible_collections/devsec/hardening 72 | 73 | - name: Downgrade Ansible for Rocky 8 tests 74 | run: pip install "ansible-core<2.17" 75 | working-directory: ansible_collections/devsec/hardening 76 | if: matrix.molecule_distro == 'rocky8' 77 | 78 | - name: Test with molecule 79 | run: molecule test -s os_hardening 80 | env: 81 | MOLECULE_DISTRO: ${{ matrix.molecule_distro }} 82 | MOLECULE_DOCKER_COMMAND: ${{ matrix.molecule_docker_command }} 83 | working-directory: ansible_collections/devsec/hardening 84 | -------------------------------------------------------------------------------- /.github/workflows/os_hardening_vm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "devsec.os_hardening VM" 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'roles/os_hardening/**' 9 | - 'molecule/os_hardening_vm/**' 10 | - '.github/workflows/os_hardening_vm.yml' 11 | - 'requirements.txt' 12 | pull_request: 13 | branches: [master] 14 | paths: 15 | - 'roles/os_hardening/**' 16 | - 'molecule/os_hardening_vm/**' 17 | - '.github/workflows/os_hardening_vm.yml' 18 | - 'requirements.txt' 19 | schedule: 20 | - cron: '0 6 * * 2' 21 | 22 | concurrency: 23 | group: >- 24 | ${{ github.workflow }}-${{ 25 | github.event.pull_request.number || github.sha 26 | }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | build: 31 | runs-on: self-hosted 32 | env: 33 | PY_COLORS: 1 34 | ANSIBLE_FORCE_COLOR: 1 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | molecule_distro: 39 | - generic/centos9s 40 | - generic/rocky8 41 | - generic/rocky9 42 | - fedora/39-cloud-base 43 | - fedora/40-cloud-base 44 | - generic/ubuntu2004 45 | - generic/ubuntu2204 46 | - alvistack/ubuntu-24.04 47 | - generic/debian11 48 | - generic/debian12 49 | - generic/opensuse15 50 | - generic/arch 51 | steps: 52 | - name: Checkout repo 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 54 | with: 55 | path: ansible_collections/devsec/hardening 56 | submodules: true 57 | 58 | - name: Install dependencies 59 | run: | 60 | source ~/.venv/ansible-collection-hardening/bin/activate 61 | python -m pip install --no-cache-dir --upgrade pip 62 | pip install -r requirements.txt 63 | pip install python-vagrant 64 | working-directory: ansible_collections/devsec/hardening 65 | 66 | - name: Downgrade Ansible for Rocky 8 tests 67 | run: | 68 | source ~/.venv/ansible-collection-hardening/bin/activate 69 | pip install "ansible-core<2.17" 70 | working-directory: ansible_collections/devsec/hardening 71 | if: matrix.molecule_distro == 'generic/rocky8' || matrix.molecule_distro == 'generic/opensuse15' 72 | 73 | - name: Update Vagrant Box 74 | run: | 75 | vagrant box update --box ${{ matrix.molecule_distro }} || true 76 | 77 | - name: Test with molecule 78 | run: | 79 | source ~/.venv/ansible-collection-hardening/bin/activate 80 | molecule test -s os_hardening_vm 81 | env: 82 | MOLECULE_DISTRO: ${{ matrix.molecule_distro }} 83 | working-directory: ansible_collections/devsec/hardening 84 | -------------------------------------------------------------------------------- /.github/workflows/prettier-md.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://github.com/creyD/prettier_action 3 | name: Prettier markdown files 4 | 5 | on: # yamllint disable-line rule:truthy 6 | push: 7 | branches: [master] 8 | paths: 9 | - '**.md' 10 | 11 | jobs: 12 | prettier-md: 13 | if: github.repository == 'dev-sec/ansible-collection-hardening' 14 | runs-on: ubuntu-22.04 15 | timeout-minutes: 1 16 | 17 | steps: 18 | - name: Git checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | with: 21 | ref: ${{ github.head_ref }} 22 | 23 | - name: Prettify code 24 | uses: creyD/prettier_action@8c18391fdc98ed0d884c6345f03975edac71b8f0 # v4.6 25 | with: 26 | prettier_options: --write {**/*,*}.md 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: New release 3 | 4 | on: # yamllint disable-line rule:truthy 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | generate_changelog: 12 | name: create release draft 13 | if: github.repository == 'dev-sec/ansible-collection-hardening' 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | with: 18 | fetch-depth: 0 19 | ref: master 20 | 21 | - name: 'Get Previous tag' 22 | id: previoustag 23 | uses: "WyriHaximus/github-action-get-previous-tag@master" 24 | env: 25 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 26 | 27 | - name: calculate next version 28 | id: version 29 | uses: patrickjahns/version-drafter-action@2076fa43abb28f31d0e8b0890253fbd1d1a966fc # v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Generate changelog 34 | uses: charmixer/auto-changelog-action@b25e89c9410e03189f0d48b02b3a8caad6e78015 # v1 35 | with: 36 | token: ${{ secrets.GITHUB_TOKEN }} 37 | future_release: ${{ steps.version.outputs.next-version }} 38 | # this excludes all versions prior to the collection-release 39 | # since they break the changelog generation with the error: 40 | # "No common ancestor between ... and $version" 41 | exclude_tags_regex: '^[0-6]\.\d\.\d' 42 | issue_line_labels: mysql_hardening,os_hardening,ssh_hardening,nginx_hardening 43 | 44 | - name: push changelog 45 | uses: github-actions-x/commit@722d56b8968bf00ced78407bbe2ead81062d8baa # v2.9 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | push-branch: 'master' 49 | commit-message: 'update changelog' 50 | force-add: 'true' 51 | files: CHANGELOG.md 52 | name: dev-sec CI 53 | email: hello@dev-sec.io 54 | 55 | - name: Generate changelog for the release 56 | run: | 57 | sed '/## \[${{ steps.previoustag.outputs.tag }}\]/Q' CHANGELOG.md > CHANGELOGRELEASE.md 58 | 59 | - name: Read CHANGELOG.md 60 | id: package 61 | uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1 62 | with: 63 | path: ./CHANGELOGRELEASE.md 64 | 65 | - name: Delete old drafts 66 | uses: hugo19941994/delete-draft-releases@1bdca1ea7ffb25ae7f468a7bdb40056dae98175e # v1.0.1 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: Create Release draft 71 | id: create_release 72 | uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e # v1 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 75 | with: 76 | release_name: ${{ steps.version.outputs.next-version }} 77 | tag_name: ${{ steps.version.outputs.next-version }} 78 | body: | 79 | ${{ steps.package.outputs.content }} 80 | draft: true 81 | -------------------------------------------------------------------------------- /.github/workflows/roles-readme.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: create roles readme 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'roles/**/meta/argument_specs.yml' 9 | - 'roles/**/meta/main.yml' 10 | pull_request: 11 | branches: [master] 12 | paths: 13 | - 'roles/**/meta/argument_specs.yml' 14 | - 'roles/**/meta/main.yml' 15 | - 'requirements.txt' 16 | 17 | jobs: 18 | readme: 19 | name: create roles readme 20 | runs-on: ubuntu-22.04 21 | strategy: 22 | matrix: 23 | roles: 24 | - mysql_hardening 25 | - nginx_hardening 26 | - os_hardening 27 | - ssh_hardening 28 | steps: 29 | - name: Check out code 30 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 31 | 32 | - name: Set up Python 33 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 34 | with: 35 | python-version: 3.12 36 | cache: 'pip' 37 | 38 | - name: Install dependencies 39 | run: pip install -r requirements.txt 40 | 41 | - name: Run aar-doc 42 | run: aar-doc roles/${{ matrix.roles }} markdown 43 | 44 | - name: Output diff 45 | run: git diff roles/${{ matrix.roles }}/README.md 46 | 47 | - name: Push README 48 | if: github.event_name != 'pull_request' 49 | uses: github-actions-x/commit@722d56b8968bf00ced78407bbe2ead81062d8baa # v2.9 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | commit-message: 'update ${{ matrix.roles }} readme' 53 | files: roles/${{ matrix.roles }}/README.md 54 | rebase: true 55 | name: dev-sec CI 56 | email: hello@dev-sec.io 57 | -------------------------------------------------------------------------------- /.github/workflows/ssh_hardening.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "devsec.ssh_hardening" 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'roles/ssh_hardening/**' 9 | - 'molecule/ssh_hardening/**' 10 | - '.github/workflows/ssh_hardening.yml' 11 | - 'requirements.txt' 12 | pull_request: 13 | branches: [master] 14 | paths: 15 | - 'roles/ssh_hardening/**' 16 | - 'molecule/ssh_hardening/**' 17 | - '.github/workflows/ssh_hardening.yml' 18 | - 'requirements.txt' 19 | schedule: 20 | - cron: '0 6 * * 5' 21 | 22 | concurrency: 23 | group: >- 24 | ${{ github.workflow }}-${{ 25 | github.event.pull_request.number || github.sha 26 | }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | build: 31 | runs-on: ubuntu-22.04 32 | env: 33 | PY_COLORS: 1 34 | ANSIBLE_FORCE_COLOR: 1 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | # needs fix - opensuse has different file location for conf and pam (/usr/etc/ssh/?, /usr/lib/pam.d/?) 40 | # - molecule_distro: opensuse_tumbleweed 41 | # molecule_docker_command: "/usr/lib/systemd/systemd" 42 | - molecule_distro: alpine 43 | molecule_docker_command: "/sbin/init" 44 | molecule_distro: 45 | - centosstream9 46 | - rocky8 47 | - rocky9 48 | - fedora39 49 | - fedora40 50 | - ubuntu2004 51 | - ubuntu2204 52 | - ubuntu2404 53 | - debian11 54 | - debian12 55 | - amazon2023 56 | - arch 57 | molecule_docker_command: 58 | - "/lib/systemd/systemd" 59 | steps: 60 | - name: Checkout repo 61 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 62 | with: 63 | path: ansible_collections/devsec/hardening 64 | submodules: true 65 | 66 | - name: Set up Python 67 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 68 | with: 69 | python-version: 3.12 70 | cache: 'pip' 71 | 72 | - name: Install dependencies 73 | run: pip install -r requirements.txt 74 | working-directory: ansible_collections/devsec/hardening 75 | 76 | - name: Downgrade Ansible for Rocky 8 tests 77 | run: pip install "ansible-core<2.17" 78 | working-directory: ansible_collections/devsec/hardening 79 | if: matrix.molecule_distro == 'rocky8' 80 | 81 | - name: Test with molecule 82 | run: molecule test -s ssh_hardening 83 | env: 84 | MOLECULE_DISTRO: ${{ matrix.molecule_distro }} 85 | MOLECULE_DOCKER_COMMAND: ${{ matrix.molecule_docker_command }} 86 | working-directory: ansible_collections/devsec/hardening 87 | -------------------------------------------------------------------------------- /.github/workflows/ssh_hardening_bsd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "devsec.ssh_hardening BSD" 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'roles/ssh_hardening/**' 9 | - 'molecule/ssh_hardening_bsd/**' 10 | - '.github/workflows/ssh_hardening_bsd.yml' 11 | - 'requirements.txt' 12 | pull_request: 13 | branches: [master] 14 | paths: 15 | - 'roles/ssh_hardening/**' 16 | - 'molecule/ssh_hardening_bsd/**' 17 | - '.github/workflows/ssh_hardening_bsd.yml' 18 | - 'requirements.txt' 19 | schedule: 20 | - cron: '0 6 * * 5' 21 | 22 | concurrency: 23 | group: >- 24 | ${{ github.workflow }}-${{ 25 | github.event.pull_request.number || github.sha 26 | }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | build: 31 | runs-on: self-hosted 32 | env: 33 | PY_COLORS: 1 34 | ANSIBLE_FORCE_COLOR: 1 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | molecule_distro: 39 | - openbsd7 40 | - freebsd13 41 | - freebsd14 42 | steps: 43 | - name: Checkout repo 44 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 45 | with: 46 | path: ansible_collections/devsec/hardening 47 | submodules: true 48 | 49 | - name: Update Vagrant Box 50 | run: vagrant box update --box generic/${{ matrix.molecule_distro }} || true 51 | 52 | - name: Test with molecule 53 | run: molecule test -s ssh_hardening_bsd 54 | env: 55 | MOLECULE_DISTRO: ${{ matrix.molecule_distro }} 56 | working-directory: ansible_collections/devsec/hardening 57 | -------------------------------------------------------------------------------- /.github/workflows/ssh_hardening_custom_tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "devsec.ssh_hardening with custom tests" 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | paths: 8 | - 'roles/ssh_hardening/**' 9 | - 'molecule/ssh_hardening_custom_tests/**' 10 | - '.github/workflows/ssh_hardening_custom_tests.yml' 11 | - 'requirements.txt' 12 | pull_request: 13 | branches: [master] 14 | paths: 15 | - 'roles/ssh_hardening/**' 16 | - 'molecule/ssh_hardening_custom_tests/**' 17 | - '.github/workflows/ssh_hardening_custom_tests.yml' 18 | - 'requirements.txt' 19 | schedule: 20 | - cron: '0 6 * * 4' 21 | 22 | concurrency: 23 | group: >- 24 | ${{ github.workflow }}-${{ 25 | github.event.pull_request.number || github.sha 26 | }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | build: 31 | runs-on: ubuntu-22.04 32 | env: 33 | PY_COLORS: 1 34 | ANSIBLE_FORCE_COLOR: 1 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | # needs fix - opensuse has different file location for conf and pam (/usr/etc/ssh/?, /usr/lib/pam.d/?) 40 | # - molecule_distro: opensuse_tumbleweed 41 | # molecule_docker_command: "/usr/lib/systemd/systemd" 42 | - molecule_distro: alpine 43 | molecule_docker_command: "/sbin/init" 44 | molecule_distro: 45 | - centosstream9 46 | - rocky8 47 | - rocky9 48 | - fedora39 49 | - fedora40 50 | - ubuntu2004 51 | - ubuntu2204 52 | - ubuntu2404 53 | - debian11 54 | - debian12 55 | - amazon2023 56 | - arch 57 | molecule_docker_command: 58 | - "/lib/systemd/systemd" 59 | steps: 60 | - name: Checkout repo 61 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 62 | with: 63 | path: ansible_collections/devsec/hardening 64 | submodules: true 65 | 66 | - name: Set up Python 67 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 68 | with: 69 | python-version: 3.12 70 | cache: 'pip' 71 | 72 | - name: Install dependencies 73 | run: pip install -r requirements.txt 74 | working-directory: ansible_collections/devsec/hardening 75 | 76 | - name: Downgrade Ansible for Rocky 8 tests 77 | run: pip install "ansible-core<2.17" 78 | working-directory: ansible_collections/devsec/hardening 79 | if: matrix.molecule_distro == 'rocky8' 80 | 81 | - name: Test with molecule 82 | run: molecule test -s ssh_hardening_custom_tests 83 | env: 84 | MOLECULE_DISTRO: ${{ matrix.molecule_distro }} 85 | MOLECULE_DOCKER_COMMAND: ${{ matrix.molecule_docker_command }} 86 | working-directory: ansible_collections/devsec/hardening 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .kitchen 2 | hosts 3 | Gemfile.lock 4 | .venv -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "roles/windows_hardening"] 2 | path = roles/windows_hardening 3 | url = https://github.com/dev-sec/ansible-windows-hardening/ 4 | [submodule "roles/apache_hardening"] 5 | path = roles/apache_hardening 6 | url = https://github.com/dev-sec/ansible-apache-hardening/ 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | namespace: devsec 2 | name: hardening 3 | version: 10.3.0 4 | readme: README.md 5 | authors: 6 | - dev-sec 7 | description: 'This collection provides battle tested hardening for Linux, SSH, nginx, MySQL' 8 | license: 9 | - Apache-2.0 10 | license_file: '' 11 | tags: 12 | - devsec 13 | - hardening 14 | - centos 15 | - ubuntu 16 | - debian 17 | - nginx 18 | - mysql 19 | - openssh 20 | - apache_hardening 21 | - mysql_hardening 22 | - nginx_hardening 23 | - os_hardening 24 | - ssh_hardening 25 | - database 26 | - linux 27 | - security 28 | dependencies: 29 | ansible.posix: '>=1.0.0' 30 | community.crypto: '>=1.0.0' 31 | community.general: '>=1.0.0' 32 | community.mysql: '>=1.3.0' 33 | repository: 'https://github.com/dev-sec/ansible-collection-hardening/' 34 | homepage: 'https://dev-sec.io/' 35 | issues: 'https://github.com/dev-sec/ansible-collection-hardening/issues' 36 | build_ignore: 37 | - codecov.yml 38 | - .github 39 | - .gitattributes 40 | - .gitignore 41 | - hacking 42 | - requirements.txt 43 | - test-requirements.txt 44 | - tests 45 | - .tox 46 | - tox.ini 47 | - .yamllint 48 | - renovate.json 49 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | requires_ansible: ">=2.9.10" 3 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ****** 2 | Docker driver installation guide 3 | ****** 4 | 5 | Requirements 6 | ============ 7 | 8 | * Docker Engine 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ python3 -m pip install 'molecule[docker]' 23 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-mysql-hardening" 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Determine required MySQL Python libraries (Ubuntu Focal Fossa ++) 11 | ansible.builtin.set_fact: 12 | mysql_python_package_debian: python3-pymysql 13 | when: 14 | - mysql_python_package_debian is not defined 15 | - ansible_distribution == "Ubuntu" 16 | - ansible_distribution_major_version|int > 19 17 | 18 | - name: Determine required MySQL Python libraries. 19 | ansible.builtin.set_fact: 20 | mysql_python_package_debian: "{% if 'python3' in ansible_python_interpreter | default('') %}python3-mysqldb{% else %}python-mysqldb{% endif %}" 21 | when: 22 | - mysql_python_package_debian is not defined 23 | - ansible_distribution != "Ubuntu" 24 | - ansible_distribution_major_version|int < 20 25 | 26 | - name: Use Python 3 on Suse 27 | ansible.builtin.set_fact: 28 | ansible_python_interpreter: /usr/bin/python3 29 | when: 30 | - ansible_os_family == 'Suse' 31 | 32 | - name: Include mysql_hardening role 33 | ansible.builtin.include_role: 34 | name: devsec.hardening.mysql_hardening 35 | vars: 36 | overwrite_global_mycnf: false 37 | mysql_root_password: iloverandompasswordsbutthiswilldo 38 | mysql_user_password: iloverandompasswordsbutthiswilldo 39 | mysql_config_file: /etc/mysql/mariadb.cnf 40 | mysql_root_password_update: true 41 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | role-file: molecule/mysql_hardening/requirements.yml 6 | requirements-file: molecule/mysql_hardening/requirements.yml 7 | driver: 8 | name: docker 9 | platforms: 10 | - name: instance 11 | image: rndmh3ro/docker-${MOLECULE_DISTRO}-ansible:latest 12 | command: ${MOLECULE_DOCKER_COMMAND:-/lib/systemd/systemd} 13 | volumes: 14 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 15 | privileged: true 16 | cgroupns_mode: host 17 | pre_build_image: true 18 | provisioner: 19 | name: ansible 20 | options: 21 | diff: true 22 | config_options: 23 | defaults: 24 | interpreter_python: auto_silent 25 | callbacks_enabled: profile_tasks, timer, yaml 26 | verifier: 27 | name: ansible 28 | 29 | scenario: 30 | create_sequence: 31 | - dependency 32 | - create 33 | - prepare 34 | check_sequence: 35 | - dependency 36 | - verify ../shared/prerequisites.yml 37 | - destroy 38 | - create 39 | - prepare 40 | - converge 41 | - check 42 | - destroy 43 | converge_sequence: 44 | - dependency 45 | - create 46 | - prepare 47 | - converge 48 | destroy_sequence: 49 | - destroy 50 | test_sequence: 51 | - dependency 52 | - verify ../shared/prerequisites.yml 53 | - destroy 54 | - syntax 55 | - create 56 | - prepare 57 | - check 58 | - converge 59 | - idempotence 60 | - verify 61 | - destroy 62 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-mysql-hardening" 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | vars: 10 | overwrite_global_mycnf: false 11 | mysql_root_password: iloverandompasswordsbutthiswilldo 12 | mysql_user_password: iloverandompasswordsbutthiswilldo 13 | mysql_config_file: /etc/mysql/mariadb.cnf 14 | mysql_root_password_update: true 15 | tasks: 16 | - name: Use Python 3 on Debian 11 17 | ansible.builtin.set_fact: 18 | ansible_python_interpreter: /usr/bin/python3 19 | when: 20 | - ansible_distribution == 'Debian' 21 | - ansible_distribution_major_version|int >= 11 22 | 23 | - name: Use Python 3 on Suse 24 | ansible.builtin.set_fact: 25 | ansible_python_interpreter: /usr/bin/python3 26 | when: 27 | - ansible_os_family == 'Suse' 28 | 29 | - name: Run the equivalent of "apt-get update && apt-get upgrade" 30 | ansible.builtin.apt: 31 | upgrade: safe 32 | update_cache: true 33 | when: ansible_os_family == 'Debian' 34 | 35 | - name: Install required python packages on Suse 36 | ansible.builtin.command: zypper -n install python311-rpm python311-PyMySQL 37 | changed_when: false 38 | when: ansible_os_family == 'Suse' 39 | 40 | - name: Create missing directory 41 | ansible.builtin.file: 42 | path: /etc/mysql 43 | state: directory 44 | mode: "0755" 45 | 46 | - name: Determine required MySQL Python libraries (Ubuntu Focal Fossa ++) 47 | ansible.builtin.set_fact: 48 | mysql_python_package_debian: python3-pymysql 49 | when: 50 | - mysql_python_package_debian is not defined 51 | - ansible_distribution == "Ubuntu" 52 | - ansible_distribution_major_version|int > 19 53 | 54 | - name: Determine required MySQL Python libraries. 55 | ansible.builtin.set_fact: 56 | mysql_python_package_debian: "{% if 'python3' in ansible_python_interpreter | default('') %}python3-mysqldb{% else %}python-mysqldb{% endif %}" 57 | when: 58 | - mysql_python_package_debian is not defined 59 | - ansible_distribution != "Ubuntu" 60 | - ansible_distribution_major_version|int < 20 61 | 62 | - name: Install required MySQL Python libraries on RHEL 63 | ansible.builtin.dnf: 64 | name: "{% if 'python3' in ansible_python_interpreter | default('') %}python36-PyMySQL{% else %}python2-PyMySQL{% endif %}" 65 | when: 66 | - ansible_os_family == "RedHat" 67 | - ansible_distribution_major_version == "7" 68 | 69 | - name: Install mysql with a generic Ansible role 70 | ansible.builtin.include_role: 71 | name: dev-sec.mysql 72 | 73 | - name: Include MySQL user prepare tasks 74 | ansible.builtin.include_tasks: prepare_tasks/mysql_users.yml 75 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/prepare_tasks/mysql_users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create users for test 3 | community.mysql.mysql_query: 4 | query: 5 | - CREATE USER 'user'@'delete'; 6 | - CREATE USER 'user'@'127.0.0.1'; 7 | - CREATE USER 'user'@'::1'; 8 | - CREATE USER 'user'@'%'; 9 | - CREATE USER 'user'@'192.168.0.%'; 10 | - CREATE USER 'user'@'192.168.0.1'; 11 | - CREATE USER '%'@'192.168.0.1'; 12 | - CREATE USER 'user'@'192.168.0.2' IDENTIFIED BY 'keep'; 13 | - CREATE USER 'user'@'keep' IDENTIFIED BY 'keep'; 14 | - CREATE USER 'user'@'192.168.%' IDENTIFIED BY 'keep'; 15 | login_unix_socket: "{{ login_unix_socket | default(omit) }}" 16 | 17 | - name: Detect role support on MySQL 18 | community.mysql.mysql_query: 19 | query: > 20 | SELECT 1 FROM information_schema.COLUMNS 21 | WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' 22 | AND COLUMN_NAME = 'is_role'; 23 | login_unix_socket: "{{ login_unix_socket | default(omit) }}" 24 | register: mysql_role_support 25 | 26 | - name: Create roles for test 27 | community.mysql.mysql_query: 28 | query: 29 | - CREATE ROLE 'role_keep'; 30 | login_unix_socket: "{{ login_unix_socket | default(omit) }}" 31 | when: 32 | - mysql_role_support.rowcount[0] > 0 33 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | roles: 3 | - name: dev-sec.mysql 4 | version: master 5 | 6 | collections: 7 | - name: https://github.com/ansible-collections/community.mysql.git 8 | type: git 9 | version: main 10 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Use Python 3 on Suse 11 | ansible.builtin.set_fact: 12 | ansible_python_interpreter: /usr/bin/python3 13 | when: 14 | - ansible_os_family == 'Suse' 15 | 16 | - name: Install procps for debian systems 17 | ansible.builtin.apt: 18 | name: procps 19 | state: present 20 | update_cache: true 21 | when: ansible_distribution == 'Debian' 22 | 23 | - name: Include tests for the service 24 | ansible.builtin.include_tasks: verify_tasks/service.yml 25 | 26 | - name: Include tests for MySQL user 27 | ansible.builtin.include_tasks: verify_tasks/mysql_users.yml 28 | 29 | - name: Verify 30 | hosts: localhost 31 | environment: 32 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 33 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 34 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 35 | tasks: 36 | - name: Execute cinc-auditor tests 37 | ansible.builtin.command: > 38 | docker run 39 | --volume /run/docker.sock:/run/docker.sock 40 | docker.io/cincproject/auditor exec 41 | -t docker://instance 42 | --no-show-progress --no-color 43 | --no-distinct-exit https://github.com/dev-sec/mysql-baseline/archive/refs/heads/master.zip 44 | register: test_results 45 | changed_when: false 46 | ignore_errors: true 47 | 48 | - name: Display details about the cinc-auditor results 49 | ansible.builtin.debug: 50 | msg: "{{ test_results.stdout_lines }}" 51 | 52 | - name: Fail when tests fail 53 | ansible.builtin.fail: 54 | msg: Inspec failed to validate 55 | when: test_results.rc != 0 56 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/verify_tasks/mysql_users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get all users from MySQL server 3 | community.mysql.mysql_query: 4 | query: > 5 | SELECT CONCAT(USER, '@', HOST) AS users FROM mysql.user; 6 | login_unix_socket: "{{ login_unix_socket | default(omit) }}" 7 | register: mysql_users 8 | 9 | - name: Create list of users from mysql query 10 | ansible.builtin.set_fact: 11 | mysql_users_list: "{{ mysql_users.query_result.0 | json_query('[*].users') | list }}" 12 | 13 | - name: Assert that only accounts with password remain 14 | ansible.builtin.assert: 15 | that: 16 | - '"user@delete" not in mysql_users_list' 17 | - '"user@127.0.0.1" not in mysql_users_list' 18 | - '"user@::1" not in mysql_users_list' 19 | - '"user@%" not in mysql_users_list' 20 | - '"user@192.168.0.%" not in mysql_users_list' 21 | - '"user@192.168.0.1" not in mysql_users_list' 22 | - '"%@192.168.0.1" not in mysql_users_list' 23 | - '"user@192.168.0.2" in mysql_users_list' 24 | - '"user@keep" in mysql_users_list' 25 | - '"user@192.168.%" in mysql_users_list' 26 | 27 | - name: Detect role support on MySQL 28 | community.mysql.mysql_query: 29 | query: > 30 | SELECT 1 FROM information_schema.COLUMNS 31 | WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' 32 | AND COLUMN_NAME = 'is_role'; 33 | login_unix_socket: "{{ login_unix_socket | default(omit) }}" 34 | register: mysql_role_support 35 | 36 | - name: Assert that roles remain 37 | ansible.builtin.assert: 38 | that: 39 | - '"role_keep@" in mysql_users_list' 40 | when: 41 | - mysql_role_support.rowcount[0] > 0 42 | -------------------------------------------------------------------------------- /molecule/mysql_hardening/verify_tasks/service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Load variables from role to use the "mysql_daemon" variable 3 | ansible.builtin.include_role: 4 | name: devsec.hardening.mysql_hardening 5 | apply: 6 | tags: 7 | - never 8 | 9 | - name: Populate service facts 10 | ansible.builtin.service_facts: 11 | 12 | - name: Check if MySQL is running and enabled 13 | ansible.builtin.assert: 14 | that: 15 | - ansible_facts.services[mysql_daemon + '.service'].state == 'running' 16 | - ansible_facts.services[mysql_daemon + '.service'].status == 'enabled' 17 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * Docker Engine 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ python3 -m pip install 'molecule[docker]' 23 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-nginx-hardening" with custom settings 3 | become: true 4 | hosts: all 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | vars: 10 | nginx_ppa_use: true 11 | nginx_ppa_version: stable 12 | tasks: 13 | - name: Include nginx_hardening role 14 | ansible.builtin.include_role: 15 | name: devsec.hardening.nginx_hardening 16 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | role-file: molecule/nginx_hardening/requirements.yml 6 | driver: 7 | name: docker 8 | platforms: 9 | - name: instance 10 | image: rndmh3ro/docker-${MOLECULE_DISTRO}-ansible:latest 11 | command: ${MOLECULE_DOCKER_COMMAND:-/lib/systemd/systemd} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | privileged: true 15 | cgroupns_mode: host 16 | pre_build_image: true 17 | provisioner: 18 | name: ansible 19 | options: 20 | diff: true 21 | config_options: 22 | defaults: 23 | interpreter_python: auto_silent 24 | callbacks_enabled: profile_tasks, timer, yaml 25 | verifier: 26 | name: ansible 27 | 28 | scenario: 29 | create_sequence: 30 | - dependency 31 | - create 32 | - prepare 33 | check_sequence: 34 | - dependency 35 | - verify ../shared/prerequisites.yml 36 | - destroy 37 | - create 38 | - prepare 39 | - converge 40 | - check 41 | - destroy 42 | converge_sequence: 43 | - dependency 44 | - create 45 | - prepare 46 | - converge 47 | destroy_sequence: 48 | - destroy 49 | test_sequence: 50 | - dependency 51 | - verify ../shared/prerequisites.yml 52 | - destroy 53 | - syntax 54 | - create 55 | - prepare 56 | - check 57 | - converge 58 | - idempotence 59 | - verify 60 | - destroy 61 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/official-nginx-role-debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-nginx-hardening" with custom settings 3 | hosts: localhost 4 | vars: 5 | nginx_main_template_enable: true 6 | nginx_main_template: 7 | template_file: nginx.conf.j2 8 | conf_file_name: nginx.conf 9 | conf_file_location: /etc/nginx/ 10 | user: www-data 11 | worker_processes: auto 12 | error_level: warn 13 | worker_connections: 1024 14 | http_enable: true 15 | http_settings: 16 | keepalive_timeout: 65 17 | cache: false 18 | rate_limit: false 19 | keyval: false 20 | stream_enable: false 21 | http_global_autoindex: false 22 | roles: 23 | - nginxinc.nginx 24 | - nginx_hardening 25 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/official-nginx-role-redhat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-nginx-hardening" with custom settings 3 | hosts: localhost 4 | roles: 5 | - nginxinc.nginx 6 | - nginx_hardening 7 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare playbook for kitchen testing "ansible-nginx-hardening" with custom settings 3 | become: true 4 | hosts: all 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Install required packages 11 | ansible.builtin.package: 12 | name: python3-apt 13 | update_cache: true 14 | ignore_errors: true # noqa ignore-errors 15 | 16 | - name: Set correct distribution Version for Amazon Linux 17 | ansible.builtin.set_fact: 18 | ansible_distribution_major_version: 7 19 | when: ansible_distribution == 'Amazon' 20 | 21 | - name: Install nginx with a generic Ansible role 22 | ansible.builtin.include_role: 23 | name: geerlingguy.nginx 24 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | roles: 3 | - name: geerlingguy.nginx 4 | -------------------------------------------------------------------------------- /molecule/nginx_hardening/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Install procps for debian systems 11 | ansible.builtin.apt: 12 | name: procps 13 | state: present 14 | update_cache: true 15 | when: ansible_distribution == 'Debian' 16 | 17 | - name: Verify 18 | hosts: localhost 19 | environment: 20 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 21 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 22 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 23 | tasks: 24 | - name: Execute cinc-auditor tests 25 | ansible.builtin.command: > 26 | docker run 27 | --volume /run/docker.sock:/run/docker.sock 28 | docker.io/cincproject/auditor exec 29 | -t docker://instance 30 | --no-show-progress --no-color 31 | --no-distinct-exit https://github.com/dev-sec/nginx-baseline/archive/refs/heads/master.zip 32 | register: test_results 33 | changed_when: false 34 | ignore_errors: true 35 | 36 | - name: Display details about the cinc-auditor results 37 | ansible.builtin.debug: 38 | msg: "{{ test_results.stdout_lines }}" 39 | 40 | - name: Fail when tests fail 41 | ansible.builtin.fail: 42 | msg: Inspec failed to validate 43 | when: test_results.rc != 0 44 | -------------------------------------------------------------------------------- /molecule/os_hardening/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * Docker Engine 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ python3 -m pip install 'molecule[docker]' 23 | -------------------------------------------------------------------------------- /molecule/os_hardening/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: docker 4 | platforms: 5 | - name: instance 6 | image: rndmh3ro/docker-${MOLECULE_DISTRO}-ansible:latest 7 | command: ${MOLECULE_DOCKER_COMMAND:-/lib/systemd/systemd} 8 | volumes: 9 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 10 | privileged: true 11 | cgroupns_mode: host 12 | pre_build_image: true 13 | provisioner: 14 | name: ansible 15 | options: 16 | diff: true 17 | config_options: 18 | defaults: 19 | interpreter_python: auto_silent 20 | callbacks_enabled: profile_tasks, timer, yaml 21 | verifier: 22 | name: ansible 23 | 24 | scenario: 25 | create_sequence: 26 | - dependency 27 | - create 28 | - prepare 29 | check_sequence: 30 | - dependency 31 | - verify ../shared/prerequisites.yml 32 | - destroy 33 | - create 34 | - prepare 35 | - converge 36 | - check 37 | - destroy 38 | converge_sequence: 39 | - dependency 40 | - create 41 | - prepare 42 | - converge 43 | destroy_sequence: 44 | - destroy 45 | test_sequence: 46 | - dependency 47 | - verify ../shared/prerequisites.yml 48 | - destroy 49 | - syntax 50 | - create 51 | - prepare 52 | - check 53 | - converge 54 | - idempotence 55 | - verify 56 | - destroy 57 | -------------------------------------------------------------------------------- /molecule/os_hardening/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-os-hardening" with custom vars for testing 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Set ansible_python_interpreter to "/usr/bin/python3" on fedora 11 | ansible.builtin.set_fact: 12 | ansible_python_interpreter: /usr/bin/python3 13 | when: ansible_facts.distribution == 'Fedora' 14 | 15 | - name: Run the equivalent of "apt-get update && apt-get upgrade" 16 | ansible.builtin.apt: 17 | upgrade: safe 18 | update_cache: true 19 | when: ansible_os_family == 'Debian' 20 | 21 | - name: Install required tools on SuSE 22 | # cannot use zypper module, since it depends on python-xml 23 | ansible.builtin.command: zypper -n install awk 24 | changed_when: false 25 | when: ansible_facts.os_family == 'Suse' 26 | 27 | - name: Install required tools on fedora 28 | ansible.builtin.dnf: 29 | name: 30 | - python 31 | - findutils 32 | - procps-ng 33 | when: ansible_facts.distribution == 'Fedora' 34 | 35 | - name: Install required tools on Arch 36 | community.general.pacman: 37 | name: 38 | - awk 39 | state: present 40 | update_cache: true 41 | when: ansible_facts.os_family == 'Archlinux' 42 | 43 | - name: Install required tools on RHEL # noqa ignore-errors 44 | ansible.builtin.dnf: 45 | name: 46 | - openssh-clients 47 | - openssh 48 | state: present 49 | update_cache: true 50 | ignore_errors: true 51 | 52 | - name: Create recursing symlink to test minimize access 53 | ansible.builtin.shell: rm -f /usr/bin/zzz && ln -s /usr/bin /usr/bin/zzz 54 | changed_when: false 55 | 56 | - name: Include YUM prepare tasks 57 | ansible.builtin.include_tasks: prepare_tasks/yum.yml 58 | when: ansible_facts.os_family == 'RedHat' 59 | 60 | - name: Include preparation tasks 61 | ansible.builtin.include_tasks: 62 | file: "{{ item }}" 63 | loop: 64 | - prepare_tasks/netrc.yml 65 | - prepare_tasks/pw_ageing.yml 66 | - prepare_tasks/sys_account_shell.yml 67 | - prepare_tasks/ignore_home_folders.yml 68 | -------------------------------------------------------------------------------- /molecule/os_hardening/prepare_tasks/ignore_home_folders.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create user whose home-folder should not be touched 3 | ansible.builtin.user: 4 | name: user_with_777_home 5 | 6 | - name: Change mode of home-folder for user to 777 7 | ansible.builtin.file: 8 | path: /home/user_with_777_home 9 | mode: "0777" 10 | -------------------------------------------------------------------------------- /molecule/os_hardening/prepare_tasks/netrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create '.netrc' in /root 3 | ansible.builtin.copy: 4 | dest: /root/.netrc 5 | mode: "0600" 6 | content: | 7 | machine localhost 8 | login root 9 | password ipsum 10 | -------------------------------------------------------------------------------- /molecule/os_hardening/prepare_tasks/pw_ageing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create user those password ageing should not be changed 3 | user: 4 | name: pw_no_ageing 5 | password: $6$mysecretsalt$qJbapG68nyRab3gxvKWPUcs2g3t0oMHSHMnSKecYNpSi3CuZm.GbBqXO8BE6EI6P1JUefhA0qvD7b5LSh./PU1 6 | 7 | - name: Create user those password ageing should be changed 8 | user: 9 | name: pw_ageing 10 | password: $6$mysecretsalt$qJbapG68nyRab3gxvKWPUcs2g3t0oMHSHMnSKecYNpSi3CuZm.GbBqXO8BE6EI6P1JUefhA0qvD7b5LSh./PU1 11 | -------------------------------------------------------------------------------- /molecule/os_hardening/prepare_tasks/sys_account_shell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create system user with shell that should keep it 3 | user: 4 | name: shell_sys_acc 5 | shell: /bin/bash 6 | -------------------------------------------------------------------------------- /molecule/os_hardening/prepare_tasks/yum.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 'foo' repository 3 | ansible.builtin.yum_repository: 4 | name: foo 5 | description: mandatory description 6 | baseurl: file:///mandatory-url 7 | enabled: false 8 | gpgcheck: false 9 | 10 | - name: Create 'bar' repository 11 | ansible.builtin.yum_repository: 12 | name: bar 13 | description: mandatory description 14 | baseurl: file:///mandatory-url 15 | enabled: false 16 | gpgcheck: false 17 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Set ansible_python_interpreter to "/usr/bin/python3" 11 | ansible.builtin.set_fact: 12 | ansible_python_interpreter: /usr/bin/python3 13 | when: ansible_facts.distribution == 'Fedora' 14 | 15 | - name: Include verification tasks 16 | ansible.builtin.include_tasks: 17 | file: "{{ item }}" 18 | loop: 19 | - verify_tasks/sys_account_shell.yml 20 | - verify_tasks/pw_ageing.yml 21 | - verify_tasks/netrc.yml 22 | - verify_tasks/ignore_home_folders.yml 23 | - verify_tasks/ssh_auth_locked.yml 24 | 25 | # temp. disabled - https://github.com/dev-sec/ansible-collection-hardening/issues/690 26 | # - name: Include PAM tests 27 | # ansible.builtin.include_tasks: verify_tasks/pam.yml 28 | # when: ansible_facts.distribution in ['Debian', 'Ubuntu'] or ansible_facts.os_family == 'RedHat' 29 | 30 | - name: Include YUM tests 31 | ansible.builtin.include_tasks: verify_tasks/yum.yml 32 | when: ansible_facts.os_family == 'RedHat' 33 | 34 | - name: Verify 35 | hosts: localhost 36 | environment: 37 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 38 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 39 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 40 | tasks: 41 | - name: Execute cinc-auditor tests 42 | ansible.builtin.command: > 43 | docker run 44 | --volume /run/docker.sock:/run/docker.sock 45 | --volume {{ playbook_dir }}/waivers.yaml:/waivers.yaml 46 | docker.io/cincproject/auditor exec 47 | -t docker://instance 48 | --no-show-progress --no-color 49 | --waiver-file /waivers.yaml 50 | --no-distinct-exit https://github.com/dev-sec/linux-baseline/archive/refs/heads/master.zip 51 | register: test_results 52 | changed_when: false 53 | ignore_errors: true 54 | 55 | - name: Display details about the cinc-auditor results 56 | ansible.builtin.debug: 57 | msg: "{{ test_results.stdout_lines }}" 58 | 59 | - name: Fail when tests fail 60 | ansible.builtin.fail: 61 | msg: Inspec failed to validate 62 | when: test_results.rc != 0 63 | 64 | - name: Verify 65 | hosts: all 66 | become: true 67 | environment: 68 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 69 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 70 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 71 | vars: 72 | os_auditd_enabled: false 73 | os_env_umask: "027 #override" 74 | tasks: 75 | # test if variable can be overridden 76 | - name: Workaround for https://github.com/ansible/ansible/issues/66304 77 | ansible.builtin.set_fact: 78 | ansible_virtualization_type: docker 79 | 80 | - name: Include os_hardening role 81 | ansible.builtin.include_role: 82 | name: devsec.hardening.os_hardening 83 | 84 | - name: Verify os_env_umask 85 | ansible.builtin.command: 86 | cmd: "grep '027 #override' /etc/login.defs" 87 | changed_when: false 88 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify_tasks/ignore_home_folders.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get properties of user user_with_777_home's home-folder 3 | ansible.builtin.stat: 4 | path: /home/user_with_777_home 5 | register: stats 6 | 7 | - name: Check that the home-folder has mode 777 8 | ansible.builtin.assert: 9 | that: 10 | - stats.stat.mode == "0777" 11 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify_tasks/netrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test that .netrc in root homedir exists 3 | ansible.builtin.file: 4 | path: /root/.netrc 5 | state: file 6 | register: result_test_netrc 7 | 8 | - name: Output result if .netrc for user root exists 9 | ansible.builtin.assert: 10 | that: 11 | - result_test_netrc.state == 'file' 12 | fail_msg: .netrc in /root/ not present 13 | success_msg: .netrc exists in /root/ 14 | 15 | - name: Delete '.netrc' in /root 16 | ansible.builtin.file: 17 | path: /root/.netrc 18 | state: absent 19 | when: result_test_netrc.state == 'file' 20 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify_tasks/pam.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install pip 3 | package: 4 | name: 5 | - python3-pip 6 | - python3-setuptools 7 | state: present 8 | 9 | - name: install pam-tester 10 | ansible.builtin.pip: 11 | name: pam-tester 12 | state: present 13 | executable: /usr/bin/pip3 14 | 15 | - name: set password for test 16 | ansible.builtin.set_fact: 17 | test_pw: myTest!pw 18 | 19 | - name: set locale for test 20 | ansible.builtin.set_fact: 21 | locale: en_US.UTF-8 22 | when: 23 | - ansible_facts.os_family == 'RedHat' 24 | - ansible_facts.distribution_major_version < '8' 25 | 26 | - name: create testuser 27 | user: 28 | name: testuser 29 | password: "{{ test_pw | password_hash('sha512') }}" 30 | 31 | - name: check successful login with correct password 32 | ansible.builtin.shell: 33 | cmd: /usr/local/bin/pam-tester --user testuser --password {{ test_pw }} 34 | environment: 35 | TMPDIR: /var/tmp 36 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 37 | LANG: "{{ locale | default('C.UTF-8') }}" 38 | 39 | - name: check unsuccessful login with incorrect password 40 | ansible.builtin.shell: 41 | cmd: /usr/local/bin/pam-tester --user testuser --password {{ test_pw }}fail --expectfail 42 | environment: 43 | TMPDIR: /var/tmp 44 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 45 | LANG: "{{ locale | default('C.UTF-8') }}" 46 | with_sequence: count=6 47 | 48 | - name: check unsuccessful login, with correct password (lockout) 49 | ansible.builtin.shell: 50 | cmd: /usr/local/bin/pam-tester --user testuser --password {{ test_pw }} --expectfail 51 | environment: 52 | TMPDIR: /var/tmp 53 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 54 | LANG: "{{ locale | default('C.UTF-8') }}" 55 | 56 | - name: wait for account to unlock 57 | pause: 58 | seconds: 20 59 | 60 | - name: check successful login 61 | ansible.builtin.shell: 62 | cmd: /usr/local/bin/pam-tester --user testuser --password {{ test_pw }} 63 | environment: 64 | TMPDIR: /var/tmp 65 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 66 | LANG: "{{ locale | default('C.UTF-8') }}" 67 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify_tasks/pw_ageing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get Password Expiry date for use pw_no_ageing 3 | ansible.builtin.shell: chage -l pw_no_ageing | grep "Password expires" | cut -d ":" -f 2 4 | changed_when: false 5 | register: expiry_date 6 | 7 | - name: Check that the expiry date of pw_no_ageing is "never" 8 | ansible.builtin.assert: 9 | that: 10 | - expiry_date.stdout | trim == 'never' 11 | 12 | - name: Get Password Expiry date for pw_ageing 13 | ansible.builtin.shell: chage -l pw_ageing | grep "Password expires" | cut -d ":" -f 2 14 | changed_when: false 15 | register: expiry_date 16 | 17 | - name: Check that the expiry date of pw_ageing is 60 days 18 | ansible.builtin.assert: 19 | # this uses the date from the expire_date variable and subtracts the current date. 20 | # it should be bigger that the password_expire_min of the user "pw_no_ageing" 21 | that: 22 | - ( expiry_date.stdout | trim | to_datetime('%b %d, %Y') - ansible_date_time.date | to_datetime('%Y-%m-%d')).days == 60 23 | 24 | - name: Get Password Expiry warning days for pw_ageing 25 | ansible.builtin.shell: chage -l pw_ageing | grep "warning before password expires" | cut -d ":" -f 2 26 | changed_when: false 27 | register: expiry_warndays 28 | 29 | - name: Check that number of days of warning before password expires is 7 days 30 | ansible.builtin.assert: 31 | that: 32 | - expiry_warndays.stdout | trim == '7' 33 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify_tasks/ssh_auth_locked.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install tools 3 | package: 4 | name: "{{ item }}" 5 | state: present 6 | ignore_errors: true 7 | loop: 8 | - sshpass 9 | - openssh 10 | - openssh-clients 11 | - openssh-server 12 | 13 | - name: Allow password Login for sshd 14 | ansible.builtin.lineinfile: 15 | path: /etc/ssh/sshd_config 16 | search_string: PasswordAuthentication no 17 | line: PasswordAuthentication yes 18 | when: 19 | - ansible_facts.distribution == "Amazon" 20 | 21 | - name: Start sshd service 22 | ansible.builtin.service: 23 | name: "{{ item }}" 24 | state: started 25 | ignore_errors: true 26 | loop: 27 | - sshd 28 | - ssh 29 | 30 | - name: Set password for test 31 | ansible.builtin.set_fact: 32 | test_pw: myTest!pw 33 | 34 | - name: Create locked_user 35 | user: 36 | name: locked_user 37 | password: "{{ test_pw | password_hash('sha512') }}" 38 | 39 | - name: Create ssh-client-keypair 40 | community.crypto.openssh_keypair: 41 | path: /root/locked_user_id 42 | type: ed25519 43 | state: present 44 | register: generated_key 45 | 46 | - name: Add ssh-public-key to locked_user 47 | ansible.posix.authorized_key: 48 | user: locked_user 49 | key: "{{ generated_key.public_key }}" 50 | state: present 51 | 52 | - name: Check successful login with password 53 | ansible.builtin.shell: 54 | cmd: sshpass -p {{ test_pw }} ssh -o StrictHostKeyChecking=no locked_user@localhost echo "success" 55 | 56 | - name: Check successful login with ssh key 57 | ansible.builtin.shell: 58 | cmd: ssh -i /root/locked_user_id -o StrictHostKeyChecking=no locked_user@localhost echo "success" 59 | 60 | - name: Set password change date for locked_user 61 | ansible.builtin.shell: 62 | cmd: chage -d 2020-01-01 locked_user 63 | 64 | - name: Check unsuccessful login with password 65 | ansible.builtin.shell: 66 | cmd: sshpass -p {{ test_pw }} ssh -o StrictHostKeyChecking=no locked_user@localhost echo "success" 67 | register: output 68 | ignore_errors: true 69 | 70 | - name: Assert check unsuccessful login 71 | ansible.builtin.assert: 72 | that: 73 | - output.rc | int == 1 74 | - "'WARNING: Your password has expired.' in output.stderr" 75 | - "'success' not in output.stdout" 76 | when: 77 | - ansible_facts.os_family != "Suse" 78 | 79 | - name: Assert check unsuccessful login 80 | ansible.builtin.assert: 81 | that: 82 | - output.rc | int == 5 83 | - output.stderr | length == 0 84 | - output.stdout | length == 0 85 | when: 86 | - ansible_facts.os_family == "Suse" 87 | 88 | - name: Check successful login with ssh key 89 | ansible.builtin.shell: 90 | cmd: ssh -i /root/locked_user_id -o StrictHostKeyChecking=no locked_user@localhost echo "success" 91 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify_tasks/sys_account_shell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Read local linux user database 3 | getent: 4 | database: passwd 5 | 6 | - name: Check that shell_sys_acc's shell is still bash 7 | ansible.builtin.assert: 8 | that: 9 | - getent_passwd['shell_sys_acc'][5] == "/bin/bash" 10 | -------------------------------------------------------------------------------- /molecule/os_hardening/verify_tasks/yum.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 'gpgcheck' was not enabled for 'foo' repository (in whitelist) 3 | ansible.builtin.command: grep -e 'gpgcheck\s*=\s*0' /etc/yum.repos.d/foo.repo 4 | changed_when: false 5 | 6 | - name: Verify 'gpgcheck' was enabled for 'bar' repository (not in whitelist) 7 | ansible.builtin.command: grep -e 'gpgcheck\s*=\s*1' /etc/yum.repos.d/bar.repo 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/os_hardening/waivers.yaml: -------------------------------------------------------------------------------- 1 | os-14: 2 | run: false 3 | justification: "ansible does not gather all fact-mounts (e.g. mounts with tmpfs) so we cannot change these. see https://github.com/ansible/ansible/pull/79787" 4 | os-15: 5 | run: false 6 | justification: "ansible does not gather all fact-mounts (e.g. mounts with tmpfs) so we cannot change these. see https://github.com/ansible/ansible/pull/79787" 7 | os-16: 8 | run: false 9 | justification: "ansible does not gather all fact-mounts (e.g. mounts with tmpfs) so we cannot change these. see https://github.com/ansible/ansible/pull/79787" 10 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * Docker Engine 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ python3 -m pip install 'molecule[docker]' 23 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-os-hardening" with custom vars for testing 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Override for arch 11 | ansible.builtin.set_fact: 12 | os_mnt_boot_enabled: false 13 | os_mnt_tmp_enabled: true 14 | os_mnt_tmp_src: tmpfs 15 | os_mnt_tmp_filesystem: tmpfs 16 | when: ansible_facts.os_family == 'Archlinux' 17 | 18 | - name: Overrides for Fedora image 19 | ansible.builtin.set_fact: 20 | os_mnt_tmp_enabled: true 21 | os_mnt_tmp_src: tmpfs 22 | os_mnt_tmp_filesystem: tmpfs 23 | when: ansible_facts.distribution == 'Fedora' 24 | 25 | - name: Overrides for Fedora 40 image 26 | ansible.builtin.set_fact: 27 | os_mnt_var_enabled: true 28 | os_mnt_var_src: UUID=282c6d73-afc2-4113-9856-c7679ad51920 29 | os_mnt_var_filesystem: btrfs 30 | os_mnt_var_options: rw,nosuid,nodev,compress=zstd:1,subvol=var 31 | when: 32 | - ansible_facts.distribution == 'Fedora' 33 | - ansible_distribution_major_version|int == 40 34 | 35 | - name: Include os_hardening role 36 | ansible.builtin.include_role: 37 | name: devsec.hardening.os_hardening 38 | vars: 39 | os_auth_pam_passwdqc_enable: false 40 | os_auth_lockout_time: 15 41 | os_yum_repo_file_whitelist: [foo.repo] 42 | os_mnt_boot_enabled: true 43 | os_mnt_home_enabled: true 44 | os_mnt_boot_src: /dev/vda1 45 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | role-file: molecule/os_hardening_vm/requirements.yml 6 | driver: 7 | name: vagrant 8 | provider: 9 | name: libvirt 10 | platforms: 11 | # we need to name every instance differently to start multiple VMs on the same host (parallelization) 12 | # since we also need to use different OS users to run the tests because of how molecule operates, 13 | # the VM names must be predictable by OS user (to clean up canceled runs) 14 | - name: ${USER} 15 | box: ${MOLECULE_DISTRO} 16 | memory: 1024 17 | cpus: 2 18 | provisioner: 19 | name: ansible 20 | options: 21 | diff: true 22 | env: 23 | ANSIBLE_PIPELINING: "True" 24 | config_options: 25 | defaults: 26 | interpreter_python: auto_silent 27 | callbacks_enabled: profile_tasks, timer, yaml 28 | verifier: 29 | name: ansible 30 | env: 31 | ANSIBLE_PIPELINING: "True" 32 | 33 | scenario: 34 | create_sequence: 35 | - dependency 36 | - create 37 | - prepare 38 | check_sequence: 39 | - dependency 40 | - verify ../shared/prerequisites.yml 41 | - destroy 42 | - create 43 | - prepare 44 | - converge 45 | - check 46 | - destroy 47 | converge_sequence: 48 | - dependency 49 | - create 50 | - prepare 51 | - converge 52 | destroy_sequence: 53 | - destroy 54 | test_sequence: 55 | - dependency 56 | - verify ../shared/prerequisites.yml 57 | - destroy 58 | - syntax 59 | - create 60 | - prepare 61 | - check 62 | - converge 63 | - idempotence 64 | - verify 65 | - destroy 66 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare Archliux host 3 | hosts: all 4 | become: true 5 | gather_facts: false 6 | tasks: 7 | - name: Install python, since it's not installed by default 8 | ansible.builtin.raw: pacman --noconfirm -Sy python 9 | changed_when: false 10 | when: lookup('env', 'MOLECULE_DISTRO') == 'generic/arch' 11 | 12 | - name: Wrapper playbook for kitchen testing "ansible-os-hardening" with custom vars for testing 13 | hosts: all 14 | become: true 15 | environment: 16 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 17 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 18 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 19 | tasks: 20 | - name: Set ansible_python_interpreter to "/usr/bin/python3" on fedora 21 | ansible.builtin.set_fact: 22 | ansible_python_interpreter: /usr/bin/python3 23 | when: ansible_facts.distribution == 'Fedora' 24 | 25 | - name: Block update of Grub, because of error 26 | ansible.builtin.dpkg_selections: 27 | name: grub-pc 28 | selection: hold 29 | when: ansible_os_family == 'Debian' 30 | 31 | # we need to free up space, since the /boot partition in some Vagrant images is 32 | # pretty small and system updates might fail 33 | - name: Find all initrd.img to delete them 34 | ansible.builtin.find: 35 | paths: /boot 36 | patterns: "initrd.img*" 37 | register: find_results 38 | when: ansible_os_family == 'Debian' 39 | 40 | - name: Delete all initrd.img to free space on /boot 41 | ansible.builtin.file: 42 | path: "{{ item['path'] }}" 43 | state: absent 44 | with_items: "{{ find_results['files'] }}" 45 | when: ansible_os_family == 'Debian' 46 | 47 | - name: Run the equivalent of "apt-get update && apt-get upgrade" 48 | ansible.builtin.apt: 49 | upgrade: safe 50 | update_cache: true 51 | when: ansible_os_family == 'Debian' 52 | 53 | - name: Install required tools on fedora 54 | ansible.builtin.dnf: 55 | name: 56 | - python 57 | - findutils 58 | - procps-ng 59 | - python3-libselinux 60 | when: ansible_facts.distribution == 'Fedora' 61 | 62 | - name: Install required tools on Arch 63 | community.general.pacman: 64 | name: 65 | - awk 66 | state: present 67 | update_cache: true 68 | when: ansible_facts.os_family == 'Archlinux' 69 | 70 | - name: Install required tools on RHEL # noqa ignore-errors 71 | ansible.builtin.dnf: 72 | name: 73 | - openssh-clients 74 | - openssh 75 | state: present 76 | update_cache: true 77 | ignore_errors: true 78 | 79 | - name: Create recursing symlink to test minimize access 80 | ansible.builtin.shell: rm -f /usr/bin/zzz && ln -s /usr/bin /usr/bin/zzz 81 | changed_when: false 82 | 83 | - name: Unmount EFI partition to get rid of vfat filesystem (qemu has no firmware image that inspec can detect) 84 | ansible.posix.mount: 85 | path: /boot/efi 86 | state: unmounted 87 | when: ansible_facts.distribution == 'Fedora' 88 | 89 | - name: Include YUM prepare tasks 90 | ansible.builtin.include_tasks: prepare_tasks/yum.yml 91 | when: ansible_facts.os_family == 'RedHat' 92 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/prepare_tasks/yum.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 'foo' repository 3 | ansible.builtin.yum_repository: 4 | name: foo 5 | description: mandatory description 6 | baseurl: file:///mandatory-url 7 | enabled: false 8 | gpgcheck: false 9 | 10 | - name: Create 'bar' repository 11 | ansible.builtin.yum_repository: 12 | name: bar 13 | description: mandatory description 14 | baseurl: file:///mandatory-url 15 | enabled: false 16 | gpgcheck: false 17 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | roles: 3 | - name: geerlingguy.git 4 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | # temp. disabled - https://github.com/dev-sec/ansible-collection-hardening/issues/690 11 | # - name: Include PAM tests 12 | # ansible.builtin.include_tasks: verify_tasks/pam.yml 13 | # when: ansible_facts.distribution in ['Debian', 'Ubuntu'] or ansible_facts.os_family == 'RedHat' 14 | 15 | - name: Include YUM tests 16 | ansible.builtin.include_tasks: verify_tasks/yum.yml 17 | when: ansible_facts.os_family == 'RedHat' 18 | 19 | - name: Verify 20 | hosts: localhost 21 | environment: 22 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 23 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 24 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 25 | tasks: 26 | - name: Get ssh-config 27 | ansible.builtin.command: 28 | cmd: vagrant ssh-config 29 | chdir: "{{ molecule_ephemeral_directory }}" 30 | register: ssh_config 31 | changed_when: false 32 | 33 | - name: Create ssh-config file 34 | ansible.builtin.copy: 35 | content: "{{ ssh_config.stdout_lines | join ('\n') }}" 36 | dest: "{{ molecule_ephemeral_directory }}/ssh-config" 37 | mode: "0400" 38 | changed_when: false 39 | 40 | - name: Execute cinc-auditor tests 41 | ansible.builtin.command: > 42 | docker run --rm 43 | --volume {{ molecule_ephemeral_directory }}:{{ molecule_ephemeral_directory }} 44 | docker.io/cincproject/auditor exec 45 | --ssh-config-file={{ molecule_ephemeral_directory }}/ssh-config 46 | -t ssh://{{ lookup('env', 'USER') }} 47 | --sudo --no-show-progress --no-color 48 | --no-distinct-exit https://github.com/dev-sec/linux-baseline/archive/refs/heads/master.zip 49 | register: test_results 50 | changed_when: false 51 | ignore_errors: true 52 | 53 | - name: Display details about the cinc-auditor results 54 | ansible.builtin.debug: 55 | msg: "{{ test_results.stdout_lines }}" 56 | 57 | - name: Fail when tests fail 58 | ansible.builtin.fail: 59 | msg: Inspec failed to validate 60 | when: test_results.rc != 0 61 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/verify_tasks/pam.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install pip 3 | package: 4 | name: 5 | - python3-pip 6 | - python3-setuptools 7 | state: present 8 | 9 | - name: install pam-tester 10 | ansible.builtin.pip: 11 | name: pam-tester 12 | state: present 13 | 14 | - name: set password for test 15 | ansible.builtin.set_fact: 16 | test_pw: myTest!pw 17 | 18 | - name: set locale for test 19 | ansible.builtin.set_fact: 20 | locale: en_US.UTF-8 21 | when: 22 | - ansible_facts.os_family == 'RedHat' 23 | - ansible_facts.distribution_major_version < '8' 24 | 25 | - name: create testuser 26 | user: 27 | name: testuser 28 | password: "{{ test_pw | password_hash('sha512') }}" 29 | 30 | - name: check successful login with correct password 31 | ansible.builtin.shell: 32 | cmd: pam-tester --user testuser --password {{ test_pw }} 33 | environment: 34 | TMPDIR: /var/tmp 35 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 36 | LANG: "{{ locale | default('C.UTF-8') }}" 37 | 38 | - name: check unsuccessful login with incorrect password 39 | ansible.builtin.shell: 40 | cmd: pam-tester --user testuser --password {{ test_pw }}fail --expectfail 41 | environment: 42 | TMPDIR: /var/tmp 43 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 44 | LANG: "{{ locale | default('C.UTF-8') }}" 45 | with_sequence: count=6 46 | 47 | - name: check unsuccessful login, with correct password (lockout) 48 | ansible.builtin.shell: 49 | cmd: pam-tester --user testuser --password {{ test_pw }} --expectfail 50 | environment: 51 | TMPDIR: /var/tmp 52 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 53 | LANG: "{{ locale | default('C.UTF-8') }}" 54 | 55 | - name: wait for account to unlock 56 | pause: 57 | seconds: 20 58 | 59 | - name: check successful login 60 | ansible.builtin.shell: 61 | cmd: pam-tester --user testuser --password {{ test_pw }} 62 | environment: 63 | TMPDIR: /var/tmp 64 | LC_ALL: "{{ locale | default('C.UTF-8') }}" 65 | LANG: "{{ locale | default('C.UTF-8') }}" 66 | -------------------------------------------------------------------------------- /molecule/os_hardening_vm/verify_tasks/yum.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 'gpgcheck' was not enabled for 'foo' repository (in whitelist) 3 | ansible.builtin.command: grep -e 'gpgcheck\s*=\s*0' /etc/yum.repos.d/foo.repo 4 | changed_when: false 5 | 6 | - name: Verify 'gpgcheck' was enabled for 'bar' repository (not in whitelist) 7 | ansible.builtin.command: grep -e 'gpgcheck\s*=\s*1' /etc/yum.repos.d/bar.repo 8 | changed_when: false 9 | -------------------------------------------------------------------------------- /molecule/shared/prerequisites.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify prerequisites 3 | hosts: localhost 4 | tasks: 5 | - name: Make sure environment variable for MOLECULE_DISTRO is set 6 | ansible.builtin.assert: 7 | that: 8 | - "lookup('env','MOLECULE_DISTRO')" 9 | fail_msg: "You need to set MOLECULE_DISTRO to a supported image name. See CONTRIBUTING.md" 10 | -------------------------------------------------------------------------------- /molecule/ssh_hardening/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * Docker Engine 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ python3 -m pip install 'molecule[docker]' 23 | -------------------------------------------------------------------------------- /molecule/ssh_hardening/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-ssh-hardening" with default settings 3 | hosts: all 4 | environment: 5 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 6 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 7 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 8 | tasks: 9 | - name: Include ssh_hardening role 10 | ansible.builtin.include_role: 11 | name: devsec.hardening.ssh_hardening 12 | vars: 13 | sftp_enabled: false 14 | -------------------------------------------------------------------------------- /molecule/ssh_hardening/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: docker 4 | platforms: 5 | - name: instance 6 | image: rndmh3ro/docker-${MOLECULE_DISTRO}-ansible:latest 7 | command: ${MOLECULE_DOCKER_COMMAND:-/lib/systemd/systemd} 8 | volumes: 9 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 10 | privileged: true 11 | cgroupns_mode: host 12 | pre_build_image: true 13 | provisioner: 14 | name: ansible 15 | options: 16 | diff: true 17 | config_options: 18 | defaults: 19 | interpreter_python: auto_silent 20 | callbacks_enabled: profile_tasks, timer, yaml 21 | inventory: 22 | host_vars: 23 | # https://molecule.readthedocs.io/en/latest/examples.html#docker-with-non-privileged-user 24 | # setting for the platform instance named 'instance' 25 | instance: 26 | ansible_user: ansible 27 | verifier: 28 | name: ansible 29 | 30 | scenario: 31 | create_sequence: 32 | - dependency 33 | - create 34 | - prepare 35 | check_sequence: 36 | - dependency 37 | - verify ../shared/prerequisites.yml 38 | - destroy 39 | - create 40 | - prepare 41 | - converge 42 | - check 43 | - destroy 44 | converge_sequence: 45 | - dependency 46 | - create 47 | - prepare 48 | - converge 49 | destroy_sequence: 50 | - destroy 51 | test_sequence: 52 | - dependency 53 | - verify ../shared/prerequisites.yml 54 | - destroy 55 | - syntax 56 | - create 57 | - prepare 58 | - check 59 | - converge 60 | - idempotence 61 | - verify 62 | - destroy 63 | -------------------------------------------------------------------------------- /molecule/ssh_hardening/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare the molecule container for the role 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Use python3 11 | ansible.builtin.set_fact: 12 | ansible_python_interpreter: /usr/bin/python3 13 | when: ansible_facts.distribution == 'Fedora' 14 | 15 | - name: Install packages # noqa ignore-errors 16 | ansible.builtin.dnf: 17 | name: 18 | - openssh-clients 19 | - openssh-server 20 | - libselinux-python 21 | state: present 22 | update_cache: true 23 | ignore_errors: true 24 | 25 | - name: Install packages # noqa ignore-errors 26 | ansible.builtin.dnf: 27 | name: 28 | - openssh-clients 29 | - openssh-server 30 | - procps-ng 31 | state: present 32 | update_cache: true 33 | ignore_errors: true 34 | 35 | - name: Install packages # noqa ignore-errors 36 | ansible.builtin.apt: 37 | name: 38 | - openssh-client 39 | - openssh-server 40 | state: present 41 | update_cache: true 42 | ignore_errors: true 43 | 44 | - name: Install required tools on SuSE 45 | # cannot use zypper module, since it depends on python-xml 46 | ansible.builtin.command: zypper -n install python-xml 47 | changed_when: false 48 | when: ansible_facts.os_family == 'Suse' 49 | 50 | - name: Install packages 51 | community.general.zypper: 52 | name: 53 | - openssh 54 | when: ansible_facts.os_family == 'Suse' 55 | 56 | - name: Install required tools on Alpine 57 | community.general.apk: 58 | name: 59 | - openssh 60 | state: present 61 | update_cache: true 62 | when: ansible_facts.os_family == 'Alpine' 63 | 64 | - name: Install required tools on Arch 65 | community.general.pacman: 66 | name: 67 | - openssh 68 | - awk 69 | state: present 70 | update_cache: true 71 | when: ansible_facts.os_family == 'Archlinux' 72 | 73 | - name: Create ssh host keys # noqa ignore-errors 74 | ansible.builtin.command: ssh-keygen -A 75 | when: not ((ansible_facts.os_family in ['Oracle Linux', 'RedHat']) and ansible_facts.distribution_major_version < '7') 76 | or ansible_facts.distribution == "Fedora" 77 | or ansible_facts.distribution == "Amazon" 78 | or ansible_facts.os_family == "Suse" 79 | changed_when: false 80 | ignore_errors: true 81 | -------------------------------------------------------------------------------- /molecule/ssh_hardening/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: localhost 4 | environment: 5 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 6 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 7 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 8 | tasks: 9 | - name: Execute cinc-auditor tests 10 | ansible.builtin.command: > 11 | docker run 12 | --volume /run/docker.sock:/run/docker.sock 13 | docker.io/cincproject/auditor exec 14 | -t docker://instance 15 | --no-show-progress --no-color 16 | --no-distinct-exit https://github.com/dev-sec/ssh-baseline/archive/refs/heads/master.zip 17 | register: test_results 18 | changed_when: false 19 | ignore_errors: true 20 | 21 | - name: Display details about the cinc-auditor results 22 | ansible.builtin.debug: 23 | msg: "{{ test_results.stdout_lines }}" 24 | 25 | - name: Fail when tests fail 26 | ansible.builtin.fail: 27 | msg: Inspec failed to validate 28 | when: test_results.rc != 0 29 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * Docker Engine 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ python3 -m pip install 'molecule[docker]' 23 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-ssh-hardening" with default settings 3 | hosts: all 4 | environment: 5 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 6 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 7 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 8 | tasks: 9 | - name: Include ssh_hardening role 10 | ansible.builtin.include_role: 11 | name: devsec.hardening.ssh_hardening 12 | vars: 13 | sftp_enabled: false 14 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: vagrant 4 | provider: 5 | name: libvirt 6 | platforms: 7 | # we need to name every instance differently to start multiple VMs on the same host (parallelization) 8 | # since we also need to use different OS users to run the tests because of how molecule operates, 9 | # the VM names must be predictable by OS user (to clean up canceled runs) 10 | - name: ${USER} 11 | box: generic/${MOLECULE_DISTRO} 12 | memory: 1024 13 | cpus: 2 14 | provisioner: 15 | name: ansible 16 | options: 17 | diff: true 18 | env: 19 | ANSIBLE_PIPELINING: "True" 20 | config_options: 21 | defaults: 22 | interpreter_python: auto_silent 23 | callbacks_enabled: profile_tasks, timer, yaml 24 | verifier: 25 | name: ansible 26 | env: 27 | ANSIBLE_PIPELINING: "True" 28 | 29 | scenario: 30 | create_sequence: 31 | - dependency 32 | - create 33 | - prepare 34 | check_sequence: 35 | - dependency 36 | - verify ../shared/prerequisites.yml 37 | - destroy 38 | - create 39 | - prepare 40 | - converge 41 | - check 42 | - destroy 43 | converge_sequence: 44 | - dependency 45 | - create 46 | - prepare 47 | - converge 48 | destroy_sequence: 49 | - destroy 50 | test_sequence: 51 | - dependency 52 | - verify ../shared/prerequisites.yml 53 | - destroy 54 | - syntax 55 | - create 56 | - prepare 57 | - converge 58 | - idempotence 59 | - verify 60 | - destroy 61 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare OpenBSD host 3 | hosts: all 4 | become: true 5 | gather_facts: false 6 | tasks: 7 | - name: Change installation source 8 | # temporary fix for https://github.com/lavabit/robox/issues/305 9 | ansible.builtin.raw: echo "https://ftp.eu.openbsd.org/pub/OpenBSD" > /etc/installurl 10 | changed_when: false 11 | when: lookup('env', 'MOLECULE_DISTRO') == 'openbsd7' 12 | 13 | - name: Install python 14 | # BSDs are special for Ansible - https://docs.ansible.com/ansible/latest/os_guide/intro_bsd.html 15 | ansible.builtin.raw: pkg_add python%3.10 16 | changed_when: false 17 | when: lookup('env', 'MOLECULE_DISTRO') == 'openbsd7' 18 | 19 | - name: Wrapper playbook for kitchen testing "ansible-ssh-hardening" with default settings 20 | hosts: all 21 | become: true 22 | environment: 23 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 24 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 25 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 26 | tasks: 27 | - name: Create ssh host keys # noqa ignore-errors 28 | ansible.builtin.command: ssh-keygen -A 29 | when: not ((ansible_facts.os_family in ['Oracle Linux', 'RedHat']) and ansible_facts.distribution_major_version < '7') 30 | or ansible_facts.distribution == "Fedora" 31 | or ansible_facts.distribution == "Amazon" 32 | or ansible_facts.os_family == "Suse" 33 | changed_when: false 34 | ignore_errors: true 35 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: OpenBSD workaround - inspec detects OpenSBD as unix and not linux compatible 3 | hosts: all 4 | become: true 5 | tasks: 6 | - name: Use the type command instead of which to detect existing commands 7 | ansible.builtin.file: 8 | src: /usr/bin/which 9 | dest: /usr/bin/type 10 | state: hard 11 | mode: "0770" 12 | when: lookup('env', 'MOLECULE_DISTRO') == 'openbsd7' 13 | 14 | - name: Verify 15 | hosts: localhost 16 | environment: 17 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 18 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 19 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 20 | tasks: 21 | - name: Get ssh-config 22 | ansible.builtin.command: 23 | cmd: vagrant ssh-config 24 | chdir: "{{ molecule_ephemeral_directory }}" 25 | register: ssh_config 26 | changed_when: false 27 | 28 | - name: Create ssh-config file 29 | ansible.builtin.copy: 30 | content: "{{ ssh_config.stdout_lines | join ('\n') }}" 31 | dest: "{{ molecule_ephemeral_directory }}/ssh-config" 32 | mode: "0400" 33 | changed_when: false 34 | 35 | - name: Execute cinc-auditor tests 36 | ansible.builtin.command: > 37 | docker run --rm 38 | --volume {{ molecule_ephemeral_directory }}:{{ molecule_ephemeral_directory }} 39 | --volume ./waivers_{{ lookup('env', 'MOLECULE_DISTRO') }}.yaml:/waivers.yaml 40 | docker.io/cincproject/auditor exec 41 | --ssh-config-file={{ molecule_ephemeral_directory }}/ssh-config 42 | -t ssh://{{ lookup('env', 'USER') }} 43 | --sudo --no-show-progress --no-color 44 | --waiver-file /waivers.yaml 45 | --no-distinct-exit https://github.com/dev-sec/ssh-baseline/archive/refs/heads/master.zip 46 | register: test_results 47 | changed_when: false 48 | ignore_errors: true 49 | 50 | - name: Display details about the cinc-auditor results 51 | ansible.builtin.debug: 52 | msg: "{{ test_results.stdout_lines }}" 53 | 54 | - name: Fail when tests fail 55 | ansible.builtin.fail: 56 | msg: Inspec failed to validate 57 | when: test_results.rc != 0 58 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/waivers_freebsd13.yaml: -------------------------------------------------------------------------------- 1 | sshd-45: 2 | run: false 3 | justification: "PrintLastLog is broken on FreeBSD. see: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=209441" 4 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/waivers_freebsd14.yaml: -------------------------------------------------------------------------------- 1 | sshd-45: 2 | run: false 3 | justification: "PrintLastLog is broken on FreeBSD. see: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=209441" 4 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_bsd/waivers_openbsd7.yaml: -------------------------------------------------------------------------------- 1 | ssh-17: 2 | run: false 3 | justification: "GSSAPIAuthentication is disabled. see: https://github.com/dev-sec/ansible-collection-hardening/pull/598" 4 | ssh-18: 5 | run: false 6 | justification: "GSSAPIDelegateCredentials is disabled. see: https://github.com/dev-sec/ansible-collection-hardening/pull/598" 7 | sshd-30: 8 | run: false 9 | justification: "KerberosAuthentication is disabled. see: https://github.com/dev-sec/ansible-ssh-hardening/pull/171" 10 | sshd-31: 11 | run: false 12 | justification: "KerberosOrLocalPasswd is disabled. see: https://github.com/dev-sec/ansible-ssh-hardening/pull/171" 13 | sshd-32: 14 | run: false 15 | justification: "KerberosTicketCleanup is disabled. see: https://github.com/dev-sec/ansible-ssh-hardening/pull/171" 16 | sshd-33: 17 | run: false 18 | justification: "GSSAPIAuthentication is disabled. see: https://github.com/dev-sec/ansible-collection-hardening/pull/598" 19 | sshd-34: 20 | run: false 21 | justification: "GSSAPICleanupCredentials is disabled. see: https://github.com/dev-sec/ansible-collection-hardening/pull/598" 22 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_custom_tests/INSTALL.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Docker driver installation guide 3 | ******* 4 | 5 | Requirements 6 | ============ 7 | 8 | * Docker Engine 9 | 10 | Install 11 | ======= 12 | 13 | Please refer to the `Virtual environment`_ documentation for installation best 14 | practices. If not using a virtual environment, please consider passing the 15 | widely recommended `'--user' flag`_ when invoking ``pip``. 16 | 17 | .. _Virtual environment: https://virtualenv.pypa.io/en/latest/ 18 | .. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site 19 | 20 | .. code-block:: bash 21 | 22 | $ python3 -m pip install 'molecule[docker]' 23 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_custom_tests/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-ssh-hardening" with custom settings 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Include ssh_hardening role 11 | ansible.builtin.include_role: 12 | name: devsec.hardening.ssh_hardening 13 | vars: 14 | ssh_trusted_user_ca_keys_file: /etc/ssh/ca.pub 15 | ssh_trusted_user_ca_keys: 16 | - "# ssh-rsa ..." 17 | ssh_authorized_principals_file: /etc/ssh/auth_principals/%u 18 | ssh_authorized_principals: 19 | - path: /etc/ssh/auth_principals/root 20 | principals: 21 | - root 22 | network_ipv6_enable: true 23 | ssh_allow_tcp_forwarding: "yes" 24 | ssh_gateway_ports: "clientspecified" 25 | ssh_allow_agent_forwarding: true 26 | ssh_server_permit_environment_vars: "yes" 27 | ssh_server_accept_env_vars: PWD HTTP_PROXY 28 | ssh_client_alive_interval: 100 29 | ssh_client_alive_count: 10 30 | ssh_client_password_login: true 31 | ssh_challengeresponseauthentication: true 32 | ssh_compression: true 33 | ssh_allow_users: root kitchen vagrant 34 | ssh_allow_groups: root kitchen vagrant 35 | ssh_deny_users: foo bar 36 | ssh_deny_groups: foo bar 37 | ssh_authorized_keys_file: /etc/ssh/authorized_keys/%u 38 | ssh_max_auth_retries: 10 39 | ssh_permit_root_login: without-password 40 | ssh_permit_tunnel: "yes" 41 | ssh_print_motd: true 42 | ssh_print_last_log: true 43 | ssh_banner: true 44 | ssh_server_password_login: true 45 | sftp_chroot: true 46 | # ssh_server_enabled: false 47 | ssh_server_ports: 48 | - 22 49 | - 222 50 | ssh_server_match_address: 51 | - address: 192.168.1.0/24 52 | rules: 53 | - AllowTcpForwarding yes 54 | - AllowAgentForwarding no 55 | ssh_server_match_group: 56 | - group: root 57 | rules: 58 | - AllowTcpForwarding yes 59 | - AllowAgentForwarding no 60 | ssh_server_match_user: 61 | - user: root 62 | rules: 63 | - AllowTcpForwarding yes 64 | - AllowAgentForwarding no 65 | ssh_server_match_local_port: 66 | - port: 222 67 | rules: 68 | - AllowTcpForwarding yes 69 | - AllowAgentForwarding no 70 | ssh_remote_hosts: 71 | - names: [example.com, example2.com] 72 | options: [Port 2222, ForwardAgent yes] 73 | - names: [example3.com] 74 | options: [StrictHostKeyChecking no] 75 | ssh_use_dns: true 76 | ssh_use_pam: true 77 | ssh_max_startups: 10:30:60 78 | ssh_host_key_algorithms: 79 | - ssh-ed25519 80 | - rsa-sha2-512 81 | - rsa-sha2-256 82 | - ssh-rsa 83 | ssh_macs: 84 | - hmac-sha2-512 85 | - hmac-sha2-256 86 | ssh_ciphers: 87 | - aes256-ctr 88 | - aes192-ctr 89 | - aes128-ctr 90 | - aes256-cbc 91 | # these should not be set in the final sshd_config 92 | ssh_kex: false 93 | ssh_custom_options: 94 | - Include /etc/ssh/ssh_config.d/* 95 | sshd_custom_options: 96 | - AcceptEnv LANG 97 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_custom_tests/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: docker 4 | platforms: 5 | - name: instance 6 | image: rndmh3ro/docker-${MOLECULE_DISTRO}-ansible:latest 7 | command: ${MOLECULE_DOCKER_COMMAND:-/lib/systemd/systemd} 8 | volumes: 9 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 10 | privileged: true 11 | cgroupns_mode: host 12 | pre_build_image: true 13 | provisioner: 14 | name: ansible 15 | options: 16 | diff: true 17 | config_options: 18 | defaults: 19 | interpreter_python: auto_silent 20 | callbacks_enabled: profile_tasks, timer, yaml 21 | verifier: 22 | name: ansible 23 | 24 | scenario: 25 | create_sequence: 26 | - dependency 27 | - create 28 | - prepare 29 | check_sequence: 30 | - dependency 31 | - verify ../shared/prerequisites.yml 32 | - destroy 33 | - create 34 | - prepare 35 | - converge 36 | - check 37 | - destroy 38 | converge_sequence: 39 | - dependency 40 | - create 41 | - prepare 42 | - converge 43 | destroy_sequence: 44 | - destroy 45 | test_sequence: 46 | - dependency 47 | - verify ../shared/prerequisites.yml 48 | - destroy 49 | - syntax 50 | - create 51 | - prepare 52 | - check 53 | - converge 54 | - idempotence 55 | - destroy 56 | -------------------------------------------------------------------------------- /molecule/ssh_hardening_custom_tests/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Wrapper playbook for kitchen testing "ansible-ssh-hardening" with default settings 3 | hosts: all 4 | become: true 5 | environment: 6 | http_proxy: "{{ lookup('env', 'http_proxy') | default(omit) }}" 7 | https_proxy: "{{ lookup('env', 'https_proxy') | default(omit) }}" 8 | no_proxy: "{{ lookup('env', 'no_proxy') | default(omit) }}" 9 | tasks: 10 | - name: Use python3 11 | ansible.builtin.set_fact: 12 | ansible_python_interpreter: /usr/bin/python3 13 | when: ansible_facts.distribution == 'Fedora' 14 | 15 | - name: Install packages # noqa ignore-errors 16 | ansible.builtin.dnf: 17 | name: 18 | - openssh-clients 19 | - openssh-server 20 | - libselinux-python 21 | state: present 22 | update_cache: true 23 | ignore_errors: true 24 | 25 | - name: Install packages # noqa ignore-errors 26 | ansible.builtin.dnf: 27 | name: 28 | - openssh-clients 29 | - openssh-server 30 | - procps-ng 31 | state: present 32 | update_cache: true 33 | ignore_errors: true 34 | 35 | - name: Install packages # noqa ignore-errors 36 | ansible.builtin.apt: 37 | name: 38 | - openssh-client 39 | - openssh-server 40 | state: present 41 | update_cache: true 42 | ignore_errors: true 43 | 44 | - name: Install required tools on SuSE 45 | # cannot use zypper module, since it depends on python-xml 46 | ansible.builtin.command: zypper -n install python-xml 47 | changed_when: false 48 | when: ansible_facts.os_family == 'Suse' 49 | 50 | - name: Install packages 51 | community.general.zypper: 52 | name: 53 | - openssh 54 | when: ansible_facts.os_family == 'Suse' 55 | 56 | - name: Install required tools on Arch 57 | community.general.pacman: 58 | name: 59 | - openssh 60 | - awk 61 | state: present 62 | update_cache: true 63 | when: ansible_facts.os_family == 'Archlinux' 64 | 65 | - name: Install required tools on Alpine 66 | community.general.apk: 67 | name: 68 | - openssh 69 | state: present 70 | update_cache: true 71 | when: ansible_facts.os_family == 'Alpine' 72 | 73 | - name: Create ssh host keys # noqa ignore-errors 74 | ansible.builtin.command: ssh-keygen -A 75 | when: not ((ansible_facts.os_family in ['Oracle Linux', 'RedHat']) and ansible_facts.distribution_major_version < '7') 76 | or ansible_facts.distribution == "Fedora" 77 | or ansible_facts.distribution == "Amazon" 78 | or ansible_facts.os_family == "Suse" 79 | changed_when: false 80 | ignore_errors: true 81 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:best-practices", 5 | ":gitSignOff" 6 | ], 7 | "dependencyDashboard": true, 8 | "dependencyDashboardAutoclose": true, 9 | "packageRules": [ 10 | { 11 | "matchUpdateTypes": ["patch", "minor"], 12 | "automerge": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | molecule==25.1.0 2 | molecule-plugins[docker]==23.7.0 3 | ansible-core==2.18.6 4 | docker==7.1.0 5 | jmespath==1.0.1 6 | aar-doc==2.1.0 7 | passlib==1.7.4 8 | -------------------------------------------------------------------------------- /roles/mysql_hardening/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # switcher to enable/disable role 3 | mysql_hardening_enabled: true 4 | 5 | mysql_daemon_enabled: true 6 | 7 | mysql_hardening_restart_mysql: true 8 | 9 | # You have to change this to your own strong enough mysql root password 10 | mysql_root_password: "-----====>SetR00tPa$$wordH3r3!!!<====-----" 11 | # There .my.cnf with mysql root credentials will be installed 12 | mysql_user_home: "{{ ansible_env.HOME }}" 13 | 14 | # ensure the following parameters are set properly 15 | mysql_remove_remote_root: true 16 | mysql_remove_anonymous_users: true 17 | mysql_remove_test_database: true 18 | 19 | # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_skip-show-database 20 | mysql_hardening_skip_show_database: true 21 | 22 | # @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_skip-grant-tables 23 | mysql_hardening_skip_grant_tables: false 24 | 25 | # @see http://www.symantec.com/connect/articles/securing-mysql-step-step 26 | # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_chroot 27 | mysql_hardening_chroot: "" 28 | 29 | mysql_hardening_options: 30 | # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_safe-user-create 31 | safe-user-create: 1 32 | 33 | # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option-mysqld-secure-auth 34 | secure-auth: 1 35 | 36 | # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option-mysqld-symbolic-links 37 | skip-symbolic-links: 1 38 | 39 | # @see http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar-local-infile 40 | local-infile: 0 41 | 42 | # @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option-mysqld-allow-suspicious-udfs 43 | allow-suspicious-udfs: 0 44 | 45 | # @see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar-automatic-sp-privileges 46 | automatic-sp-privileges: 0 47 | 48 | # @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option-mysqld-secure-file-priv 49 | secure-file-priv: /tmp 50 | # @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_user 51 | user: "{{ mysql_hardening_user }}" 52 | -------------------------------------------------------------------------------- /roles/mysql_hardening/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart mysql 3 | ansible.builtin.service: 4 | name: "{{ mysql_daemon }}" 5 | state: restarted 6 | when: mysql_hardening_restart_mysql | bool 7 | -------------------------------------------------------------------------------- /roles/mysql_hardening/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Sebastian Gumprich 4 | description: This Ansible playbook provides security configuration for mysql. 5 | company: Hardening Framework Team 6 | license: Apache License 2.0 7 | min_ansible_version: 2.9.10 8 | platforms: 9 | - name: EL 10 | versions: 11 | - "8" 12 | - "9" 13 | - name: Ubuntu 14 | versions: 15 | - focal 16 | - jammy 17 | - noble 18 | - name: Debian 19 | versions: 20 | - bullseye 21 | - bookworm 22 | - name: Amazon 23 | - name: opensuse 24 | galaxy_tags: 25 | - system 26 | - security 27 | - hardening 28 | - database 29 | - mysql 30 | dependencies: [] 31 | -------------------------------------------------------------------------------- /roles/mysql_hardening/tasks/configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Protect my.cnf 3 | ansible.builtin.file: 4 | path: "{{ mysql_hardening_mysql_conf_file }}" 5 | mode: "0640" 6 | owner: "{{ mysql_cnf_owner }}" 7 | group: "{{ mysql_cnf_group }}" 8 | follow: true 9 | state: file 10 | 11 | - name: Ensure permissions on mysql-datadir are correct 12 | ansible.builtin.file: 13 | path: "{{ item }}" 14 | state: directory 15 | owner: "{{ mysql_hardening_user }}" 16 | group: "{{ mysql_hardening_user }}" 17 | mode: "0750" 18 | when: item is defined and item != "" 19 | loop: 20 | - "{{ mysql_settings.settings.datadir }}" 21 | - '{{ mysql_datadir | default("") }}' 22 | 23 | - name: Ensure permissions on mysql-logfile are correct 24 | ansible.builtin.file: 25 | path: "{{ item }}" 26 | state: file 27 | owner: "{{ mysql_hardening_user }}" 28 | group: "{{ mysql_hardening_group }}" 29 | mode: "0640" 30 | when: 31 | - item is defined 32 | - item != "" 33 | - item != "stderr" 34 | - item != "stdout" 35 | loop: 36 | - "{{ mysql_settings.settings.log_error }}" 37 | - '{{ mysql_hardening_log_file | default("") }}' 38 | 39 | - name: Check mysql configuration-directory exists and has right permissions 40 | ansible.builtin.file: 41 | path: "{{ mysql_hardening_mysql_confd_dir }}" 42 | state: directory 43 | owner: "{{ mysql_hardening_user }}" 44 | group: "{{ mysql_hardening_group }}" 45 | mode: "0750" 46 | 47 | - name: Check include-dir directive is present in my.cnf 48 | ansible.builtin.lineinfile: 49 | dest: "{{ mysql_hardening_mysql_conf_file }}" 50 | line: "!includedir {{ mysql_hardening_mysql_confd_dir }}" 51 | insertafter: EOF 52 | state: present 53 | backup: true 54 | notify: Restart mysql 55 | 56 | - name: Apply hardening configuration 57 | ansible.builtin.template: 58 | src: hardening.cnf.j2 59 | dest: "{{ mysql_hardening_mysql_confd_dir + '/hardening.cnf' }}" 60 | owner: "{{ mysql_cnf_owner }}" 61 | group: "{{ mysql_cnf_group }}" 62 | mode: "0640" 63 | notify: Restart mysql 64 | 65 | - name: Enable mysql 66 | ansible.builtin.service: 67 | name: "{{ mysql_daemon }}" 68 | enabled: "{{ mysql_daemon_enabled }}" 69 | -------------------------------------------------------------------------------- /roles/mysql_hardening/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fetch OS dependent variables 3 | ansible.builtin.include_vars: 4 | file: "{{ item }}" 5 | name: os_vars 6 | with_first_found: 7 | - files: 8 | - "{{ ansible_facts.distribution }}_{{ ansible_facts.distribution_major_version }}.yml" 9 | - "{{ ansible_facts.distribution }}.yml" 10 | - "{{ ansible_facts.os_family }}_{{ ansible_facts.distribution_major_version }}.yml" 11 | - "{{ ansible_facts.os_family }}.yml" 12 | skip: true 13 | tags: always 14 | 15 | # we only override variables with our default if they have not been specified already. 16 | # by default the lookup functions finds all varnames containing the string, therefore 17 | # we add ^ and $ to denote start and end of string, so this returns only exact matches. 18 | - name: Set OS dependent variables, if not already defined by user # noqa var-naming 19 | ansible.builtin.set_fact: 20 | "{{ item.key }}": "{{ item.value }}" 21 | when: not lookup('varnames', '^' + item.key + '$') 22 | with_dict: "{{ os_vars }}" 23 | tags: always 24 | 25 | - name: Check that the variable mysql_distribution is set correctly 26 | ansible.builtin.assert: 27 | that: mysql_distribution == 'mysql' or mysql_distribution == 'mariadb' 28 | fail_msg: mysql_distribution must be set to either mysql or mariadb! 29 | when: mysql_distribution is defined 30 | 31 | - name: Gather package facts to check for mysql/mariadb version 32 | ansible.builtin.package_facts: 33 | manager: auto 34 | when: not mysql_distribution is defined 35 | 36 | - name: Check if MySQL or MariaDB is used 37 | ansible.builtin.set_fact: 38 | mysql_distribution: "{{ ansible_facts.packages['mysql-server'] is defined | ternary('mysql', 'mariadb') }}" 39 | when: not mysql_distribution is defined 40 | 41 | - name: Check which MySQL/MariaDB version is used 42 | community.mysql.mysql_info: 43 | filter: version 44 | login_unix_socket: "{{ login_unix_socket | default(omit) }}" 45 | register: mysql_version 46 | 47 | - name: Check MySQL/MariaDB settings 48 | community.mysql.mysql_info: 49 | filter: settings 50 | login_unix_socket: "{{ login_unix_socket | default(omit) }}" 51 | register: mysql_settings 52 | 53 | # see https://stackoverflow.com/a/59451077/2953919 for the 54 | # dict2items and vice versa magic 55 | - name: Drop the secure-auth parameter on MySQL >=8.0.3 (not mariadb) 56 | ansible.builtin.set_fact: 57 | mysql_hardening_options: "{{ mysql_hardening_options | dict2items | rejectattr('key', 'search', 'secure-auth') | list | items2dict }}" 58 | when: 59 | - mysql_version.version.full is version('8.0.3', '>=') 60 | - mysql_distribution == "mysql" 61 | 62 | - name: Include tasks for configuration 63 | ansible.builtin.import_tasks: configure.yml 64 | when: mysql_hardening_enabled | bool 65 | tags: 66 | - mysql_hardening 67 | 68 | - name: Include tasks to secure mysql installation 69 | ansible.builtin.import_tasks: mysql_secure_installation.yml 70 | when: mysql_hardening_enabled | bool 71 | tags: 72 | - mysql_hardening 73 | - mysql_secure_installation 74 | -------------------------------------------------------------------------------- /roles/mysql_hardening/templates/hardening.cnf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | [mysqld] 5 | {% if mysql_hardening_skip_show_database -%} 6 | skip-show-database 7 | {% endif %} 8 | {% if mysql_hardening_skip_grant_tables -%} 9 | skip-grant-tables 10 | {% endif %} 11 | 12 | {% for (key, value) in mysql_hardening_options.items() %} 13 | {{ key }} = {{ value }} 14 | {% endfor %} 15 | 16 | {% if mysql_hardening_chroot %} 17 | chroot = '{{ mysql_hardening_chroot }}' 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /roles/mysql_hardening/templates/my.cnf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | [client] 5 | user=root 6 | password='{{ mysql_root_password | mandatory }}' 7 | #ssl 8 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_daemon: mariadb 3 | 4 | mysql_hardening_mysql_conf_file: /etc/mysql/my.cnf 5 | mysql_hardening_mysql_confd_dir: /etc/mysql/conf.d 6 | 7 | mysql_hardening_group: adm 8 | 9 | mysql_cnf_owner: root # owner of /etc/mysql/*.cnf files 10 | mysql_cnf_group: mysql # owner of /etc/mysql/*.cnf files 11 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/Fedora.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_daemon: mysqld 3 | mysql_hardening_mysql_conf_file: /etc/my.cnf 4 | mysql_hardening_mysql_confd_dir: /etc/my.cnf.d 5 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/FreeBSD.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_daemon: mysql-server 3 | 4 | mysql_hardening_mysql_conf_file: /usr/local/etc/mysql/my.cnf 5 | mysql_hardening_mysql_confd_dir: /usr/local/etc/mysql/conf.d 6 | 7 | mysql_hardening_group: mysql 8 | 9 | mysql_cnf_owner: root # owner of /usr/local/etc/mysql/*.cnf files 10 | mysql_cnf_group: mysql # owner of /usr/local/etc/mysql/*.cnf files 11 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/Oracle Linux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_daemon: mysqld 3 | 4 | mysql_hardening_mysql_conf_file: /etc/my.cnf 5 | mysql_hardening_mysql_confd_dir: /etc/my.cnf.d 6 | 7 | mysql_hardening_group: adm 8 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/RedHat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_daemon: mariadb 3 | mysql_hardening_mysql_conf_file: /etc/my.cnf 4 | mysql_hardening_mysql_confd_dir: /etc/my.cnf.d 5 | 6 | mysql_cnf_owner: root # owner of /etc/mysql/*.cnf files 7 | mysql_cnf_group: mysql # owner of /etc/mysql/*.cnf files 8 | 9 | mysql_hardening_group: mysql 10 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/Suse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_daemon: mariadb 3 | mysql_hardening_mysql_conf_file: '/etc/my.cnf' 4 | mysql_hardening_mysql_confd_dir: '/etc/my.cnf.d' 5 | 6 | mysql_cnf_owner: 'root' # owner of /etc/my.cnf.d/*.cnf files 7 | mysql_cnf_group: 'mysql' # owner of /etc/my.cnf.d/*.cnf files 8 | 9 | mysql_hardening_group: 'mysql' 10 | login_unix_socket: '/run/mysql/mysql.sock' 11 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/Ubuntu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_daemon: mysql 3 | 4 | mysql_hardening_mysql_conf_file: /etc/mysql/my.cnf 5 | mysql_hardening_mysql_confd_dir: /etc/mysql/conf.d 6 | 7 | mysql_cnf_owner: root # owner of /etc/mysql/*.cnf files 8 | mysql_cnf_group: mysql # owner of /etc/mysql/*.cnf files 9 | 10 | mysql_hardening_group: adm 11 | -------------------------------------------------------------------------------- /roles/mysql_hardening/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | mysql_hardening_user: mysql # owner of data 3 | -------------------------------------------------------------------------------- /roles/nginx_hardening/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.1.0](https://github.com/dev-sec/ansible-nginx-hardening/tree/2.1.0) (2018-11-18) 4 | 5 | [Full Changelog](https://github.com/dev-sec/ansible-nginx-hardening/compare/2.0.0...2.1.0) 6 | 7 | **Merged pull requests:** 8 | 9 | - add ubuntu 18.04 support [\#20](https://github.com/dev-sec/ansible-nginx-hardening/pull/20) ([rndmh3ro](https://github.com/rndmh3ro)) 10 | - updated minimum required ansible version to 2.5 in README, as the used 'loop' keyword was introduced in version 2.5 [\#19](https://github.com/dev-sec/ansible-nginx-hardening/pull/19) ([szEvEz](https://github.com/szEvEz)) 11 | 12 | ## [2.0.0](https://github.com/dev-sec/ansible-nginx-hardening/tree/2.0.0) (2018-09-08) 13 | 14 | [Full Changelog](https://github.com/dev-sec/ansible-nginx-hardening/compare/1.0.2...2.0.0) 15 | 16 | **Implemented enhancements:** 17 | 18 | - Update readme to include baselines [\#10](https://github.com/dev-sec/ansible-nginx-hardening/issues/10) 19 | - Update testing, remove useless params, style update [\#18](https://github.com/dev-sec/ansible-nginx-hardening/pull/18) ([rndmh3ro](https://github.com/rndmh3ro)) 20 | - Update README.md [\#14](https://github.com/dev-sec/ansible-nginx-hardening/pull/14) ([vishesh92](https://github.com/vishesh92)) 21 | - Add comment filter to {{ansible\_managed}} string [\#12](https://github.com/dev-sec/ansible-nginx-hardening/pull/12) ([fazlearefin](https://github.com/fazlearefin)) 22 | - use new Docker images [\#8](https://github.com/dev-sec/ansible-nginx-hardening/pull/8) ([rndmh3ro](https://github.com/rndmh3ro)) 23 | 24 | **Fixed bugs:** 25 | 26 | - Running kitchen verify asks for 'roots' password [\#11](https://github.com/dev-sec/ansible-nginx-hardening/issues/11) 27 | - Fix duplicate ssl_prefer_server_ciphers error [\#16](https://github.com/dev-sec/ansible-nginx-hardening/pull/16) ([oakey-b1](https://github.com/oakey-b1)) 28 | 29 | ## [1.0.2](https://github.com/dev-sec/ansible-nginx-hardening/tree/1.0.2) (2016-10-24) 30 | 31 | [Full Changelog](https://github.com/dev-sec/ansible-nginx-hardening/compare/1.0.1...1.0.2) 32 | 33 | **Fixed bugs:** 34 | 35 | - Syntax Error while loading YAML in defaults/main.yml [\#6](https://github.com/dev-sec/ansible-nginx-hardening/issues/6) 36 | 37 | **Merged pull requests:** 38 | 39 | - remove tabs. fix \#6 [\#7](https://github.com/dev-sec/ansible-nginx-hardening/pull/7) ([rndmh3ro](https://github.com/rndmh3ro)) 40 | 41 | ## [1.0.1](https://github.com/dev-sec/ansible-nginx-hardening/tree/1.0.1) (2016-09-23) 42 | 43 | [Full Changelog](https://github.com/dev-sec/ansible-nginx-hardening/compare/1.0.0...1.0.1) 44 | 45 | **Fixed bugs:** 46 | 47 | - ssl_dhparam [\#4](https://github.com/dev-sec/ansible-nginx-hardening/issues/4) 48 | 49 | **Closed issues:** 50 | 51 | - Make the owner /etc/nginx configurabe [\#3](https://github.com/dev-sec/ansible-nginx-hardening/issues/3) 52 | - Running worker process as non-privileged user \(1 failed\) [\#2](https://github.com/dev-sec/ansible-nginx-hardening/issues/2) 53 | 54 | **Merged pull requests:** 55 | 56 | - create dhparam file. fix \#4 [\#5](https://github.com/dev-sec/ansible-nginx-hardening/pull/5) ([rndmh3ro](https://github.com/rndmh3ro)) 57 | - improve gemfile, update readme for local tests [\#1](https://github.com/dev-sec/ansible-nginx-hardening/pull/1) ([chris-rock](https://github.com/chris-rock)) 58 | 59 | ## [1.0.0](https://github.com/dev-sec/ansible-nginx-hardening/tree/1.0.0) (2016-08-11) 60 | 61 | [Full Changelog](https://github.com/dev-sec/ansible-nginx-hardening/compare/1b9dcf16cfbf45ff5f50cd83509245d1527f9fd0...1.0.0) 62 | 63 | \* _This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_ 64 | -------------------------------------------------------------------------------- /roles/nginx_hardening/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nginx_client_body_buffer_size: 1k 3 | nginx_remove_default_site: true 4 | nginx_client_max_body_size: 1k 5 | nginx_keepalive_timeout: 5 5 6 | nginx_server_tokens: "off" 7 | nginx_client_header_buffer_size: 1k 8 | nginx_large_client_header_buffers: 2 1k 9 | nginx_client_body_timeout: "10" 10 | nginx_client_header_timeout: "10" 11 | nginx_send_timeout: "10" 12 | nginx_limit_conn_zone: $binary_remote_addr zone=default:10m 13 | nginx_limit_conn: default 5 14 | nginx_configuration_dir: /etc/nginx 15 | nginx_configuration_hardening_dir: /etc/nginx 16 | nginx_owner_user: root 17 | nginx_owner_group: root 18 | nginx_add_header: 19 | # avoid clickjacking 20 | - X-Frame-Options SAMEORIGIN 21 | # disable content-type sniffing 22 | - X-Content-Type-Options nosniff 23 | # XSS filter 24 | - X-XSS-Protection "1; mode=block" 25 | - Strict-Transport-Security max-age=15768000 26 | - Content-Security-Policy "script-src 'self'; object-src 'self'" 27 | 28 | nginx_ssl_prefer_server_ciphers: "on" 29 | nginx_ssl_protocols: TLSv1.2 TLSv1.3 30 | # yamllint disable-line rule:line-length 31 | nginx_ssl_ciphers: ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256 32 | nginx_ssl_session_tickets: "off" 33 | nginx_dh_size: "4096" 34 | -------------------------------------------------------------------------------- /roles/nginx_hardening/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart nginx 3 | ansible.builtin.service: 4 | name: nginx 5 | state: restarted 6 | -------------------------------------------------------------------------------- /roles/nginx_hardening/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Sebastian Gumprich 4 | description: This Ansible role provides secure nginx configurations. http://dev-sec.io/ 5 | company: Hardening Framework Team 6 | license: Apache License 2.0 7 | min_ansible_version: 2.9.10 8 | platforms: 9 | - name: EL 10 | versions: 11 | - "8" 12 | - "9" 13 | - name: Ubuntu 14 | versions: 15 | - focal 16 | - jammy 17 | - noble 18 | - name: Debian 19 | versions: 20 | - bookworm 21 | - bullseye 22 | - name: Amazon 23 | galaxy_tags: 24 | - system 25 | - security 26 | - hardening 27 | - nginx 28 | dependencies: [] 29 | -------------------------------------------------------------------------------- /roles/nginx_hardening/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create additional configuration 3 | ansible.builtin.template: 4 | src: hardening.conf.j2 5 | dest: "{{ nginx_configuration_dir }}/conf.d/90.hardening.conf" 6 | mode: "0600" 7 | owner: "{{ nginx_owner_user }}" 8 | group: "{{ nginx_owner_group }}" 9 | notify: Restart nginx 10 | 11 | - name: Change configuration in main nginx.conf 12 | ansible.builtin.lineinfile: 13 | dest: "{{ nginx_configuration_dir }}/nginx.conf" 14 | regexp: ^\s*server_tokens 15 | line: " server_tokens {{ nginx_server_tokens }};" 16 | insertafter: http { 17 | mode: "0640" 18 | owner: "{{ nginx_owner_user }}" 19 | group: "{{ nginx_owner_group }}" 20 | notify: Restart nginx 21 | 22 | - name: Change ssl_protocols in main nginx.conf 23 | ansible.builtin.lineinfile: 24 | dest: "{{ nginx_configuration_dir }}/nginx.conf" 25 | regexp: ^\s*ssl_protocols 26 | line: " ssl_protocols {{ nginx_ssl_protocols }};" 27 | insertafter: http { 28 | mode: "0640" 29 | owner: "{{ nginx_owner_user }}" 30 | group: "{{ nginx_owner_group }}" 31 | notify: Restart nginx 32 | 33 | - name: Change ssl_prefer_server_ciphers in main nginx.conf 34 | ansible.builtin.lineinfile: 35 | dest: "{{ nginx_configuration_dir }}/nginx.conf" 36 | regexp: ^\s*ssl_prefer_server_ciphers 37 | line: " ssl_prefer_server_ciphers {{ nginx_ssl_prefer_server_ciphers }};" 38 | insertafter: http { 39 | mode: "0640" 40 | owner: "{{ nginx_owner_user }}" 41 | group: "{{ nginx_owner_group }}" 42 | notify: Restart nginx 43 | 44 | - name: Change client_max_body_size in main nginx.conf 45 | ansible.builtin.lineinfile: 46 | dest: "{{ nginx_configuration_dir }}/nginx.conf" 47 | regexp: ^\s*client_max_body_size 48 | line: " client_max_body_size {{ nginx_client_max_body_size }};" 49 | insertafter: http { 50 | mode: "0640" 51 | owner: "{{ nginx_owner_user }}" 52 | group: "{{ nginx_owner_group }}" 53 | notify: Restart nginx 54 | 55 | - name: Change client_body_buffer_size in main nginx.conf 56 | ansible.builtin.lineinfile: 57 | dest: "{{ nginx_configuration_dir }}/nginx.conf" 58 | regexp: ^\s*client_body_buffer_size 59 | line: " client_body_buffer_size {{ nginx_client_body_buffer_size }};" 60 | insertafter: http { 61 | mode: "0640" 62 | owner: "{{ nginx_owner_user }}" 63 | group: "{{ nginx_owner_group }}" 64 | notify: Restart nginx 65 | 66 | - name: Change keepalive_timeout in main nginx.conf 67 | ansible.builtin.lineinfile: 68 | dest: "{{ nginx_configuration_dir }}/nginx.conf" 69 | regexp: ^\s*keepalive_timeout 70 | line: " keepalive_timeout {{ nginx_keepalive_timeout }};" 71 | insertafter: http { 72 | mode: "0640" 73 | owner: "{{ nginx_owner_user }}" 74 | group: "{{ nginx_owner_group }}" 75 | notify: Restart nginx 76 | 77 | - name: Remove default.conf 78 | ansible.builtin.file: 79 | path: "{{ item }}" 80 | state: absent 81 | when: nginx_remove_default_site 82 | notify: Restart nginx 83 | loop: 84 | - "{{ nginx_configuration_dir }}/conf.d/default.conf" 85 | - "{{ nginx_configuration_dir }}/sites-enabled/default" 86 | 87 | - name: Generate dh group 88 | community.crypto.openssl_dhparam: 89 | path: "{{ nginx_configuration_dir }}/dh{{ nginx_dh_size }}.pem" 90 | size: "{{ nginx_dh_size }}" 91 | mode: "0640" 92 | owner: "{{ nginx_owner_user }}" 93 | group: "{{ nginx_owner_group }}" 94 | notify: Restart nginx 95 | -------------------------------------------------------------------------------- /roles/nginx_hardening/templates/hardening.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed|comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | # Additional configuration for Nginx. 4 | 5 | client_header_buffer_size {{ nginx_client_header_buffer_size }}; 6 | large_client_header_buffers {{ nginx_large_client_header_buffers }}; 7 | client_body_timeout {{ nginx_client_body_timeout }}; 8 | client_header_timeout {{ nginx_client_header_timeout }}; 9 | send_timeout {{ nginx_send_timeout }}; 10 | limit_conn_zone {{ nginx_limit_conn_zone }}; 11 | limit_conn {{ nginx_limit_conn }}; 12 | ssl_ciphers '{{ nginx_ssl_ciphers }}'; 13 | ssl_session_tickets {{ nginx_ssl_session_tickets }}; 14 | ssl_dhparam {{ nginx_configuration_hardening_dir }}/dh{{ nginx_dh_size }}.pem; 15 | {% for header in nginx_add_header %} 16 | add_header {{ header }}; 17 | {% endfor %} 18 | -------------------------------------------------------------------------------- /roles/os_hardening/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update-initramfs # noqa no-changed-when 3 | ansible.builtin.command: update-initramfs -u 4 | 5 | - name: Reload systemd 6 | ansible.builtin.systemd: 7 | daemon_reload: true 8 | 9 | - name: Restart auditd via systemd 10 | ansible.builtin.systemd: 11 | name: auditd.service 12 | state: restarted 13 | ignore_errors: "{{ ansible_check_mode }}" 14 | when: 15 | - molecule_yml.driver.name | default() != "docker" # restarting auditd in a container does not work 16 | - not ansible_facts.os_family == 'RedHat' 17 | 18 | - name: Restart auditd via service # noqa command-instead-of-module no-changed-when 19 | ansible.builtin.command: 20 | cmd: service auditd restart # rhel: see: https://access.redhat.com/solutions/2664811 21 | when: 22 | - molecule_yml.driver.name | default() != "docker" # restarting auditd in a container does not work 23 | - ansible_facts.os_family == 'RedHat' 24 | 25 | - name: Remount filesystems 26 | ansible.posix.mount: 27 | path: "{{ item }}" 28 | state: remounted 29 | loop: "{{ mountpoints_changed }}" 30 | -------------------------------------------------------------------------------- /roles/os_hardening/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Sebastian Gumprich 4 | description: This Ansible role provides numerous security-related ssh configurations, providing all-round base protection. 5 | company: Hardening Framework Team 6 | license: Apache License 2.0 7 | min_ansible_version: "2.16" 8 | platforms: 9 | - name: EL 10 | versions: 11 | - "8" 12 | - "9" 13 | - name: Ubuntu 14 | versions: 15 | - focal 16 | - jammy 17 | - noble 18 | - name: Debian 19 | versions: 20 | - bookworm 21 | - bullseye 22 | - name: Amazon 23 | - name: Fedora 24 | - name: ArchLinux 25 | - name: SmartOS 26 | - name: opensuse 27 | galaxy_tags: 28 | - system 29 | - security 30 | - hardening 31 | dependencies: [] 32 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/apt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Remove deprecated or insecure packages | package-01 - package-09 3 | ansible.builtin.apt: 4 | name: "{{ os_security_packages_list }}" 5 | state: absent 6 | purge: true 7 | when: os_security_packages_clean | bool 8 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/auditd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install auditd package | package-08 3 | ansible.builtin.package: 4 | name: "{{ auditd_package }}" 5 | state: present 6 | tags: auditd 7 | 8 | - name: Configure auditd | package-08 9 | ansible.builtin.template: 10 | src: "{{ os_auditd_template }}" 11 | dest: /etc/audit/auditd.conf 12 | owner: root 13 | group: root 14 | mode: "0640" 15 | notify: 16 | - Restart auditd via service 17 | - Restart auditd via systemd 18 | tags: auditd 19 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/cron.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Granting write access to this directory for non-privileged users could provide 3 | # them the means for gaining unauthorized elevated privileges. 4 | # Granting read access to this directory could give an unprivileged user insight 5 | # in how to gain elevated privileges or circumvent auditing controls. 6 | # CIS 5.1.2 - CIS 5.1.7 7 | # 8 | - name: Find cron files and directories 9 | ansible.builtin.find: 10 | paths: 11 | - /etc 12 | patterns: 13 | - cron.hourly 14 | - cron.daily 15 | - cron.weekly 16 | - cron.monthly 17 | - cron.d 18 | - crontab 19 | file_type: any 20 | register: cron_directories 21 | 22 | - name: Ensure permissions on cron files and directories are configured 23 | ansible.builtin.file: 24 | path: "{{ item.path }}" 25 | owner: root 26 | group: root 27 | mode: og-rwx 28 | with_items: "{{ cron_directories.files }}" 29 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/ctrlaltdel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Disable CTRL-ALT-DEL 3 | ansible.builtin.systemd: 4 | name: ctrl-alt-del.target 5 | masked: true 6 | daemon_reload: true 7 | when: ansible_service_mgr == "systemd" 8 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/limits.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Disable coredumps 3 | when: not os_security_kernel_enable_core_dump | bool 4 | block: 5 | - name: Create limits.d-directory if it does not exist | sysctl-31a, sysctl-31b 6 | ansible.builtin.file: 7 | path: /etc/security/limits.d 8 | owner: root 9 | group: root 10 | mode: "0755" 11 | state: directory 12 | 13 | - name: Create additional limits config file -> 10.hardcore.conf | sysctl-31a, sysctl-31b 14 | community.general.pam_limits: 15 | dest: /etc/security/limits.d/10.hardcore.conf 16 | domain: "*" 17 | limit_type: hard 18 | limit_item: core 19 | value: "0" 20 | comment: Prevent core dumps for all users. These are usually not needed and may contain sensitive information 21 | when: not ansible_check_mode # the directory needs to be created first, which does not happen in check_mode 22 | 23 | - name: Set 10.hardcore.conf perms to 0400 and root ownership 24 | ansible.builtin.file: 25 | path: /etc/security/limits.d/10.hardcore.conf 26 | owner: root 27 | group: root 28 | mode: "0440" 29 | state: touch 30 | modification_time: preserve 31 | access_time: preserve 32 | 33 | - name: Create coredump.conf.d-directory if it does not exist 34 | ansible.builtin.file: 35 | path: /etc/systemd/coredump.conf.d 36 | owner: root 37 | group: root 38 | mode: "0755" 39 | state: directory 40 | when: ansible_service_mgr == "systemd" 41 | 42 | - name: Create custom.conf for disabling coredumps 43 | ansible.builtin.template: 44 | src: etc/systemd/coredump.conf.d/coredumps.conf.j2 45 | dest: /etc/systemd/coredump.conf.d/custom.conf 46 | owner: root 47 | group: root 48 | mode: "0644" 49 | when: ansible_service_mgr == "systemd" 50 | notify: Reload systemd 51 | 52 | - name: Enable coredumps 53 | when: os_security_kernel_enable_core_dump | bool 54 | block: 55 | - name: Remove coredump.conf.d directory with files 56 | ansible.builtin.file: 57 | path: /etc/systemd/coredump.conf.d 58 | state: absent 59 | when: ansible_service_mgr == "systemd" 60 | notify: Reload systemd 61 | 62 | - name: Remove 10.hardcore.conf config file 63 | ansible.builtin.file: 64 | path: /etc/security/limits.d/10.hardcore.conf 65 | state: absent 66 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/login_defs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create login.defs | os-05, os-05b 3 | ansible.builtin.template: 4 | src: etc/login.defs.j2 5 | dest: /etc/login.defs 6 | owner: root 7 | group: root 8 | mode: "0444" 9 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include hardening tasks 3 | ansible.builtin.include_tasks: hardening.yml 4 | when: os_hardening_enabled | bool 5 | tags: 6 | - always 7 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/minimize_access_fs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Configure hardened options for mount {{ mount.path }}" 3 | ansible.posix.mount: 4 | path: "{{ mount.path }}" 5 | src: "{{ mount.src | default(mountinfo.device, true) }}" 6 | fstype: "{{ mount.fstype | default(mountinfo.fstype, true) }}" 7 | opts: "{{ mount.opts }}" 8 | dump: "{{ mount.dump | default('1' if mount.fstype | default(mountinfo.fstype, true) in ['ext3', 'ext4'] else '0', true) }}" 9 | passno: "{{ mount.passno | default('2' if mount.fstype | default(mountinfo.fstype, true) in ['ext3', 'ext4'] else '0', true) }}" 10 | state: present 11 | register: mountpoint 12 | when: 13 | - mount.enabled | bool 14 | - mount.path in mountpoints_list 15 | vars: 16 | mountinfo: "{{ ansible_mounts | selectattr('mount', 'equalto', mount.path) | list | first | default(None) }}" 17 | notify: Remount filesystems 18 | 19 | - name: "Check for existence of mount {{ mount.path }}" 20 | ansible.builtin.stat: 21 | path: "{{ mount.path }}" 22 | register: mountpoint_exists 23 | 24 | - name: "Harden permissions for directory of mount {{ mount.path }}" 25 | ansible.builtin.file: 26 | dest: "{{ mount.path }}" 27 | owner: "{{ mount.owner }}" 28 | group: "{{ mount.group }}" 29 | mode: "{{ mount.mode }}" 30 | when: 31 | - mountpoint_exists.stat.exists | bool 32 | 33 | - name: "Register changed mountpoints" 34 | ansible.builtin.set_fact: 35 | mountpoints_changed: "{{ mountpoints_changed | default([]) + [mount.path] }}" 36 | when: mountpoint.changed # noqa no-handler 37 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/modprobe.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install modprobe to disable filesystems | os-10 3 | ansible.builtin.package: 4 | name: "{{ modprobe_package }}" 5 | state: present 6 | 7 | - name: Check if efi is installed 8 | ansible.builtin.stat: 9 | path: /sys/firmware/efi 10 | register: efi_installed 11 | 12 | - name: Remove vfat from fs-list if efi is used 13 | ansible.builtin.set_fact: 14 | os_unused_filesystems: "{{ os_unused_filesystems | difference('vfat') }}" 15 | when: 16 | - efi_installed.stat.isdir is defined 17 | - efi_installed.stat.isdir 18 | 19 | - name: Remove used filesystems from fs-list 20 | ansible.builtin.set_fact: 21 | os_unused_filesystems: "{{ os_unused_filesystems | difference(ansible_mounts | map(attribute='fstype') | list) }}" 22 | 23 | - name: Disable unused filesystems | os-10 24 | ansible.builtin.template: 25 | src: etc/modprobe.d/modprobe.j2 26 | dest: /etc/modprobe.d/dev-sec.conf 27 | owner: root 28 | group: root 29 | mode: "0644" 30 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/netrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get user accounts | os-09 3 | ansible.builtin.command: "awk -F: '{print $1}' /etc/passwd" 4 | changed_when: false 5 | check_mode: false 6 | register: users_accounts 7 | 8 | - name: Delete .netrc-files from system | os-09 9 | ansible.builtin.file: 10 | dest: ~{{ item }}/.netrc 11 | state: absent 12 | loop: "{{ users_accounts.stdout_lines | flatten | default([]) }}" 13 | when: item not in os_netrc_whitelist_user 14 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/pam.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Gather package facts 3 | ansible.builtin.package_facts: 4 | manager: auto 5 | when: 6 | - ansible_facts.os_family != 'Suse' 7 | - ansible_facts.os_family != 'Archlinux' 8 | 9 | # the reason for this is so a user cannot connect to a server, 10 | # that isn't connected to an LDAP server anymore. 11 | # normally caching credentials shouldn't be necessary for most machines. 12 | # removing it provides some more security while not removing usability. 13 | - name: Remove pam ccreds to disable password caching 14 | ansible.builtin.package: 15 | name: "{{ os_packages_pam_ccreds }}" 16 | state: absent 17 | when: 18 | - ansible_facts.os_family != 'Archlinux' 19 | 20 | - name: Import tasks for Debian PAM 21 | ansible.builtin.import_tasks: pam_debian.yml 22 | when: 23 | - ansible_facts.os_family == 'Debian' 24 | 25 | - name: Import tasks for RedHat PAM 26 | ansible.builtin.import_tasks: pam_rhel.yml 27 | when: 28 | - ansible_facts.os_family == 'RedHat' 29 | 30 | - name: Allow Login with SSH Keys, when user password is expired 31 | ansible.builtin.lineinfile: 32 | path: /etc/pam.d/system-auth 33 | backrefs: true 34 | regexp: "^(account.*pam_unix.so(?!.*no_pass_expiry).*)$" 35 | line: '\1 no_pass_expiry' 36 | when: 37 | - ansible_facts.os_family == 'Archlinux' 38 | 39 | - name: Allow Login with SSH Keys, when user password is expired 40 | ansible.builtin.lineinfile: 41 | path: /etc/pam.d/common-account 42 | backrefs: true 43 | regexp: "^(account.*pam_unix.so(?!.*no_pass_expiry).*)$" 44 | line: '\1 no_pass_expiry' 45 | when: 46 | - ansible_facts.os_family == 'Suse' 47 | 48 | - name: NSA 2.3.3.5 Upgrade Password Hashing Algorithm to SHA-512 49 | ansible.builtin.template: 50 | src: etc/libuser.conf.j2 51 | dest: /etc/libuser.conf 52 | mode: "0640" 53 | owner: root 54 | group: root 55 | when: 56 | - ansible_facts.os_family != 'Suse' 57 | - ansible_facts.os_family != 'Archlinux' 58 | - "'libuser' in ansible_facts.packages" 59 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/pam_rhel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install sssd-clients 3 | ansible.builtin.dnf: 4 | name: sssd-client 5 | state: present 6 | when: 7 | - os_auth_pam_sssd_enable | bool 8 | 9 | - name: Configure passwdqc and faillock via central system-auth config 10 | ansible.builtin.template: 11 | src: etc/pam.d/rhel_auth.j2 12 | dest: /etc/pam.d/system-auth-local 13 | mode: "0640" 14 | owner: root 15 | group: root 16 | 17 | - name: Enable our config for system-auth 18 | ansible.builtin.file: 19 | src: /etc/pam.d/system-auth-local 20 | dest: /etc/pam.d/system-auth 21 | mode: "0640" 22 | owner: root 23 | group: root 24 | state: link 25 | force: true 26 | 27 | - name: Configure passwdqc and faillock via central password-auth config 28 | ansible.builtin.template: 29 | src: etc/pam.d/rhel_auth.j2 30 | dest: /etc/pam.d/password-auth-local 31 | mode: "0640" 32 | owner: root 33 | group: root 34 | 35 | - name: Enable our config for password-auth 36 | ansible.builtin.file: 37 | src: /etc/pam.d/password-auth-local 38 | dest: /etc/pam.d/password-auth 39 | mode: "0640" 40 | owner: root 41 | group: root 42 | state: link 43 | force: true 44 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/profile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add pinerolo_profile.sh to profile.d 3 | ansible.builtin.template: 4 | src: etc/profile.d/profile.conf.j2 5 | dest: /etc/profile.d/pinerolo_profile.sh 6 | owner: root 7 | group: root 8 | mode: "0750" 9 | when: not os_security_kernel_enable_core_dump | bool 10 | 11 | - name: Remove pinerolo_profile.sh from profile.d 12 | ansible.builtin.file: 13 | path: /etc/profile.d/pinerolo_profile.sh 14 | state: absent 15 | when: os_security_kernel_enable_core_dump | bool 16 | 17 | - name: Add autologout to profile env 18 | ansible.builtin.template: 19 | src: etc/profile.d/tmout.sh.j2 20 | dest: /etc/profile.d/tmout.sh 21 | owner: root 22 | group: root 23 | mode: "0644" 24 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/rhosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get user accounts | os-09 3 | ansible.builtin.command: "awk -F: '{print $1}' /etc/passwd" 4 | changed_when: false 5 | check_mode: false 6 | register: users_accounts 7 | 8 | - name: Delete rhosts-files from system | os-09 9 | ansible.builtin.file: 10 | dest: ~{{ item }}/.rhosts 11 | state: absent 12 | loop: "{{ users_accounts.stdout_lines | flatten | default([]) }}" 13 | 14 | - name: Delete hosts.equiv from system | os-01 15 | ansible.builtin.file: 16 | dest: /etc/hosts.equiv 17 | state: absent 18 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/securetty.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create securetty 3 | ansible.builtin.template: 4 | src: etc/securetty.j2 5 | dest: /etc/securetty 6 | owner: root 7 | group: root 8 | mode: "0400" 9 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/selinux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure selinux | selinux-01 3 | ansible.posix.selinux: 4 | policy: "{{ os_selinux_policy }}" 5 | state: "{{ os_selinux_state }}" # noqa args - see https://github.com/ansible/ansible-lint/issues/2930 6 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/suid_sgid.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Remove suid/sgid bit from binaries in blacklist | os-06 3 | ansible.builtin.file: 4 | path: "{{ item }}" 5 | mode: a-s 6 | state: file 7 | follow: true 8 | failed_when: false 9 | with_community.general.flattened: 10 | - "{{ os_security_suid_sgid_system_blacklist }}" 11 | - "{{ os_security_suid_sgid_blacklist }}" 12 | 13 | - name: Find binaries with suid/sgid set | os-06 14 | ansible.builtin.shell: find / -xdev \( -perm -4000 -o -perm -2000 \) -type f ! -path '/proc/*' -print 2>/dev/null 15 | register: sbit_binaries 16 | when: os_security_suid_sgid_remove_from_unknown | bool 17 | changed_when: false 18 | check_mode: false 19 | 20 | - name: Gather files from which to remove suids/sgids and remove system white-listed files | os-06 21 | ansible.builtin.set_fact: 22 | suid: "{{ sbit_binaries.stdout_lines | difference(os_security_suid_sgid_system_whitelist) }}" 23 | when: os_security_suid_sgid_remove_from_unknown | bool 24 | 25 | - name: Remove suid/sgid bit from all binaries except in system and user whitelist | os-06 26 | ansible.builtin.file: 27 | path: "{{ item }}" 28 | mode: a-s 29 | state: file 30 | follow: true 31 | with_community.general.flattened: 32 | - "{{ suid | default([]) | difference(os_security_suid_sgid_whitelist) }}" 33 | when: os_security_suid_sgid_remove_from_unknown | bool 34 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/sysctl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Protect sysctl.conf 3 | ansible.builtin.file: 4 | path: /etc/sysctl.conf 5 | owner: root 6 | group: root 7 | mode: "0440" 8 | state: touch 9 | modification_time: preserve 10 | access_time: preserve 11 | 12 | - name: Set Daemon umask, do config for rhel-family | NSA 2.2.4.1 13 | ansible.builtin.template: 14 | src: etc/sysconfig/rhel_sysconfig_init.j2 15 | dest: /etc/sysconfig/init 16 | owner: root 17 | group: root 18 | mode: "0544" 19 | when: ansible_facts.os_family == 'RedHat' 20 | 21 | - name: Change sysctls 22 | when: ansible_virtualization_type not in ['docker', 'lxc', 'openvz'] 23 | block: 24 | - name: Change various sysctl-settings, look at the sysctl-vars file for documentation 25 | ansible.posix.sysctl: 26 | name: "{{ item.key }}" 27 | value: "{{ item.value }}" 28 | sysctl_set: true 29 | state: present 30 | reload: true 31 | ignoreerrors: true 32 | # sysctl_rhel_config is kept for backwards-compatibility. use sysctl_custom_config instead 33 | # combines all sysctl-dicts into one, adds empty dicts if they are not defined 34 | with_dict: "{{ ((sysctl_config 35 | | combine(sysctl_custom_config | default({}))) 36 | | combine(sysctl_rhel_config | default({}))) 37 | | combine(sysctl_overwrite | default({})) }}" 38 | when: item.key not in sysctl_unsupported_entries | default() 39 | 40 | - name: Apply ufw defaults 41 | ansible.builtin.template: 42 | src: etc/default/ufw.j2 43 | dest: /etc/default/ufw 44 | mode: "0644" 45 | when: 46 | - ufw_manage_defaults 47 | - ansible_facts.os_family == 'Debian' 48 | tags: ufw 49 | -------------------------------------------------------------------------------- /roles/os_hardening/tasks/yum.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Remove unused repositories 3 | ansible.builtin.file: 4 | name: /etc/yum.repos.d/{{ item }}.repo 5 | state: absent 6 | loop: 7 | - CentOS-Debuginfo 8 | - CentOS-Media 9 | - CentOS-Vault 10 | when: os_security_packages_clean | bool 11 | 12 | - name: Get yum repository files 13 | ansible.builtin.find: 14 | paths: /etc/yum.repos.d 15 | patterns: "*.repo" 16 | register: yum_repos 17 | 18 | # for the 'default([])' see here: 19 | # https://github.com/dev-sec/ansible-os-hardening/issues/99 and 20 | # https://stackoverflow.com/questions/37067827/ansible-deprecation-warning-for-undefined-variable-despite-when-clause 21 | - name: Activate gpg-check for yum repository files 22 | ansible.builtin.replace: 23 | path: "{{ item }}" 24 | regexp: ^\s*gpgcheck.* 25 | replace: gpgcheck=1 26 | mode: "0644" 27 | with_items: 28 | # yamllint disable-line rule:line-length 29 | - "{{ yum_repos.files | default([]) | map(attribute='path') | difference(os_yum_repo_file_whitelist | map('regex_replace', '^', '/etc/yum.repos.d/') | list) }}" 30 | 31 | # failed_when is needed because by default replace module will fail if the file doesn't exists. 32 | # status.rc is only defined if an error accrued and only error code (rc) 257 will be ignored. 33 | # All other errors will still be raised. 34 | - name: Activate gpg-check for config files 35 | ansible.builtin.replace: 36 | path: "{{ item }}" 37 | regexp: ^\s*gpgcheck\W.* 38 | replace: gpgcheck=1 39 | mode: "0644" 40 | register: status 41 | failed_when: status.rc is defined and status.rc not in (257, 0) 42 | loop: 43 | - /etc/yum.conf 44 | - /etc/dnf/dnf.conf 45 | - /etc/yum/pluginconf.d/rhnplugin.conf 46 | 47 | - name: Remove deprecated or insecure packages | package-01 - package-09 48 | ansible.builtin.dnf: 49 | name: "{{ os_security_packages_list }}" 50 | state: absent 51 | when: os_security_packages_clean | bool 52 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/audit/auditd.conf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | write_logs = {{ os_auditd_write_logs | bool | ternary('yes', 'no') }} 5 | log_file = {{ os_auditd_log_file }} 6 | log_format = {{ os_auditd_log_format }} 7 | log_group = {{ os_auditd_log_group }} 8 | priority_boost = 4 9 | flush = {{ os_auditd_flush }} 10 | freq = {{ os_auditd_freq }} 11 | num_logs = {{ os_auditd_num_logs }} 12 | disp_qos = lossy 13 | dispatcher = /sbin/audispd 14 | name_format = {{ os_auditd_name_format }} 15 | max_log_file = {{ os_auditd_max_log_file }} 16 | max_log_file_action = {{ os_auditd_max_log_file_action }} 17 | space_left = {{ os_auditd_space_left }} 18 | space_left_action = {{ os_auditd_space_left_action }} 19 | action_mail_acct = {{ os_auditd_action_mail_acct }} 20 | admin_space_left = {{ os_auditd_admin_space_left }} 21 | admin_space_left_action = {{ os_auditd_admin_space_left_action }} 22 | disk_full_action = {{ os_auditd_disk_full_action }} 23 | disk_error_action = {{ os_auditd_disk_error_action }} 24 | tcp_listen_queue = 5 25 | tcp_max_per_addr = 1 26 | tcp_client_max_idle = 0 27 | enable_krb5 = no 28 | krb5_principal = auditd 29 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/default/ufw.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | # /etc/default/ufw 5 | # 6 | 7 | # Set to yes to apply rules to support IPv6 (no means only IPv6 on loopback 8 | # accepted). You will need to 'disable' and then 'enable' the firewall for 9 | # the changes to take affect. 10 | IPV6={{ 'yes' if ufw_enable_ipv6 else 'no' }} 11 | 12 | # Set the default input policy to ACCEPT, DROP, or REJECT. Please note that if 13 | # you change this you will most likely want to adjust your rules. 14 | DEFAULT_INPUT_POLICY="{{ ufw_default_input_policy }}" 15 | 16 | # Set the default output policy to ACCEPT, DROP, or REJECT. Please note that if 17 | # you change this you will most likely want to adjust your rules. 18 | DEFAULT_OUTPUT_POLICY="{{ ufw_default_output_policy }}" 19 | 20 | # Set the default forward policy to ACCEPT, DROP or REJECT. Please note that 21 | # if you change this you will most likely want to adjust your rules 22 | DEFAULT_FORWARD_POLICY="{{ ufw_default_forward_policy }}" 23 | 24 | # Set the default application policy to ACCEPT, DROP, REJECT or SKIP. Please 25 | # note that setting this to ACCEPT may be a security risk. See 'man ufw' for 26 | # details 27 | DEFAULT_APPLICATION_POLICY="{{ ufw_default_application_policy }}" 28 | 29 | # By default, ufw only touches its own chains. Set this to 'yes' to have ufw 30 | # manage the built-in chains too. Warning: setting this to 'yes' will break 31 | # non-ufw managed firewall rules 32 | MANAGE_BUILTINS="{{ ufw_manage_builtins }}" 33 | 34 | # 35 | # IPT backend 36 | # 37 | # only enable if using iptables backend and want to overwrite /etc/sysctl.conf 38 | {% if ufw_ipt_sysctl == '' %}#{% endif %}IPT_SYSCTL={{ ufw_ipt_sysctl }} 39 | 40 | # Extra connection tracking modules to load. Complete list can be found in 41 | # net/netfilter/Kconfig of your kernel source. Some common modules: 42 | # nf_conntrack_irc, nf_nat_irc: DCC (Direct Client to Client) support 43 | # nf_conntrack_netbios_ns: NetBIOS (samba) client support 44 | # nf_conntrack_pptp, nf_nat_pptp: PPTP over stateful firewall/NAT 45 | # nf_conntrack_ftp, nf_nat_ftp: active FTP support 46 | # nf_conntrack_tftp, nf_nat_tftp: TFTP support (server side) 47 | IPT_MODULES="{{ ufw_ipt_modules }}" 48 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/libuser.conf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | # See libuser.conf(5) for more information. 5 | 6 | # Do not modify the default module list if you care about unattended calls 7 | # to programs (i.e., scripts) working! 8 | 9 | [import] 10 | # Data from these files is used when libuser.conf does not define a value. 11 | # The mapping is documented in the man page. 12 | login_defs = /etc/login.defs 13 | default_useradd = /etc/default/useradd 14 | 15 | [defaults] 16 | # The default (/usr/lib*/libuser) is usually correct 17 | # moduledir = /your/custom/directory 18 | 19 | # The following variables are usually imported: 20 | # skeleton = /etc/skel 21 | # mailspooldir = /var/mail 22 | 23 | # NSA 2.3.3.5 Upgrade Password Hashing Algorithm to SHA-512 24 | crypt_style = sha512 25 | 26 | modules = files shadow 27 | create_modules = files shadow 28 | # modules = files shadow ldap 29 | # create_modules = ldap 30 | 31 | [userdefaults] 32 | LU_USERNAME = %n 33 | # LU_UIDNUMBER = 500 34 | LU_GIDNUMBER = %u 35 | # LU_USERPASSWORD = !! 36 | # LU_GECOS = %n 37 | # LU_HOMEDIRECTORY = /home/%n 38 | # LU_LOGINSHELL = /bin/bash 39 | 40 | # LU_SHADOWNAME = %n 41 | # LU_SHADOWPASSWORD = !! 42 | # LU_SHADOWLASTCHANGE = %d 43 | # LU_SHADOWMIN = 0 44 | # LU_SHADOWMAX = 99999 45 | # LU_SHADOWWARNING = 7 46 | # LU_SHADOWINACTIVE = -1 47 | # LU_SHADOWEXPIRE = -1 48 | # LU_SHADOWFLAG = -1 49 | 50 | [groupdefaults] 51 | LU_GROUPNAME = %n 52 | # LU_GIDNUMBER = 500 53 | # LU_GROUPPASSWORD = !! 54 | # LU_MEMBERUID = 55 | # LU_ADMINISTRATORUID = 56 | 57 | [files] 58 | # This is useful for the case where some master files are used to 59 | # populate a different NSS mechanism which this workstation uses. 60 | # directory = /etc 61 | 62 | [shadow] 63 | # This is useful for the case where some master files are used to 64 | # populate a different NSS mechanism which this workstation uses. 65 | # directory = /etc 66 | 67 | [ldap] 68 | # Setting these is always necessary. 69 | # server = ldap 70 | # basedn = dc=example,dc=com 71 | 72 | # Setting these is rarely necessary, since it's usually correct. 73 | # userBranch = ou=People 74 | # groupBranch = ou=Group 75 | 76 | # Set only if your administrative user uses simple bind operations to 77 | # connect to the server. 78 | # binddn = cn=Manager,dc=example,dc=com 79 | 80 | # Set this only if the default user (as determined by SASL) is incorrect 81 | # for SASL bind operations. Usually, it's correct, so you'll rarely need 82 | # to set these. 83 | # user = Manager 84 | # authuser = Manager 85 | 86 | [sasl] 87 | # Set these only if your sasldb is only used by a particular application, and 88 | # in a particular domain. The default (all applications, all domains) is 89 | # probably correct for most installations. 90 | # appname = imap 91 | # domain = EXAMPLE.COM 92 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/modprobe.d/modprobe.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | {% for fs in os_unused_filesystems | difference(os_filesystem_whitelist) | sort %} 5 | install {{fs}} /bin/true 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/pam.d/rhel_auth.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | #%PAM-1.0 5 | auth required pam_env.so 6 | auth required pam_faildelay.so delay=2000000 7 | {% if os_auth_retries|int > 0 %} 8 | auth required pam_faillock.so preauth silent audit even_deny_root deny={{ os_auth_retries }} unlock_time={{ os_auth_lockout_time }} 9 | {% endif %} 10 | {% if (os_auth_pam_sssd_enable | bool) %} 11 | auth [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet 12 | auth [default=1 ignore=ignore success=ok] pam_localuser.so 13 | {% endif %} 14 | auth sufficient pam_unix.so nullok try_first_pass 15 | {% if (os_auth_pam_sssd_enable | bool) %} 16 | auth [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet 17 | auth sufficient pam_sss.so forward_pass 18 | {% endif %} 19 | {% if os_auth_retries|int > 0 %} 20 | auth required pam_faillock.so authfail audit even_deny_root deny={{ os_auth_retries }} unlock_time={{ os_auth_lockout_time }} 21 | {% endif %} 22 | auth required pam_deny.so 23 | 24 | {% if os_auth_retries|int > 0 %} 25 | account required pam_faillock.so 26 | {% endif %} 27 | account required pam_unix.so no_pass_expiry 28 | account sufficient pam_localuser.so 29 | account sufficient pam_succeed_if.so uid < 1000 quiet 30 | {% if (os_auth_pam_sssd_enable | bool) %} 31 | account [default=bad success=ok user_unknown=ignore] pam_sss.so 32 | {% endif %} 33 | account required pam_permit.so 34 | 35 | {% if (os_auth_pam_passwdqc_enable | bool) %} 36 | password requisite pam_pwquality.so {{ os_auth_pam_pwquality_options }} 37 | {% endif %} 38 | {# NSA 2.3.3.6 Limit Password Reuse #} 39 | password requisite pam_pwhistory.so remember={{ os_auth_pw_remember }} use_authtok 40 | {# NSA 2.3.3.5 Upgrade Password Hashing Algorithm to SHA-512 #} 41 | password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok rounds={{ os_sha_crypt_min_rounds }} 42 | {% if (os_auth_pam_sssd_enable | bool) %} 43 | password sufficient pam_sss.so use_authtok 44 | {% endif %} 45 | password required pam_deny.so 46 | 47 | session optional pam_keyinit.so revoke 48 | session required pam_limits.so 49 | -session optional pam_systemd.so 50 | {% if (os_auth_pam_oddjob_mkhomedir | bool) %} 51 | session optional pam_oddjob_mkhomedir.so 52 | {% endif %} 53 | session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid 54 | session required pam_unix.so 55 | {% if (os_auth_pam_sssd_enable | bool) %} 56 | session optional pam_sss.so 57 | {% endif %} 58 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/profile.d/profile.conf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | # Disable core dumps via soft limits for all users. Compliance to this setting is voluntary and can be modified by users up to a hard limit. This setting is a sane default. 5 | ulimit -S -c 0 > /dev/null 2>&1 6 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/profile.d/tmout.sh.j2: -------------------------------------------------------------------------------- 1 | # Logout Timeout 2 | export TMOUT={{ os_security_auto_logout }} 3 | readonly TMOUT 4 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/securetty.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | # A list of TTYs, from which root can log in 5 | # see `man securetty` for reference 6 | {{ "\n".join(os_auth_root_ttys) }} 7 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/security/faillock.conf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | # 4 | # Configuration for locking the user after multiple failed 5 | # authentication attempts. 6 | # 7 | # The directory where the user files with the failure records are kept. 8 | # The default is /var/run/faillock. 9 | # dir = /var/run/faillock 10 | # 11 | # Will log the user name into the system log if the user is not found. 12 | # Enabled if option is present. 13 | # audit 14 | # 15 | # Don't print informative messages. 16 | # Enabled if option is present. 17 | # silent 18 | # 19 | # Don't log informative messages via syslog. 20 | # Enabled if option is present. 21 | # no_log_info 22 | # 23 | # Only track failed user authentications attempts for local users 24 | # in /etc/passwd and ignore centralized (AD, IdM, LDAP, etc.) users. 25 | # The `faillock` command will also no longer track user failed 26 | # authentication attempts. Enabling this option will prevent a 27 | # double-lockout scenario where a user is locked out locally and 28 | # in the centralized mechanism. 29 | # Enabled if option is present. 30 | # local_users_only 31 | # 32 | # Deny access if the number of consecutive authentication failures 33 | # for this user during the recent interval exceeds n tries. 34 | # The default is 3. 35 | deny = {{ os_auth_retries }} 36 | # 37 | # The length of the interval during which the consecutive 38 | # authentication failures must happen for the user account 39 | # lock out is n seconds. 40 | # The default is 900 (15 minutes). 41 | # fail_interval = 900 42 | # 43 | # The access will be re-enabled after n seconds after the lock out. 44 | # The value 0 has the same meaning as value `never` - the access 45 | # will not be re-enabled without resetting the faillock 46 | # entries by the `faillock` command. 47 | # The default is 600 (10 minutes). 48 | unlock_time = {{ os_auth_lockout_time }} 49 | # 50 | # Root account can become locked as well as regular accounts. 51 | # Enabled if option is present. 52 | even_deny_root 53 | # 54 | # This option implies the `even_deny_root` option. 55 | # Allow access after n seconds to root account after the 56 | # account is locked. In case the option is not specified 57 | # the value is the same as of the `unlock_time` option. 58 | # root_unlock_time = 900 59 | # 60 | # If a group name is specified with this option, members 61 | # of the group will be handled by this module the same as 62 | # the root account (the options `even_deny_root>` and 63 | # `root_unlock_time` will apply to them. 64 | # By default, the option is not set. 65 | # admin_group = 66 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/sysconfig/rhel_sysconfig_init.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | # color => new RH6.0 boot-up 5 | # verbose => old-style boot-up 6 | # anything else => new style boot-up without ANSI colors or positioning 7 | BOOTUP=color 8 | # column to start "[ OK ]" label in 9 | RES_COL=60 10 | # terminal sequence to move to that column. You could change this 11 | # to something like "tput hpa ${RES_COL}" if your terminal supports it 12 | MOVE_TO_COL="echo -en \\033[${RES_COL}G" 13 | # terminal sequence to set color to a 'success' color (currently: green) 14 | SETCOLOR_SUCCESS="echo -en \\033[0;32m" 15 | # terminal sequence to set color to a 'failure' color (currently: red) 16 | SETCOLOR_FAILURE="echo -en \\033[0;31m" 17 | # terminal sequence to set color to a 'warning' color (currently: yellow) 18 | SETCOLOR_WARNING="echo -en \\033[0;33m" 19 | # terminal sequence to reset to the default color. 20 | SETCOLOR_NORMAL="echo -en \\033[0;39m" 21 | # Set to anything other than 'no' to allow hotkey interactive startup... 22 | PROMPT={{ 'yes' if (os_security_init_prompt|bool) else 'no' }} 23 | # Set to 'yes' to allow probing for devices with swap signatures 24 | AUTOSWAP=no 25 | # What ttys should gettys be started on? 26 | ACTIVE_CONSOLES=/dev/tty[1-6] 27 | # Set to '/sbin/sulogin' to prompt for password on single-user mode 28 | # Set to '/sbin/sushell' otherwise 29 | SINGLE={{ '/sbin/sulogin' if os_security_init_single else '/sbin/sushell' }} 30 | 31 | # NSA 2.2.4.1 Set Daemon umask 32 | umask 027 33 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/etc/systemd/coredump.conf.d/coredumps.conf.j2: -------------------------------------------------------------------------------- 1 | [Coredump] 2 | Storage=none 3 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/usr/share/pam-configs/pam_faillock.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | Name: faillock boilerplate 5 | Default: yes 6 | Priority: 1024 7 | Auth-Type: Primary 8 | Auth: 9 | required pam_faillock.so preauth 10 | Account-Type: Primary 11 | Account: 12 | required pam_faillock.so 13 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/usr/share/pam-configs/pam_faillock_authfail.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | Name: faillock lockout after failed attempts enforcement 5 | Default: yes 6 | Priority: 0 7 | Auth-Type: Primary 8 | Auth: 9 | required pam_faillock.so authfail 10 | 11 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/usr/share/pam-configs/pam_passwdqc.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | Name: passwdqc password strength enforcement 5 | Default: yes 6 | Priority: 1024 7 | Conflicts: cracklib 8 | Password-Type: Primary 9 | Password: 10 | requisite pam_passwdqc.so {{ os_auth_pam_passwdqc_options }} 11 | -------------------------------------------------------------------------------- /roles/os_hardening/templates/usr/share/pam-configs/pam_tally2.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | Name: tally2 lockout after failed attempts enforcement 5 | Default: yes 6 | Priority: 1024 7 | Conflicts: cracklib 8 | Auth-Type: Primary 9 | Auth-Initial: 10 | required pam_tally2.so deny={{ os_auth_retries }} onerr=fail unlock_time={{ os_auth_lockout_time }} 11 | Account-Type: Primary 12 | Account-Initial: 13 | required pam_tally2.so 14 | -------------------------------------------------------------------------------- /roles/os_hardening/vars/Amazon.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os_packages_pam_ccreds: pam_ccreds 3 | os_nologin_shell_path: /sbin/nologin 4 | 5 | # Different distros use different standards for /etc/shadow perms, e.g. 6 | # RHEL derivatives use root:root 0000, whereas Debian-based use root:shadow 0640. 7 | # You must provide key/value pairs for owner, group, and mode if overriding. 8 | os_shadow_perms: 9 | owner: root 10 | group: root 11 | mode: "0000" 12 | 13 | os_passwd_perms: 14 | owner: root 15 | group: root 16 | mode: "0644" 17 | 18 | os_env_umask: "077" 19 | 20 | os_auth_uid_min: 1000 21 | os_auth_uid_max: 60000 22 | os_auth_gid_min: 1000 23 | os_auth_gid_max: 60000 24 | os_auth_sys_uid_min: 201 25 | os_auth_sys_uid_max: 999 26 | os_auth_sys_gid_min: 201 27 | os_auth_sys_gid_max: 999 28 | os_auth_sub_uid_min: 100000 29 | os_auth_sub_uid_max: 600100000 30 | os_auth_sub_uid_count: 65536 31 | os_auth_sub_gid_min: 100000 32 | os_auth_sub_gid_max: 600100000 33 | os_auth_sub_gid_count: 65536 34 | 35 | os_auth_pam_sssd_enable: false 36 | 37 | os_mnt_boot_dir_mode: '0700' 38 | os_mnt_boot_group: 'root' 39 | os_mnt_boot_owner: 'root' 40 | 41 | os_mnt_dev_dir_mode: '0755' 42 | os_mnt_dev_group: 'root' 43 | os_mnt_dev_owner: 'root' 44 | 45 | os_mnt_dev_shm_dir_mode: '1777' 46 | os_mnt_dev_shm_group: 'root' 47 | os_mnt_dev_shm_owner: 'root' 48 | 49 | os_mnt_home_dir_mode: '0755' 50 | os_mnt_home_group: 'root' 51 | os_mnt_home_owner: 'root' 52 | 53 | os_mnt_run_dir_mode: '0755' 54 | os_mnt_run_group: 'root' 55 | os_mnt_run_owner: 'root' 56 | 57 | os_mnt_tmp_dir_mode: '1777' 58 | os_mnt_tmp_group: 'root' 59 | os_mnt_tmp_owner: 'root' 60 | 61 | os_mnt_var_dir_mode: '0755' 62 | os_mnt_var_group: 'root' 63 | os_mnt_var_owner: 'root' 64 | 65 | os_mnt_var_log_dir_mode: '0755' 66 | os_mnt_var_log_group: 'root' 67 | os_mnt_var_log_owner: 'root' 68 | 69 | os_mnt_var_log_audit_dir_mode: '0700' 70 | os_mnt_var_log_audit_group: 'root' 71 | os_mnt_var_log_audit_owner: 'root' 72 | 73 | os_mnt_var_tmp_dir_mode: '1777' 74 | os_mnt_var_tmp_group: 'root' 75 | os_mnt_var_tmp_owner: 'root' 76 | 77 | # defaults for useradd 78 | os_useradd_mail_dir: /var/spool/mail 79 | os_useradd_create_home: true 80 | 81 | modprobe_package: module-init-tools 82 | auditd_package: audit 83 | 84 | # system accounts that do not get their login disabled and password changed 85 | os_always_ignore_users: [root, sync, shutdown, halt, ec2-user] 86 | hidepid_option: "2" # allowed values: 0, 1, 2 87 | -------------------------------------------------------------------------------- /roles/os_hardening/vars/Archlinux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os_nologin_shell_path: /sbin/nologin 3 | 4 | os_shadow_perms: 5 | owner: root 6 | group: root 7 | mode: "0600" 8 | 9 | os_passwd_perms: 10 | owner: root 11 | group: root 12 | mode: "0644" 13 | 14 | os_env_umask: "027" 15 | 16 | os_auth_uid_min: 1000 17 | os_auth_uid_max: 60000 18 | os_auth_gid_min: 1000 19 | os_auth_gid_max: 60000 20 | os_auth_sys_uid_min: 500 21 | os_auth_sys_uid_max: 999 22 | os_auth_sys_gid_min: 500 23 | os_auth_sys_gid_max: 999 24 | os_auth_sub_uid_min: 100000 25 | os_auth_sub_uid_max: 600100000 26 | os_auth_sub_uid_count: 65536 27 | os_auth_sub_gid_min: 100000 28 | os_auth_sub_gid_max: 600100000 29 | os_auth_sub_gid_count: 65536 30 | 31 | os_mnt_boot_dir_mode: '0700' 32 | os_mnt_boot_group: 'root' 33 | os_mnt_boot_owner: 'root' 34 | 35 | os_mnt_dev_dir_mode: '0755' 36 | os_mnt_dev_group: 'root' 37 | os_mnt_dev_owner: 'root' 38 | 39 | os_mnt_dev_shm_dir_mode: '1777' 40 | os_mnt_dev_shm_group: 'root' 41 | os_mnt_dev_shm_owner: 'root' 42 | 43 | os_mnt_home_dir_mode: '0755' 44 | os_mnt_home_group: 'root' 45 | os_mnt_home_owner: 'root' 46 | 47 | os_mnt_run_dir_mode: '0755' 48 | os_mnt_run_group: 'root' 49 | os_mnt_run_owner: 'root' 50 | 51 | os_mnt_tmp_dir_mode: '1777' 52 | os_mnt_tmp_group: 'root' 53 | os_mnt_tmp_owner: 'root' 54 | 55 | os_mnt_var_dir_mode: '0755' 56 | os_mnt_var_group: 'root' 57 | os_mnt_var_owner: 'root' 58 | 59 | os_mnt_var_log_dir_mode: '0755' 60 | os_mnt_var_log_group: 'root' 61 | os_mnt_var_log_owner: 'root' 62 | 63 | os_mnt_var_log_audit_dir_mode: '0700' 64 | os_mnt_var_log_audit_group: 'root' 65 | os_mnt_var_log_audit_owner: 'root' 66 | 67 | os_mnt_var_tmp_dir_mode: '1777' 68 | os_mnt_var_tmp_group: 'root' 69 | os_mnt_var_tmp_owner: 'root' 70 | 71 | modprobe_package: kmod 72 | auditd_package: audit 73 | 74 | hidepid_option: "2" # allowed values: 0, 1, 2 75 | 76 | sysctl_custom_config: 77 | # Mitigation of vulnerability CVE-2021-33909 78 | kernel.unprivileged_userns_clone: 0 79 | # Mitigation of vulnerability CVE-2021-33910 80 | kernel.unprivileged_bpf_disabled: 1 81 | -------------------------------------------------------------------------------- /roles/os_hardening/vars/Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os_packages_pam_ccreds: libpam-ccreds 3 | os_nologin_shell_path: /usr/sbin/nologin 4 | 5 | # Different distros use different standards for /etc/shadow perms, e.g. 6 | # RHEL derivatives use root:root 0000, whereas Debian-based use root:shadow 0640. 7 | # You must provide key/value pairs for owner, group, and mode if overriding. 8 | os_shadow_perms: 9 | owner: root 10 | group: shadow 11 | mode: "0640" 12 | 13 | os_passwd_perms: 14 | owner: root 15 | group: root 16 | mode: "0644" 17 | 18 | os_env_umask: "027" 19 | 20 | os_auth_uid_min: 1000 21 | os_auth_uid_max: 60000 22 | os_auth_gid_min: 1000 23 | os_auth_gid_max: 60000 24 | os_auth_sys_uid_min: 100 25 | os_auth_sys_uid_max: 999 26 | os_auth_sys_gid_min: 100 27 | os_auth_sys_gid_max: 999 28 | os_auth_sub_uid_min: 100000 29 | os_auth_sub_uid_max: 600100000 30 | os_auth_sub_uid_count: 65536 31 | os_auth_sub_gid_min: 100000 32 | os_auth_sub_gid_max: 600100000 33 | os_auth_sub_gid_count: 65536 34 | 35 | os_mnt_boot_dir_mode: '0700' 36 | os_mnt_boot_group: 'root' 37 | os_mnt_boot_owner: 'root' 38 | 39 | os_mnt_dev_dir_mode: '0755' 40 | os_mnt_dev_group: 'root' 41 | os_mnt_dev_owner: 'root' 42 | 43 | os_mnt_dev_shm_dir_mode: '1777' 44 | os_mnt_dev_shm_group: 'root' 45 | os_mnt_dev_shm_owner: 'root' 46 | 47 | os_mnt_home_dir_mode: '0755' 48 | os_mnt_home_group: 'root' 49 | os_mnt_home_owner: 'root' 50 | 51 | os_mnt_run_dir_mode: '0755' 52 | os_mnt_run_group: 'root' 53 | os_mnt_run_owner: 'root' 54 | 55 | os_mnt_tmp_dir_mode: '1777' 56 | os_mnt_tmp_group: 'root' 57 | os_mnt_tmp_owner: 'root' 58 | 59 | os_mnt_var_dir_mode: '0755' 60 | os_mnt_var_group: 'root' 61 | os_mnt_var_owner: 'root' 62 | 63 | os_mnt_var_log_dir_mode: '0755' 64 | os_mnt_var_log_group: 'root' 65 | os_mnt_var_log_owner: 'root' 66 | 67 | os_mnt_var_log_audit_dir_mode: '0700' 68 | os_mnt_var_log_audit_group: 'root' 69 | os_mnt_var_log_audit_owner: 'root' 70 | 71 | os_mnt_var_tmp_dir_mode: '1777' 72 | os_mnt_var_tmp_group: 'root' 73 | os_mnt_var_tmp_owner: 'root' 74 | 75 | # defaults for useradd 76 | os_useradd_mail_dir: /var/mail 77 | 78 | modprobe_package: kmod 79 | auditd_package: auditd 80 | 81 | tally2_path: /usr/share/pam-configs/tally2 82 | passwdqc_path: /usr/share/pam-configs/passwdqc 83 | 84 | hidepid_option: "2" # allowed values: 0, 1, 2 85 | 86 | sysctl_custom_config: 87 | # Mitigation of vulnerability CVE-2021-33909 88 | kernel.unprivileged_userns_clone: 0 89 | # Mitigation of vulnerability CVE-2021-33910 90 | kernel.unprivileged_bpf_disabled: 1 91 | -------------------------------------------------------------------------------- /roles/os_hardening/vars/Fedora.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os_packages_pam_ccreds: pam_ccreds 3 | os_nologin_shell_path: /sbin/nologin 4 | 5 | # Different distros use different standards for /etc/shadow perms, e.g. 6 | # RHEL derivatives use root:root 0000, whereas Debian-based use root:shadow 0640. 7 | # You must provide key/value pairs for owner, group, and mode if overriding. 8 | os_shadow_perms: 9 | owner: root 10 | group: root 11 | mode: "0000" 12 | 13 | os_passwd_perms: 14 | owner: root 15 | group: root 16 | mode: "0644" 17 | 18 | os_env_umask: "027" 19 | 20 | os_auth_uid_min: 1000 21 | os_auth_uid_max: 60000 22 | os_auth_gid_min: 1000 23 | os_auth_gid_max: 60000 24 | os_auth_sys_uid_min: 201 25 | os_auth_sys_uid_max: 999 26 | os_auth_sys_gid_min: 201 27 | os_auth_sys_gid_max: 999 28 | os_auth_sub_uid_min: 100000 29 | os_auth_sub_uid_max: 600100000 30 | os_auth_sub_uid_count: 65536 31 | os_auth_sub_gid_min: 100000 32 | os_auth_sub_gid_max: 600100000 33 | os_auth_sub_gid_count: 65536 34 | 35 | os_auth_pam_sssd_enable: true 36 | 37 | os_mnt_boot_dir_mode: '0700' 38 | os_mnt_boot_group: 'root' 39 | os_mnt_boot_owner: 'root' 40 | 41 | os_mnt_dev_dir_mode: '0755' 42 | os_mnt_dev_group: 'root' 43 | os_mnt_dev_owner: 'root' 44 | 45 | os_mnt_dev_shm_dir_mode: '1777' 46 | os_mnt_dev_shm_group: 'root' 47 | os_mnt_dev_shm_owner: 'root' 48 | 49 | os_mnt_home_dir_mode: '0755' 50 | os_mnt_home_group: 'root' 51 | os_mnt_home_owner: 'root' 52 | 53 | os_mnt_run_dir_mode: '0755' 54 | os_mnt_run_group: 'root' 55 | os_mnt_run_owner: 'root' 56 | 57 | os_mnt_tmp_dir_mode: '1777' 58 | os_mnt_tmp_group: 'root' 59 | os_mnt_tmp_owner: 'root' 60 | 61 | os_mnt_var_dir_mode: '0755' 62 | os_mnt_var_group: 'root' 63 | os_mnt_var_owner: 'root' 64 | 65 | os_mnt_var_log_dir_mode: '0755' 66 | os_mnt_var_log_group: 'root' 67 | os_mnt_var_log_owner: 'root' 68 | 69 | os_mnt_var_log_audit_dir_mode: '0700' 70 | os_mnt_var_log_audit_group: 'root' 71 | os_mnt_var_log_audit_owner: 'root' 72 | 73 | os_mnt_var_tmp_dir_mode: '1777' 74 | os_mnt_var_tmp_group: 'root' 75 | os_mnt_var_tmp_owner: 'root' 76 | 77 | # defaults for useradd 78 | os_useradd_mail_dir: /var/spool/mail 79 | os_useradd_create_home: true 80 | 81 | modprobe_package: module-init-tools 82 | auditd_package: audit 83 | 84 | hidepid_option: "2" # allowed values: 0, 1, 2 85 | -------------------------------------------------------------------------------- /roles/os_hardening/vars/RedHat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os_packages_pam_ccreds: pam_ccreds 3 | os_nologin_shell_path: /sbin/nologin 4 | 5 | # Different distros use different standards for /etc/shadow perms, e.g. 6 | # RHEL derivatives use root:root 0000, whereas Debian-based use root:shadow 0640. 7 | # You must provide key/value pairs for owner, group, and mode if overriding. 8 | os_shadow_perms: 9 | owner: root 10 | group: root 11 | mode: "0000" 12 | 13 | os_passwd_perms: 14 | owner: root 15 | group: root 16 | mode: "0644" 17 | 18 | os_env_umask: "077" 19 | 20 | os_auth_uid_min: 1000 21 | os_auth_uid_max: 60000 22 | os_auth_gid_min: 1000 23 | os_auth_gid_max: 60000 24 | os_auth_sys_uid_min: 201 25 | os_auth_sys_uid_max: 999 26 | os_auth_sys_gid_min: 201 27 | os_auth_sys_gid_max: 999 28 | os_auth_sub_uid_min: 100000 29 | os_auth_sub_uid_max: 600100000 30 | os_auth_sub_uid_count: 65536 31 | os_auth_sub_gid_min: 100000 32 | os_auth_sub_gid_max: 600100000 33 | os_auth_sub_gid_count: 65536 34 | 35 | os_auth_pam_sssd_enable: true 36 | 37 | os_mnt_boot_dir_mode: '0700' 38 | os_mnt_boot_group: 'root' 39 | os_mnt_boot_owner: 'root' 40 | 41 | os_mnt_dev_dir_mode: '0755' 42 | os_mnt_dev_group: 'root' 43 | os_mnt_dev_owner: 'root' 44 | 45 | os_mnt_dev_shm_dir_mode: '1777' 46 | os_mnt_dev_shm_group: 'root' 47 | os_mnt_dev_shm_owner: 'root' 48 | 49 | os_mnt_home_dir_mode: '0755' 50 | os_mnt_home_group: 'root' 51 | os_mnt_home_owner: 'root' 52 | 53 | os_mnt_run_dir_mode: '0755' 54 | os_mnt_run_group: 'root' 55 | os_mnt_run_owner: 'root' 56 | 57 | os_mnt_tmp_dir_mode: '1777' 58 | os_mnt_tmp_group: 'root' 59 | os_mnt_tmp_owner: 'root' 60 | 61 | os_mnt_var_dir_mode: '0755' 62 | os_mnt_var_group: 'root' 63 | os_mnt_var_owner: 'root' 64 | 65 | os_mnt_var_log_dir_mode: '0755' 66 | os_mnt_var_log_group: 'root' 67 | os_mnt_var_log_owner: 'root' 68 | 69 | os_mnt_var_log_audit_dir_mode: '0700' 70 | os_mnt_var_log_audit_group: 'root' 71 | os_mnt_var_log_audit_owner: 'root' 72 | 73 | os_mnt_var_tmp_dir_mode: '1777' 74 | os_mnt_var_tmp_group: 'root' 75 | os_mnt_var_tmp_owner: 'root' 76 | 77 | # defaults for useradd 78 | os_useradd_mail_dir: /var/spool/mail 79 | os_useradd_create_home: true 80 | 81 | modprobe_package: module-init-tools 82 | auditd_package: audit 83 | 84 | hidepid_option: "2" # allowed values: 0, 1, 2 85 | -------------------------------------------------------------------------------- /roles/os_hardening/vars/Suse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | os_packages_pam_ccreds: pam_ccreds 3 | os_nologin_shell_path: /sbin/nologin 4 | 5 | # Different distros use different standards for /etc/shadow perms, e.g. 6 | # RHEL derivatives use root:root 0000, whereas Debian-based use root:shadow 0640. 7 | # You must provide key/value pairs for owner, group, and mode if overriding. 8 | os_shadow_perms: 9 | owner: root 10 | group: shadow 11 | mode: "0640" 12 | 13 | os_passwd_perms: 14 | owner: root 15 | group: root 16 | mode: "0644" 17 | 18 | os_env_umask: "027" 19 | 20 | os_auth_uid_min: 1000 21 | os_auth_uid_max: 60000 22 | os_auth_gid_min: 1000 23 | os_auth_gid_max: 60000 24 | os_auth_sys_uid_min: 100 25 | os_auth_sys_uid_max: 499 26 | os_auth_sys_gid_min: 100 27 | os_auth_sys_gid_max: 499 28 | os_auth_sub_uid_min: 100000 29 | os_auth_sub_uid_max: 600100000 30 | os_auth_sub_uid_count: 65536 31 | os_auth_sub_gid_min: 100000 32 | os_auth_sub_gid_max: 600100000 33 | os_auth_sub_gid_count: 65536 34 | 35 | os_mnt_boot_dir_mode: '0700' 36 | os_mnt_boot_group: 'root' 37 | os_mnt_boot_owner: 'root' 38 | 39 | os_mnt_dev_dir_mode: '0755' 40 | os_mnt_dev_group: 'root' 41 | os_mnt_dev_owner: 'root' 42 | 43 | os_mnt_dev_shm_dir_mode: '1777' 44 | os_mnt_dev_shm_group: 'root' 45 | os_mnt_dev_shm_owner: 'root' 46 | 47 | os_mnt_home_dir_mode: '0755' 48 | os_mnt_home_group: 'root' 49 | os_mnt_home_owner: 'root' 50 | 51 | os_mnt_run_dir_mode: '0755' 52 | os_mnt_run_group: 'root' 53 | os_mnt_run_owner: 'root' 54 | 55 | os_mnt_tmp_dir_mode: '1777' 56 | os_mnt_tmp_group: 'root' 57 | os_mnt_tmp_owner: 'root' 58 | 59 | os_mnt_var_dir_mode: '0755' 60 | os_mnt_var_group: 'root' 61 | os_mnt_var_owner: 'root' 62 | 63 | os_mnt_var_log_dir_mode: '0755' 64 | os_mnt_var_log_group: 'root' 65 | os_mnt_var_log_owner: 'root' 66 | 67 | os_mnt_var_log_audit_dir_mode: '0700' 68 | os_mnt_var_log_audit_group: 'root' 69 | os_mnt_var_log_audit_owner: 'root' 70 | 71 | os_mnt_var_tmp_dir_mode: '1777' 72 | os_mnt_var_tmp_group: 'root' 73 | os_mnt_var_tmp_owner: 'root' 74 | 75 | # defaults for useradd 76 | os_useradd_create_home: false 77 | 78 | modprobe_package: kmod-compat 79 | auditd_package: audit 80 | 81 | hidepid_option: "2" # allowed values: 0, 1, 2 82 | 83 | sysctl_unsupported_entries: 84 | - kernel.yama.ptrace_scope # SuSE has disabled yama by default - https://bugzilla.suse.com/show_bug.cgi?id=1128245 85 | -------------------------------------------------------------------------------- /roles/os_hardening/vars/Ubuntu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | os_packages_pam_ccreds: 'libpam-ccreds' 4 | os_nologin_shell_path: '/usr/sbin/nologin' 5 | 6 | # Different distros use different standards for /etc/shadow perms, e.g. 7 | # RHEL derivatives use root:root 0000, whereas Debian-based use root:shadow 0640. 8 | # You must provide key/value pairs for owner, group, and mode if overriding. 9 | os_shadow_perms: 10 | owner: root 11 | group: shadow 12 | mode: '0640' 13 | 14 | os_passwd_perms: 15 | owner: root 16 | group: root 17 | mode: '0644' 18 | 19 | os_env_umask: '027' 20 | 21 | os_auth_uid_min: 1000 22 | os_auth_uid_max: 60000 23 | os_auth_gid_min: 1000 24 | os_auth_gid_max: 60000 25 | os_auth_sys_uid_min: 100 26 | os_auth_sys_uid_max: 999 27 | os_auth_sys_gid_min: 100 28 | os_auth_sys_gid_max: 999 29 | os_auth_sub_uid_min: 100000 30 | os_auth_sub_uid_max: 600100000 31 | os_auth_sub_uid_count: 65536 32 | os_auth_sub_gid_min: 100000 33 | os_auth_sub_gid_max: 600100000 34 | os_auth_sub_gid_count: 65536 35 | 36 | os_mnt_boot_dir_mode: '0700' 37 | os_mnt_boot_group: 'root' 38 | os_mnt_boot_owner: 'root' 39 | 40 | os_mnt_dev_dir_mode: '0755' 41 | os_mnt_dev_group: 'root' 42 | os_mnt_dev_owner: 'root' 43 | 44 | os_mnt_dev_shm_dir_mode: '1777' 45 | os_mnt_dev_shm_group: 'root' 46 | os_mnt_dev_shm_owner: 'root' 47 | 48 | os_mnt_home_dir_mode: '0755' 49 | os_mnt_home_group: 'root' 50 | os_mnt_home_owner: 'root' 51 | 52 | os_mnt_run_dir_mode: '0755' 53 | os_mnt_run_group: 'root' 54 | os_mnt_run_owner: 'root' 55 | 56 | os_mnt_tmp_dir_mode: '1777' 57 | os_mnt_tmp_group: 'root' 58 | os_mnt_tmp_owner: 'root' 59 | 60 | os_mnt_var_dir_mode: '0755' 61 | os_mnt_var_group: 'root' 62 | os_mnt_var_owner: 'root' 63 | 64 | os_mnt_var_log_dir_mode: '0775' 65 | os_mnt_var_log_group: 'syslog' 66 | os_mnt_var_log_owner: 'root' 67 | 68 | os_mnt_var_log_audit_dir_mode: '0700' 69 | os_mnt_var_log_audit_group: 'root' 70 | os_mnt_var_log_audit_owner: 'root' 71 | 72 | os_mnt_var_tmp_dir_mode: '1777' 73 | os_mnt_var_tmp_group: 'root' 74 | os_mnt_var_tmp_owner: 'root' 75 | 76 | # defaults for useradd 77 | os_useradd_mail_dir: /var/mail 78 | 79 | modprobe_package: 'kmod' 80 | auditd_package: 'auditd' 81 | 82 | tally2_path: '/usr/share/pam-configs/tally2' 83 | passwdqc_path: '/usr/share/pam-configs/passwdqc' 84 | 85 | hidepid_option: '2' # allowed values: 0, 1, 2 86 | 87 | sysctl_custom_config: 88 | # Mitigation of vulnerability CVE-2021-33909 89 | kernel.unprivileged_userns_clone: 0 90 | # Mitigation of vulnerability CVE-2021-33910 91 | kernel.unprivileged_bpf_disabled: 1 92 | -------------------------------------------------------------------------------- /roles/ssh_hardening/files/ssh_password: -------------------------------------------------------------------------------- 1 | module ssh_password 1.0; 2 | 3 | require { 4 | type sshd_t; 5 | type shadow_t; 6 | class file { read open }; 7 | } 8 | 9 | #============= sshd_t ============== 10 | allow sshd_t shadow_t:file { read open }; 11 | -------------------------------------------------------------------------------- /roles/ssh_hardening/files/sshd: -------------------------------------------------------------------------------- 1 | # Configuration file for the sshd service. 2 | 3 | # The server keys are automatically generated if they are missing. 4 | # To change the automatic creation, adjust sshd.service options for 5 | # example using systemctl enable sshd-keygen@dsa.service to allow creation 6 | # of DSA key or systemctl mask sshd-keygen@rsa.service to disable RSA key 7 | # creation. 8 | 9 | # Do not change this option unless you have hardware random 10 | # generator and you REALLY know what you are doing 11 | 12 | SSH_USE_STRONG_RNG=0 13 | # SSH_USE_STRONG_RNG=1 14 | 15 | # System-wide crypto policy: 16 | # To opt-out, uncomment the following line 17 | CRYPTO_POLICY= 18 | -------------------------------------------------------------------------------- /roles/ssh_hardening/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart sshd 3 | ansible.builtin.service: 4 | name: "{{ sshd_service_name }}" 5 | state: restarted 6 | when: ssh_server_enabled | bool 7 | become: true 8 | -------------------------------------------------------------------------------- /roles/ssh_hardening/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Sebastian Gumprich 4 | description: This Ansible role provides numerous security-related ssh configurations, providing all-round base protection. 5 | company: Hardening Framework Team 6 | license: Apache License 2.0 7 | min_ansible_version: 2.9.10 8 | platforms: 9 | - name: EL 10 | versions: 11 | - "8" 12 | - "9" 13 | - name: Ubuntu 14 | versions: 15 | - focal 16 | - jammy 17 | - noble 18 | - name: Debian 19 | versions: 20 | - bookworm 21 | - bullseye 22 | - name: Alpine 23 | - name: Amazon 24 | - name: Fedora 25 | - name: ArchLinux 26 | - name: SmartOS 27 | - name: FreeBSD 28 | versions: 29 | - "13.2" 30 | - "14.0" 31 | - name: OpenBSD 32 | versions: 33 | - "7.0" 34 | galaxy_tags: 35 | - system 36 | - security 37 | - hardening 38 | dependencies: [] 39 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/ca_keys_and_principals.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set ssh CA pub keys 3 | ansible.builtin.template: 4 | src: trusted_user_ca_keys.j2 5 | dest: "{{ ssh_trusted_user_ca_keys_file }}" 6 | mode: "0644" 7 | owner: "{{ ssh_owner }}" 8 | group: "{{ ssh_group }}" 9 | notify: Restart sshd 10 | 11 | - name: Create ssh authorized principals directories 12 | ansible.builtin.file: 13 | path: "{{ item.path | dirname }}" 14 | mode: '{{ item.directorymode | default("700") }}' 15 | owner: "{{ item.directoryowner | default(ssh_owner) }}" 16 | group: "{{ item.directorygroup | default(ssh_group) }}" 17 | state: directory 18 | loop: "{{ ssh_authorized_principals }}" 19 | 20 | - name: Set ssh authorized principals 21 | ansible.builtin.template: 22 | src: authorized_principals.j2 23 | dest: "{{ item.path }}" 24 | mode: '{{ item.filemode | default("600") }}' 25 | owner: "{{ item.owner | default(ssh_owner) }}" 26 | group: "{{ item.group | default(ssh_group) }}" 27 | loop: "{{ ssh_authorized_principals }}" 28 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/crypto_ciphers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set ciphers according to openssh-version if openssh >= 5.3 3 | ansible.builtin.set_fact: 4 | ssh_ciphers: "{{ ssh_ciphers_53_default }}" 5 | when: sshd_version is version('5.3', '>=') 6 | 7 | - name: Set ciphers according to openssh-version if openssh >= 6.6 8 | ansible.builtin.set_fact: 9 | ssh_ciphers: "{{ ssh_ciphers_66_default }}" 10 | when: sshd_version is version('6.6', '>=') 11 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/crypto_hostkeys.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Replace default 2048 bits RSA keypair 3 | community.crypto.openssh_keypair: 4 | state: present 5 | type: rsa 6 | size: "{{ ssh_host_rsa_key_size }}" 7 | path: "{{ ssh_host_keys_dir }}/ssh_host_rsa_key" 8 | force: false 9 | regenerate: partial_idempotence 10 | 11 | - name: Set hostkeys according to openssh-version if openssh >= 5.3 12 | ansible.builtin.set_fact: 13 | ssh_host_key_files: 14 | - "{{ ssh_host_keys_dir }}/ssh_host_rsa_key" 15 | when: sshd_version is version('5.3', '>=') 16 | 17 | - name: Set hostkeys according to openssh-version if openssh >= 6.0 18 | ansible.builtin.set_fact: 19 | ssh_host_key_files: 20 | - "{{ ssh_host_keys_dir }}/ssh_host_rsa_key" 21 | - "{{ ssh_host_keys_dir }}/ssh_host_ecdsa_key" 22 | when: sshd_version is version('6.0', '>=') 23 | 24 | - name: Set hostkeys according to openssh-version if openssh >= 6.3 25 | ansible.builtin.set_fact: 26 | ssh_host_key_files: 27 | - "{{ ssh_host_keys_dir }}/ssh_host_rsa_key" 28 | - "{{ ssh_host_keys_dir }}/ssh_host_ecdsa_key" 29 | - "{{ ssh_host_keys_dir }}/ssh_host_ed25519_key" 30 | when: sshd_version is version('6.3', '>=') 31 | 32 | - name: Change host private key ownership, group and permissions 33 | ansible.builtin.file: 34 | path: "{{ item }}" 35 | owner: "{{ ssh_host_keys_owner }}" 36 | group: "{{ ssh_host_keys_group }}" 37 | mode: "{{ ssh_host_keys_mode }}" 38 | loop: "{{ ssh_host_key_files }}" 39 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/crypto_kex.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set kex according to openssh-version if openssh >= 5.9 3 | ansible.builtin.set_fact: 4 | ssh_kex: "{{ ssh_kex_59_default }}" 5 | when: sshd_version is version('5.9', '>=') 6 | 7 | - name: Set kex according to openssh-version if openssh >= 6.6 8 | ansible.builtin.set_fact: 9 | ssh_kex: "{{ ssh_kex_66_default }}" 10 | when: sshd_version is version('6.6', '>=') 11 | 12 | - name: Set kex according to openssh-version if openssh >= 8.0 13 | ansible.builtin.set_fact: 14 | ssh_kex: "{{ ssh_kex_80_default }}" 15 | when: sshd_version is version('8.0', '>=') 16 | 17 | - name: Set kex according to openssh-version if openssh >= 8.5 18 | ansible.builtin.set_fact: 19 | ssh_kex: "{{ ssh_kex_85_default }}" 20 | when: sshd_version is version('8.5', '>=') 21 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/crypto_macs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set macs according to openssh-version if openssh >= 5.3 3 | ansible.builtin.set_fact: 4 | ssh_macs: "{{ ssh_macs_53_default }}" 5 | when: sshd_version is version('5.3', '>=') 6 | 7 | - name: Set macs according to openssh-version if openssh >= 5.9 8 | ansible.builtin.set_fact: 9 | ssh_macs: "{{ ssh_macs_59_default }}" 10 | when: sshd_version is version('5.9', '>=') 11 | 12 | - name: Set macs according to openssh-version if openssh >= 6.6 13 | ansible.builtin.set_fact: 14 | ssh_macs: "{{ ssh_macs_66_default }}" 15 | when: sshd_version is version('6.6', '>=') 16 | 17 | - name: Set macs according to openssh-version if openssh >= 7.6 18 | ansible.builtin.set_fact: 19 | ssh_macs: "{{ ssh_macs_76_default }}" 20 | when: sshd_version is version('7.6', '>=') 21 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/disable-systemd-socket.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Remove ssh service systemd-socket file 3 | ansible.builtin.file: 4 | path: "{{ item }}" 5 | state: absent 6 | loop: 7 | - /etc/systemd/system/ssh.service.d/00-socket.conf 8 | - /etc/systemd/system/ssh.service.requires/ssh.socket 9 | - /etc/systemd/system/sockets.target.wants/ssh.socket 10 | 11 | - name: Disable systemd-socket activation 12 | ansible.builtin.systemd: 13 | name: ssh.socket 14 | state: stopped 15 | enabled: false 16 | masked: true 17 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install openssh package(s) 4 | ansible.builtin.package: 5 | name: "{{ pkg }}" 6 | state: present 7 | loop: "{{ ssh_pkgs }}" 8 | loop_control: 9 | loop_var: pkg 10 | 11 | # see https://github.com/dev-sec/ansible-collection-hardening/issues/763 12 | - name: Change Debian/Ubuntu systems so ssh starts traditionally instead of socket-activated 13 | ansible.builtin.include_tasks: disable-systemd-socket.yml 14 | when: 15 | - ssh_server_hardening | bool 16 | - ssh_server_enabled | bool 17 | - (ansible_facts.distribution == 'Ubuntu' and ansible_facts.distribution_major_version is version('22.04', '>=')) or 18 | (ansible_facts.os_family == 'Debian' and ansible_facts.distribution_major_version is version('12', '>=')) 19 | 20 | - name: Ensure privilege separation directory exists 21 | ansible.builtin.file: 22 | path: /run/sshd 23 | state: directory 24 | owner: root 25 | group: root 26 | mode: '0755' 27 | when: 28 | - ssh_server_hardening | bool 29 | - ssh_server_enabled | bool 30 | - ansible_facts.os_family == 'Debian' 31 | 32 | - name: Enable or disable sshd service 33 | ansible.builtin.service: 34 | name: "{{ sshd_service_name }}" 35 | enabled: "{{ ssh_server_service_enabled }}" 36 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include hardening tasks 3 | ansible.builtin.include_tasks: hardening.yml 4 | args: 5 | apply: 6 | become: true 7 | when: ssh_hardening_enabled | bool 8 | -------------------------------------------------------------------------------- /roles/ssh_hardening/tasks/selinux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install selinux dependencies when selinux is installed 3 | ansible.builtin.package: 4 | name: "{{ ssh_selinux_packages }}" 5 | state: present 6 | 7 | - name: Authorize the following ports for selinux - {{ ssh_server_ports }} 8 | community.general.seport: 9 | ports: "{{ item }}" 10 | proto: tcp 11 | setype: ssh_port_t 12 | state: present 13 | loop: "{{ ssh_server_ports }}" 14 | 15 | - name: Check if ssh_password module is already installed 16 | ansible.builtin.shell: set -o pipefail && semodule -l | grep ssh_password 17 | args: 18 | executable: /bin/bash 19 | register: ssh_password_module 20 | failed_when: false 21 | changed_when: false 22 | check_mode: false 23 | 24 | # The following tasks only get executed when selinux is in state enforcing, 25 | # UsePam is 'no' and the ssh_password module is not installed. See this issue for 26 | # more info: https://github.com/hardening-io/ansible-ssh-hardening/issues/23 27 | - name: Run selinux tasks 28 | when: 29 | - not (ssh_use_pam | bool) 30 | - ('ssh_password' not in ssh_password_module.stdout) 31 | block: 32 | - name: Create selinux custom policy drop folder 33 | ansible.builtin.file: 34 | path: "{{ ssh_custom_selinux_dir }}" 35 | state: directory 36 | owner: root 37 | group: root 38 | mode: "0750" 39 | 40 | - name: Distributing custom selinux policies 41 | ansible.builtin.copy: 42 | src: ssh_password 43 | dest: "{{ ssh_custom_selinux_dir }}" 44 | owner: root 45 | group: root 46 | mode: "0600" 47 | 48 | - name: Check and compile policy # noqa no-changed-when 49 | ansible.builtin.command: checkmodule -M -m -o {{ ssh_custom_selinux_dir }}/ssh_password.mod {{ ssh_custom_selinux_dir }}/ssh_password 50 | 51 | - name: Create selinux policy module package # noqa no-changed-when 52 | ansible.builtin.command: semodule_package -o {{ ssh_custom_selinux_dir }}/ssh_password.pp -m {{ ssh_custom_selinux_dir }}/ssh_password.mod 53 | 54 | - name: Install selinux policy # noqa no-changed-when 55 | ansible.builtin.command: semodule -i {{ ssh_custom_selinux_dir }}/ssh_password.pp 56 | 57 | # The following tasks only get executed when selinux is installed, UsePam is 58 | # 'yes' and the ssh_password module is installed. See 59 | # http://danwalsh.livejournal.com/12333.html for more info 60 | - name: Remove selinux-policy when PAM is used, because allowing sshd to read the shadow file is considered a security risk # noqa no-changed-when 61 | ansible.builtin.command: semodule -r ssh_password 62 | when: 63 | - ssh_use_pam | bool 64 | - ('ssh_password' in ssh_password_module.stdout) 65 | -------------------------------------------------------------------------------- /roles/ssh_hardening/templates/authorized_principals.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | {% for principal in item.principals %} 5 | {{ principal }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /roles/ssh_hardening/templates/revoked_keys.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | {% for key in ssh_server_revoked_keys %} 5 | {{ key }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /roles/ssh_hardening/templates/trusted_user_ca_keys.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | # Generated by Ansible role {{ ansible_role_name }} 3 | 4 | {% for key in ssh_trusted_user_ca_keys %} 5 | {{ key }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/Alpine.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: root 11 | ssh_host_keys_mode: "0600" 12 | 13 | # true if SSH support Kerberos 14 | ssh_kerberos_support: true 15 | 16 | # true if SSH has PAM support 17 | ssh_pam_support: true 18 | 19 | sshd_moduli_file: /etc/ssh/moduli 20 | 21 | # CRYPTO_POLICY is not supported on Archlinux 22 | # and the package check only works in Ansible >2.10 23 | sshd_disable_crypto_policy: false 24 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/Amazon_2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: ssh_keys 11 | ssh_host_keys_mode: "0600" 12 | ssh_selinux_packages: 13 | - policycoreutils-python 14 | - checkpolicy 15 | 16 | # true if SSH support Kerberos 17 | ssh_kerberos_support: true 18 | 19 | # true if SSH has PAM support 20 | ssh_pam_support: true 21 | 22 | sshd_moduli_file: /etc/ssh/moduli 23 | 24 | # disable CRYPTO_POLICY to take settings from sshd configuration 25 | # see: https://access.redhat.com/solutions/4410591 26 | sshd_disable_crypto_policy: true 27 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/Archlinux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: root 11 | ssh_host_keys_mode: "0600" 12 | 13 | # true if SSH support Kerberos 14 | ssh_kerberos_support: true 15 | 16 | # true if SSH has PAM support 17 | ssh_pam_support: true 18 | 19 | sshd_moduli_file: /etc/ssh/moduli 20 | 21 | # CRYPTO_POLICY is not supported on Archlinux 22 | # and the package check only works in Ansible >2.10 23 | sshd_disable_crypto_policy: false 24 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh-server 4 | - openssh-client 5 | sshd_path: /usr/sbin/sshd 6 | ssh_host_keys_dir: /etc/ssh 7 | sshd_service_name: ssh 8 | ssh_owner: root 9 | ssh_group: root 10 | ssh_host_keys_owner: root 11 | ssh_host_keys_group: root 12 | ssh_host_keys_mode: "0600" 13 | ssh_selinux_packages: 14 | - policycoreutils-python-utils 15 | - checkpolicy 16 | 17 | # true if SSH support Kerberos 18 | ssh_kerberos_support: true 19 | 20 | # true if SSH has PAM support 21 | ssh_pam_support: true 22 | 23 | sshd_moduli_file: /etc/ssh/moduli 24 | 25 | sshd_disable_crypto_policy: false 26 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/Fedora.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: root 11 | ssh_host_keys_mode: "0600" 12 | ssh_selinux_packages: 13 | - python3-policycoreutils 14 | - checkpolicy 15 | 16 | # true if SSH support Kerberos 17 | ssh_kerberos_support: true 18 | 19 | # true if SSH has PAM support 20 | ssh_pam_support: true 21 | 22 | sshd_moduli_file: /etc/ssh/moduli 23 | 24 | # disable CRYPTO_POLICY to take settings from sshd configuration 25 | # see: https://access.redhat.com/solutions/4410591 26 | sshd_disable_crypto_policy: true 27 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/Fedora_37.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: ssh_keys 11 | ssh_host_keys_mode: "0600" 12 | ssh_selinux_packages: 13 | - python3-policycoreutils 14 | - checkpolicy 15 | 16 | # true if SSH support Kerberos 17 | ssh_kerberos_support: true 18 | 19 | # true if SSH has PAM support 20 | ssh_pam_support: true 21 | 22 | sshd_moduli_file: /etc/ssh/moduli 23 | 24 | # disable CRYPTO_POLICY to take settings from sshd configuration 25 | # see: https://access.redhat.com/solutions/4410591 26 | sshd_disable_crypto_policy: true 27 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/FreeBSD.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh-portable 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: wheel 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: wheel 11 | ssh_host_keys_mode: "0600" 12 | 13 | # true if SSH support Kerberos 14 | ssh_kerberos_support: true 15 | 16 | # true if SSH has PAM support 17 | ssh_pam_support: true 18 | 19 | sshd_moduli_file: /etc/ssh/moduli 20 | 21 | sshd_disable_crypto_policy: false 22 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/OpenBSD.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # It seems that OpenBSD comes with openssh in the base install so we can't 3 | # really install it as a separated package. Feel free to patch this if it's 4 | # not the expected way to handle that on OpenBSD 5 | ssh_pkgs: [] 6 | sshd_path: /usr/sbin/sshd 7 | ssh_host_keys_dir: /etc/ssh 8 | sshd_service_name: sshd 9 | ssh_owner: root 10 | ssh_group: wheel 11 | ssh_host_keys_owner: root 12 | ssh_host_keys_group: wheel 13 | ssh_host_keys_mode: "0600" 14 | 15 | # true if SSH support Kerberos 16 | ssh_kerberos_support: false 17 | 18 | # true if SSH has PAM support 19 | ssh_pam_support: false 20 | 21 | sshd_moduli_file: /etc/moduli 22 | 23 | sshd_disable_crypto_policy: false 24 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/RedHat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: ssh_keys 11 | ssh_host_keys_mode: "0600" 12 | ssh_selinux_packages: 13 | - policycoreutils-python-utils 14 | - checkpolicy 15 | 16 | # true if SSH support Kerberos 17 | ssh_kerberos_support: true 18 | 19 | # true if SSH has PAM support 20 | ssh_pam_support: true 21 | 22 | sshd_moduli_file: /etc/ssh/moduli 23 | 24 | # disable CRYPTO_POLICY to take settings from sshd configuration 25 | # see: https://access.redhat.com/solutions/4410591 26 | sshd_disable_crypto_policy: true 27 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/RedHat_9.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: root 11 | ssh_host_keys_mode: "0600" 12 | ssh_selinux_packages: 13 | - policycoreutils-python-utils 14 | - checkpolicy 15 | 16 | # true if SSH support Kerberos 17 | ssh_kerberos_support: true 18 | 19 | # true if SSH has PAM support 20 | ssh_pam_support: true 21 | 22 | sshd_moduli_file: /etc/ssh/moduli 23 | 24 | # disable CRYPTO_POLICY to take settings from sshd configuration 25 | # see: https://access.redhat.com/solutions/4410591 26 | sshd_disable_crypto_policy: true 27 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/SmartOS.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/lib/ssh/sshd 5 | ssh_host_keys_dir: /var/ssh 6 | sshd_service_name: ssh 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: root 11 | ssh_host_keys_mode: "0600" 12 | 13 | # true if SSH support Kerberos 14 | ssh_kerberos_support: true 15 | 16 | # true if SSH has PAM support 17 | ssh_pam_support: false 18 | 19 | sshd_moduli_file: /etc/ssh/moduli 20 | 21 | sshd_disable_crypto_policy: false 22 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/Suse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssh_pkgs: 3 | - openssh 4 | sshd_path: /usr/sbin/sshd 5 | ssh_host_keys_dir: /etc/ssh 6 | sshd_service_name: sshd 7 | ssh_owner: root 8 | ssh_group: root 9 | ssh_host_keys_owner: root 10 | ssh_host_keys_group: root 11 | ssh_host_keys_mode: "0600" 12 | 13 | # true if SSH support Kerberos 14 | ssh_kerberos_support: true 15 | 16 | # true if SSH has PAM support 17 | ssh_pam_support: true 18 | 19 | sshd_moduli_file: /etc/ssh/moduli 20 | 21 | sshd_disable_crypto_policy: false 22 | -------------------------------------------------------------------------------- /roles/ssh_hardening/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ssh_macs_53_default: 4 | - hmac-ripemd160 5 | - hmac-sha1 6 | 7 | ssh_macs_59_default: 8 | - hmac-sha2-512 9 | - hmac-sha2-256 10 | - hmac-ripemd160 11 | 12 | ssh_macs_66_default: 13 | - hmac-sha2-512-etm@openssh.com 14 | - hmac-sha2-256-etm@openssh.com 15 | - umac-128-etm@openssh.com 16 | - hmac-sha2-512 17 | - hmac-sha2-256 18 | 19 | ssh_macs_76_default: 20 | - hmac-sha2-512-etm@openssh.com 21 | - hmac-sha2-256-etm@openssh.com 22 | - umac-128-etm@openssh.com 23 | - hmac-sha2-512 24 | - hmac-sha2-256 25 | 26 | ssh_ciphers_53_default: 27 | - aes256-ctr 28 | - aes192-ctr 29 | - aes128-ctr 30 | 31 | ssh_ciphers_66_default: 32 | - chacha20-poly1305@openssh.com 33 | - aes256-gcm@openssh.com 34 | - aes128-gcm@openssh.com 35 | - aes256-ctr 36 | - aes192-ctr 37 | - aes128-ctr 38 | 39 | ssh_kex_59_default: 40 | - diffie-hellman-group-exchange-sha256 41 | 42 | ssh_kex_66_default: 43 | - curve25519-sha256@libssh.org 44 | - diffie-hellman-group-exchange-sha256 45 | 46 | ssh_kex_80_default: 47 | - sntrup4591761x25519-sha512@tinyssh.org 48 | - curve25519-sha256@libssh.org 49 | - diffie-hellman-group-exchange-sha256 50 | 51 | ssh_kex_85_default: 52 | - sntrup761x25519-sha512@openssh.com 53 | - curve25519-sha256@libssh.org 54 | - diffie-hellman-group-exchange-sha256 55 | --------------------------------------------------------------------------------