Restructure into one folder per private repo/host, not a flat Docker/ dump
rocky-docker-lxc/, caddy-vm/, monitoring-vm/ (was missing entirely -
Grafana+Loki+Prometheus stack, refreshed and CA mount stripped same as
everything else), and automation/{ansible,automation-vm,iac}/ for the
three repos that live on the automation VM. Hawser is genuinely
deployed on all three of caddy-vm/monitoring-vm/automation-vm so it's
duplicated in each rather than picking one canonical home.
Old Ansible/ and "Infrastrucutre as Code/" content moved under
automation/ansible/old-playbooks and automation/iac/old-content as-is
for now - contents still need refreshing against the current repos and
a per-folder README, that's the next pass.
This commit is contained in:
146 files changed
+101
-84
No files matched your search
@@ -0,0 +1 @@
|
||||
asupersecretpasswordfor ansiblevaultunlock
|
||||
@@ -0,0 +1,3 @@
|
||||
Coming Soon...
|
||||
|
||||
There will soon be a full write-up of the way that the Ansible Roles and Tags laid out during the VM creation via Terraform, is used to configure a VM based on very specific use cases and criteria. The way I have designed the entire workflow is condusive to having a secure Packer built ISO expanded in its capabilites simply by the addition of a new Ansible Role and Tag.
|
||||
@@ -0,0 +1,8 @@
|
||||
[defaults]
|
||||
inventory = ./inventory.ini
|
||||
roles_path = ./roles:./roles/bootstrap_roles:./roles/cis_hardening:./roles/kestra:./roles/monitoring:./roles/secure_vault:./roles/wazuh_docker
|
||||
ansible_python_interpreter = /path/to/ansible-modern/bin/python3
|
||||
collections_paths = /path/to/ansible-modern/lib64/python3.11/site-packages/ansible_collections:/path/to/ansible_collections:/usr/share/ansible/collections
|
||||
remote_user = ansible
|
||||
private_key_file = /path/to/.ssh/key_file
|
||||
host_key_checking = False
|
||||
@@ -0,0 +1,2 @@
|
||||
---
|
||||
roles_metadata: "{{ lookup('file', 'roles_metadata.yml') | from_yaml }}"
|
||||
@@ -0,0 +1,14 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
64373439343462336430396135326665346633343564656532636363356339326561643161353635
|
||||
3636363336663638373932643830646634366336366363370a616235646239386166343964636336
|
||||
66303364663462396235626566613266313561376466646631346464653564623630333137383536
|
||||
3630363935333333390a633633333264323834323863623035626662326639663731323938633534
|
||||
65303062303837636430346435316538336434386464373836336538633933633465376162333138
|
||||
63626433643734633634363433666139653736306630393031653566666362343833363936383265
|
||||
38656136636434653135633561323839656337623963393731663762333836343665323239356536
|
||||
35663433316535313063323264316338353761323537393035373338376634656265363361623037
|
||||
32323935333262373437373662646538613665626265626534333862616138373361666639376437
|
||||
36613238336332313734316235363265636164363566326636646534646561363666333637323737
|
||||
63313336623130306636383362646138656130383661633738623365663762393761306164333536
|
||||
39356564636164306332333763663461343462653562343330623862363131653863333363333332
|
||||
3162
|
||||
@@ -0,0 +1,31 @@
|
||||
# VM metadata from Terraform provisioning
|
||||
vmid: id number here
|
||||
vm_name: "vm name"
|
||||
target_node: "hypervisor node name"
|
||||
template: "template-name"
|
||||
cores: 4
|
||||
sockets: 1
|
||||
memory: 57344
|
||||
bridge: vmbr0
|
||||
macaddr: "xx:x:x:x:x:x"
|
||||
disk_size: 250GB
|
||||
disk_storage: "storage name here"
|
||||
ip: xx.xx.xx.xx
|
||||
vlan: 0
|
||||
vm_tags:
|
||||
- packages
|
||||
- hostname_network
|
||||
- root_password
|
||||
- disk_resize
|
||||
- base_users
|
||||
- firewall_ssh
|
||||
- endlessh
|
||||
- zsh_custom
|
||||
- ssh_hardening
|
||||
- banners
|
||||
- file_permissions
|
||||
- service_disable
|
||||
- kernel_hardening
|
||||
- docker_gitlab_setup
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# VM metadata from Terraform provisioning
|
||||
vmid: id number here
|
||||
vm_name: "vm name"
|
||||
target_node: "hypervisor node name"
|
||||
template: "template-name"
|
||||
cores: 1
|
||||
sockets: 1
|
||||
memory: 4096
|
||||
bridge: vmbr0
|
||||
macaddr: "xx:x:x:x:x:x"
|
||||
disk_size: 20GB
|
||||
disk_storage: "storage name here"
|
||||
ip: xx.xx.xx.xx
|
||||
vlan: 0
|
||||
vm_tags:
|
||||
- packages
|
||||
- hostname_network
|
||||
- root_password
|
||||
- disk_resize
|
||||
- base_users
|
||||
- firewall_ssh
|
||||
- endlessh
|
||||
- zsh_custom
|
||||
- vault_agent
|
||||
- haproxy
|
||||
- ssh_hardening
|
||||
- banners
|
||||
- file_permissions
|
||||
- service_disable
|
||||
- kernel_hardening
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
all:
|
||||
hosts:
|
||||
haproxy:
|
||||
ansible_host: xx.xx.xx.xx
|
||||
ansible_port: xxxx
|
||||
initialized: true
|
||||
service: haproxy
|
||||
docker:
|
||||
ansible_host: xx.xx.xx.xx
|
||||
ansible_port: xxxx
|
||||
initialized: true
|
||||
service: docker
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
@@ -0,0 +1,8 @@
|
||||
all:
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
hosts:
|
||||
haproxy:
|
||||
ansible_host: xx.xx.xx.xx
|
||||
ansible_port: 22
|
||||
service: haproxy
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Compliance stage
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
stage_name: compliance
|
||||
|
||||
vars_files:
|
||||
- /path/to/Infrastrucutre as Code/ansible/roles_metadata.yml
|
||||
|
||||
tasks:
|
||||
- name: Run roles for this stage
|
||||
ansible.builtin.include_role:
|
||||
name: "{{ item }}"
|
||||
loop: >-
|
||||
{{
|
||||
vm_tags
|
||||
| select('in',
|
||||
roles_metadata
|
||||
| dict2items
|
||||
| selectattr('value.' ~ stage_name, 'defined')
|
||||
| selectattr('value.' ~ stage_name)
|
||||
| map(attribute='key')
|
||||
| list
|
||||
)
|
||||
| list
|
||||
}}
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
- name: Initialization stage
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
stage_name: initialization
|
||||
inventory_init: "/path/to/Infrastrucutre as Code/ansible/inventory_initialization.yml"
|
||||
inventory_active: "/path/to/Infrastrucutre as Code/ansible/inventory.yml"
|
||||
|
||||
vars_files:
|
||||
- /path/to/Infrastrucutre as Code/ansible/roles_metadata.yml
|
||||
- /path/to/Infrastrucutre as Code/ansible/group_vars/all/vault_token.yml
|
||||
|
||||
pre_tasks:
|
||||
- name: Ensure active inventory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ inventory_active }}"
|
||||
state: touch
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Load existing active inventory
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ inventory_active }}"
|
||||
register: active_inv_raw
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: Parse active inventory YAML (or initialize empty)
|
||||
ansible.builtin.set_fact:
|
||||
active_inventory: >-
|
||||
{{
|
||||
(active_inv_raw.content | b64decode | from_yaml)
|
||||
if (active_inv_raw.content | b64decode | from_yaml is mapping)
|
||||
else {'all': {'hosts': {}}}
|
||||
}}
|
||||
run_once: true
|
||||
|
||||
tasks:
|
||||
- name: Run initialization roles for uninitialized hosts
|
||||
ansible.builtin.include_role:
|
||||
name: "{{ item }}"
|
||||
loop: >-
|
||||
{{
|
||||
vm_tags
|
||||
| select('in',
|
||||
roles_metadata
|
||||
| dict2items
|
||||
| selectattr('value.' ~ stage_name, 'defined')
|
||||
| selectattr('value.' ~ stage_name)
|
||||
| map(attribute='key')
|
||||
| list
|
||||
)
|
||||
| list
|
||||
}}
|
||||
|
||||
post_tasks:
|
||||
- name: Append or update host in active inventory
|
||||
ansible.builtin.set_fact:
|
||||
active_inventory: >-
|
||||
{{
|
||||
active_inventory |
|
||||
combine({
|
||||
'all': {
|
||||
'hosts': (
|
||||
active_inventory.all.hosts |
|
||||
combine({
|
||||
inventory_hostname: {
|
||||
'ansible_host': hostvars[inventory_hostname]['ansible_host'],
|
||||
'ansible_port': hostvars[inventory_hostname]['ssh_port'] | default(22),
|
||||
'initialized': true
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}, recursive=True)
|
||||
}}
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Write updated active inventory back to file
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ inventory_active }}"
|
||||
content: "{{ active_inventory | to_nice_yaml }}"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Provisioning stage
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
stage_name: provisioning
|
||||
|
||||
vars_files:
|
||||
- /path/to/Infrastrucutre as Code/ansible/roles_metadata.yml
|
||||
|
||||
tasks:
|
||||
- name: Run roles for this stage
|
||||
ansible.builtin.include_role:
|
||||
name: "{{ item }}"
|
||||
loop: >-
|
||||
{{
|
||||
vm_tags
|
||||
| select('in',
|
||||
roles_metadata
|
||||
| dict2items
|
||||
| selectattr('value.' ~ stage_name, 'defined')
|
||||
| selectattr('value.' ~ stage_name)
|
||||
| map(attribute='key')
|
||||
| list
|
||||
)
|
||||
| list
|
||||
}}
|
||||
@@ -0,0 +1,9 @@
|
||||
collections:
|
||||
- name: community.hashi_vault
|
||||
- name: community.general
|
||||
- name: ansible.posix
|
||||
- name: community.crypto
|
||||
- name: community.dns
|
||||
- name: middleware_automation.keycloak
|
||||
- name: community.network
|
||||
- name: community.docker
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
- name: Configure default home directory and shell for system users
|
||||
block:
|
||||
- name: Set default home directory and shell for system users
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/default/useradd
|
||||
regexp: "^{{ setting.key }}="
|
||||
line: "{{ setting.key }}={{ setting.value }}"
|
||||
create: yes
|
||||
backup: yes
|
||||
loop:
|
||||
- { key: "HOME", value: "/nonexistent" }
|
||||
- { key: "SHELL", value: "/sbin/nologin" }
|
||||
loop_control:
|
||||
loop_var: setting
|
||||
@@ -0,0 +1,28 @@
|
||||
- name: Expand disk and root filesystem
|
||||
block:
|
||||
- name: Attempt to grow partition (safe if already full)
|
||||
command: growpart /dev/sda 2
|
||||
register: growpart_result
|
||||
ignore_errors: true
|
||||
|
||||
- name: Determine if partition actually grew
|
||||
set_fact:
|
||||
partition_grew: >-
|
||||
{{ growpart_result.rc == 0 and
|
||||
(growpart_result.stdout is defined and 'NOCHANGE' not in growpart_result.stdout) }}
|
||||
|
||||
- name: Resize LVM physical volume if needed
|
||||
command: pvresize /dev/sda2
|
||||
when: partition_grew
|
||||
register: pvresize_result
|
||||
changed_when: "'Physical volume' in pvresize_result.stdout and 'resized' in pvresize_result.stdout"
|
||||
|
||||
- name: Extend logical volume if needed
|
||||
command: lvextend -l +100%FREE /dev/mapper/rocky-lv_root
|
||||
when: partition_grew
|
||||
register: lvextend_result
|
||||
changed_when: lvextend_result.rc == 0 and "'0 extents found" not in lvextend_result.stderr
|
||||
|
||||
- name: Grow the XFS filesystem if needed
|
||||
command: xfs_growfs /
|
||||
when: partition_grew and (lvextend_result is changed or lvextend_result is not defined)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
docker_old_root: "/var/lib/docker"
|
||||
docker_new_root_base: "/opt/docker/docker_data"
|
||||
docker_new_root: "{{ docker_new_root_base }}/docker"
|
||||
|
||||
docker_volumes_dir: "/opt/docker/docker_volumes"
|
||||
docker_homelab_dir: "/opt/docker/homelab"
|
||||
|
||||
docker_volumes_owner: "user"
|
||||
docker_volumes_group: "group"
|
||||
|
||||
git_repo_url: "[email protected]:USER/REPO.git"
|
||||
git_repo_version: "main"
|
||||
|
||||
ssh_key_path: "/home/USER/.ssh/id_rsa"
|
||||
git_repo_owner: "user"
|
||||
git_repo_group: "group"
|
||||
|
||||
docker_daemon_conf: "/etc/docker/daemon.json"
|
||||
daemon_json_template: |
|
||||
{
|
||||
"data-root": "{{ docker_new_root }}"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: restart docker
|
||||
service:
|
||||
name: docker
|
||||
state: restarted
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
---
|
||||
# 1. Ensure SSH key exists so git clone works
|
||||
- name: Ensure .ssh directory exists
|
||||
file:
|
||||
path: "{{ ssh_key_path | dirname }}"
|
||||
state: directory
|
||||
owner: "user"
|
||||
group: "user"
|
||||
mode: "0700"
|
||||
|
||||
- name: Ensure SSH key exists (don't overwrite)
|
||||
copy:
|
||||
src: "{{ ssh_key_path }}"
|
||||
dest: "{{ ssh_key_path }}"
|
||||
owner: "user"
|
||||
group: "user"
|
||||
mode: "0600"
|
||||
|
||||
# 2. Ensure docker_data directory exists
|
||||
- name: Ensure new Docker data root base exists
|
||||
file:
|
||||
path: "{{ docker_new_root_base }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
# 3. Stop Docker ONLY if /var/lib/docker still exists
|
||||
- name: Stop Docker if old root exists
|
||||
service:
|
||||
name: docker
|
||||
state: stopped
|
||||
when: docker_old_root is exists
|
||||
|
||||
# 4. Move /var/lib/docker → new location (idempotent)
|
||||
- name: Move Docker data root to new location
|
||||
command: "mv {{ docker_old_root }} {{ docker_new_root }}"
|
||||
args:
|
||||
creates: "{{ docker_new_root }}"
|
||||
when: docker_old_root is exists
|
||||
|
||||
# 5. Ensure daemon.json points to new data-root
|
||||
- name: Configure Docker daemon.json
|
||||
copy:
|
||||
dest: "{{ docker_daemon_conf }}"
|
||||
content: "{{ daemon_json_template }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: restart docker
|
||||
|
||||
# 6. Ensure docker_volumes exists
|
||||
- name: Ensure docker_volumes directory exists
|
||||
file:
|
||||
path: "{{ docker_volumes_dir }}"
|
||||
state: directory
|
||||
owner: "{{ docker_volumes_owner }}"
|
||||
group: "{{ docker_volumes_group }}"
|
||||
mode: "0755"
|
||||
|
||||
# 7. Ensure homelab directory exists
|
||||
- name: Ensure homelab directory exists
|
||||
file:
|
||||
path: "{{ docker_homelab_dir }}"
|
||||
state: directory
|
||||
owner: "{{ git_repo_owner }}"
|
||||
group: "{{ git_repo_group }}"
|
||||
mode: "0755"
|
||||
|
||||
# 8. Start Docker if it was stopped
|
||||
- name: Start Docker
|
||||
service:
|
||||
name: docker
|
||||
state: started
|
||||
when: docker_new_root is exists
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
endlessh_port: 22
|
||||
endlessh_repo: https://github.com/skeeto/endlessh.git
|
||||
endlessh_clone_path: /usr/local/src/endlessh
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
- name: Ensure endlessh group exists
|
||||
ansible.builtin.group:
|
||||
name: endlessh
|
||||
system: true
|
||||
|
||||
- name: Ensure endlessh user exists
|
||||
ansible.builtin.user:
|
||||
name: endlessh
|
||||
group: endlessh
|
||||
system: true
|
||||
shell: /usr/sbin/nologin
|
||||
create_home: false
|
||||
|
||||
- name: Ensure Endlessh repo is cloned and updated
|
||||
ansible.builtin.git:
|
||||
repo: "{{ endlessh_repo }}"
|
||||
dest: "{{ endlessh_clone_path }}"
|
||||
version: master
|
||||
update: yes
|
||||
register: git_result
|
||||
|
||||
- name: Build Endlessh if repo was updated
|
||||
ansible.builtin.command: make
|
||||
args:
|
||||
chdir: "{{ endlessh_clone_path }}"
|
||||
when: git_result.after != git_result.before
|
||||
register: make_result
|
||||
# no changed_when needed; we'll rely on the copy step to track changes
|
||||
|
||||
- name: Copy Endlessh binary to /usr/local/bin if updated
|
||||
ansible.builtin.copy:
|
||||
src: "{{ endlessh_clone_path }}/endlessh"
|
||||
dest: /usr/local/bin/endlessh
|
||||
mode: "0755"
|
||||
remote_src: true
|
||||
when: git_result.after != git_result.before
|
||||
register: copy_result
|
||||
|
||||
- name: Create Endlessh config directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/endlessh
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Deploy Endlessh config
|
||||
ansible.builtin.template:
|
||||
src: endlessh_config.j2
|
||||
dest: /etc/endlessh/config
|
||||
mode: "0644"
|
||||
|
||||
- name: Deploy Endlessh systemd service
|
||||
ansible.builtin.template:
|
||||
src: endlessh.service.j2
|
||||
dest: /etc/systemd/system/endlessh.service
|
||||
mode: "0644"
|
||||
|
||||
- name: Reload systemd daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart Endlessh if binary updated
|
||||
ansible.builtin.systemd:
|
||||
name: endlessh
|
||||
enabled: true
|
||||
state: restarted
|
||||
when: copy_result is changed
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
[Unit]
|
||||
Description=Endlessh SSH Tarpit
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/endlessh -f /etc/endlessh/config
|
||||
Restart=always
|
||||
User=endlessh
|
||||
Group=endlessh
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
ReadWritePaths=/etc/endlessh
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Port {{ endlessh_port }}
|
||||
Delay 10000
|
||||
MaxLineLength 32
|
||||
MaxClients 4096
|
||||
LogLevel 1
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
# Customize if needed; this defaults to port 22 for endlessh
|
||||
endlessh_port: 22
|
||||
# The SSH port will be dynamically computed as: 10000 + (vmid % 100)
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
- name: Configure firewall and SSH port
|
||||
block:
|
||||
- name: Fail if vmid is not defined
|
||||
ansible.builtin.fail:
|
||||
msg: "'vmid' must be defined to calculate dynamic SSH port"
|
||||
when: vmid is not defined
|
||||
|
||||
- name: Calculate SSH port from vmid
|
||||
ansible.builtin.set_fact:
|
||||
ssh_port: "{{ 10000 + (vmid | int % 100) }}"
|
||||
|
||||
- name: Enable and start firewalld
|
||||
ansible.builtin.service:
|
||||
name: firewalld
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Set firewalld default zone to drop
|
||||
ansible.posix.firewalld:
|
||||
zone: public
|
||||
state: enabled
|
||||
permanent: yes
|
||||
target: DROP
|
||||
|
||||
- name: Reload firewalld to apply permanent changes
|
||||
ansible.builtin.command: firewall-cmd --reload
|
||||
|
||||
- name: Allow custom SSH port in firewalld
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ ssh_port }}/tcp"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
|
||||
- name: Allow Endlessh port in firewalld
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ endlessh_port }}/tcp"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
|
||||
- name: Ensure custom SSH port is set in sshd_config
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^#?Port\s+'
|
||||
line: "Port {{ ssh_port }}"
|
||||
backup: yes
|
||||
validate: "/usr/sbin/sshd -T -f %s"
|
||||
register: sshd_config_changed
|
||||
|
||||
- name: Gather SELinux facts
|
||||
ansible.builtin.setup:
|
||||
filter: ansible_selinux
|
||||
|
||||
- name: Allow SSH to use custom port in SELinux
|
||||
ansible.builtin.command: >
|
||||
semanage port -a -t ssh_port_t -p tcp {{ ssh_port }}
|
||||
register: semanage_result
|
||||
failed_when: semanage_result.rc not in [0, 1]
|
||||
changed_when: semanage_result.rc == 0
|
||||
when: ansible_selinux.status == "enabled"
|
||||
|
||||
- name: Restart and enable sshd service if needed
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
enabled: true
|
||||
when: sshd_config_changed is changed
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Set hostname and update /etc/hosts
|
||||
vars:
|
||||
default_domain: subdomain.domain
|
||||
block:
|
||||
- name: Set hostname fact
|
||||
ansible.builtin.set_fact:
|
||||
short_hostname: "{{ vm_name }}"
|
||||
custom_hostname: "{{ vm_name }}.{{ default_domain }}"
|
||||
|
||||
- name: Set system hostname
|
||||
ansible.builtin.hostname:
|
||||
name: "{{ custom_hostname }}"
|
||||
|
||||
- name: Ensure /etc/hosts contains loopback entry for hostname
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
regexp: '^127\.0\.1\.1\s+'
|
||||
line: "127.0.1.1 {{ custom_hostname }} {{ short_hostname }}"
|
||||
state: present
|
||||
create: yes
|
||||
|
||||
- name: Ensure /etc/hosts contains primary network IP entry
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
regexp: '^{{ ip }}\s+'
|
||||
line: "{{ ip }} {{ custom_hostname }} {{ short_hostname }}"
|
||||
state: present
|
||||
create: yes
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
base_packages:
|
||||
- firewalld
|
||||
- policycoreutils-python-utils
|
||||
- git
|
||||
- make
|
||||
- gcc
|
||||
- cloud-utils-growpart
|
||||
- lvm2
|
||||
- xfsprogs
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: Package management tasks
|
||||
tags: [packages]
|
||||
block:
|
||||
- name: Clean all cached metadata
|
||||
ansible.builtin.command: dnf clean all
|
||||
become: true
|
||||
|
||||
- name: Rebuild the DNF cache
|
||||
ansible.builtin.command: dnf makecache
|
||||
become: true
|
||||
|
||||
- name: Update all packages to latest
|
||||
ansible.builtin.dnf:
|
||||
name: "*"
|
||||
state: latest
|
||||
update_cache: true
|
||||
become: true
|
||||
|
||||
- name: Reboot if kernel was upgraded
|
||||
ansible.builtin.reboot:
|
||||
reboot_timeout: 120
|
||||
pre_reboot_delay: 10
|
||||
when: ansible_facts.packages['kernel'] is defined
|
||||
become: true
|
||||
|
||||
- name: Install base system packages
|
||||
ansible.builtin.package:
|
||||
name: "{{ base_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
become: true
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
---
|
||||
- name: Set Portainer Agent installation directory
|
||||
vars:
|
||||
portainer_agent_dir: /opt/portainer-agent
|
||||
block:
|
||||
- name: Create Portainer Agent directory
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: "{{ portainer_agent_dir }}"
|
||||
state: directory
|
||||
owner: user
|
||||
group: user
|
||||
mode: "0755"
|
||||
|
||||
- name: Create Portainer Agent docker-compose file
|
||||
become: true
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ portainer_agent_dir }}/docker-compose.yml"
|
||||
owner: user
|
||||
group: user
|
||||
mode: "0644"
|
||||
content: |
|
||||
services:
|
||||
agent:
|
||||
image: portainer/agent:latest
|
||||
container_name: portainer_agent
|
||||
restart: always
|
||||
ports:
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/lib/docker/volumes:/var/lib/docker/volumes
|
||||
|
||||
- name: Ensure Portainer Agent is running
|
||||
become: true
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ portainer_agent_dir }}"
|
||||
state: present # ensures containers exist
|
||||
recreate: always # recreates if the definition changed
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
---
|
||||
root_password_plain: "{{ lookup('env', 'ROOT_PASSWORD') }}"
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
- name: Set root password
|
||||
block:
|
||||
- name: Fail if root password is not defined
|
||||
fail:
|
||||
msg: "The variable 'root_password_plain' must be provided (e.g., via GitLab CI/CD Variable)"
|
||||
when: root_password_plain is not defined
|
||||
|
||||
- name: Generate root password hash
|
||||
set_fact:
|
||||
root_password_hash: "{{ root_password_plain | password_hash('sha512') }}"
|
||||
|
||||
- name: Set root user password
|
||||
user:
|
||||
name: root
|
||||
password: "{{ root_password_hash }}"
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
- name: Customize Zsh for users
|
||||
vars:
|
||||
zsh_users:
|
||||
- user
|
||||
|
||||
block:
|
||||
- name: Ensure parent home directory exists (safety)
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ user }}"
|
||||
state: directory
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0755"
|
||||
loop: "{{ zsh_users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
become: true
|
||||
|
||||
- name: Remove any old oh-my-zsh
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ user }}/.oh-my-zsh"
|
||||
state: absent
|
||||
loop: "{{ zsh_users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
become: true
|
||||
|
||||
- name: Clone Oh My Zsh as root
|
||||
ansible.builtin.git:
|
||||
repo: "https://github.com/ohmyzsh/ohmyzsh.git"
|
||||
dest: "/home/{{ user }}/.oh-my-zsh"
|
||||
version: master
|
||||
update: yes
|
||||
force: yes
|
||||
loop: "{{ zsh_users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
become: true
|
||||
|
||||
- name: Add trusted safe.directory system-wide for Oh My Zsh
|
||||
ansible.builtin.command:
|
||||
cmd: git config --system --add safe.directory /home/{{ user }}/.oh-my-zsh
|
||||
loop: "{{ zsh_users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
become: true
|
||||
|
||||
- name: Install syntax-highlighting plugin
|
||||
ansible.builtin.git:
|
||||
repo: "https://github.com/zsh-users/zsh-syntax-highlighting.git"
|
||||
dest: "/home/{{ user }}/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting"
|
||||
version: master
|
||||
update: yes
|
||||
force: yes
|
||||
loop: "{{ zsh_users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
become: true
|
||||
|
||||
- name: Deploy customized .zshrc
|
||||
ansible.builtin.template:
|
||||
src: zshrc.j2
|
||||
dest: "/home/{{ user }}/.zshrc"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
mode: "0644"
|
||||
loop: "{{ zsh_users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
become: true
|
||||
|
||||
- name: Fix ownership recursively for Oh My Zsh directory
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ user }}/.oh-my-zsh"
|
||||
owner: "{{ user }}"
|
||||
group: "{{ user }}"
|
||||
recurse: yes
|
||||
loop: "{{ zsh_users }}"
|
||||
loop_control:
|
||||
loop_var: user
|
||||
become: true
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
export ZSH="\$HOME/.oh-my-zsh"
|
||||
ZSH_THEME="robbyrussell"
|
||||
plugins=(git zsh-autosuggestions zsh-syntax-highlighting)
|
||||
source \$ZSH/oh-my-zsh.sh
|
||||
|
||||
# Where to save history and how much to keep
|
||||
HISTFILE=~/.zsh_history
|
||||
HISTSIZE=10000 # In-memory history
|
||||
SAVEHIST=10000 # Lines to save to file
|
||||
|
||||
# Recommended options
|
||||
setopt APPEND_HISTORY # Don’t overwrite history file; append to it
|
||||
setopt SHARE_HISTORY # Share history across all sessions
|
||||
setopt HIST_IGNORE_DUPS # Don’t record duplicate commands
|
||||
setopt HIST_IGNORE_SPACE # Ignore commands starting with a space
|
||||
setopt HIST_VERIFY # Edit a command before running from history
|
||||
setopt INC_APPEND_HISTORY # Write history incrementally as commands are run
|
||||
|
||||
|
||||
PS1='%F{51}%n%F{255}@%F{227}%m %F{84}%~%f %F{15}$ '
|
||||
fastfetch
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
cis_banner: |
|
||||
*********************************************************************
|
||||
* WARNING: Unauthorized access to this system is prohibited. *
|
||||
* All activities are monitored and recorded. *
|
||||
*********************************************************************
|
||||
|
||||
cis_motd_banner: |
|
||||
*********************************************************************
|
||||
* Welcome to the system. Access is restricted to authorized users. *
|
||||
*********************************************************************
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
# Login banners for /etc/motd, /etc/issue, /etc/issue.net
|
||||
|
||||
- name: Ensure /etc/motd contains standard CIS banner
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/motd
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: "{{ cis_motd_banner }}"
|
||||
|
||||
- name: Ensure /etc/issue contains standard CIS banner
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/issue
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: "{{ cis_motd_banner }}"
|
||||
|
||||
- name: Ensure /etc/issue.net contains standard CIS banner
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/issue.net
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: "{{ cis_banner }}"
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# Parent-level defaults
|
||||
cis_tag: cis_hardening
|
||||
audit_rules_file: /etc/audit/rules.d/cis.rules
|
||||
rsyslog_remote: "" # set to "logserver.example.com:514" to forward logs
|
||||
txt_grub_args: "" # additional kernel args if needed, e.g. "audit=1"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
---
|
||||
# Default ownership and permissions for critical files and directories
|
||||
|
||||
file_permissions_list:
|
||||
- { path: "/root", mode: "0700", state: "directory" }
|
||||
- { path: "/etc/passwd", mode: "0644", state: "file" }
|
||||
- { path: "/etc/shadow", mode: "0400", state: "file" }
|
||||
- { path: "/etc/group", mode: "0644", state: "file" }
|
||||
- { path: "/etc/gshadow", mode: "0400", state: "file" }
|
||||
- { path: "/etc/crontab", mode: "0600", state: "file" }
|
||||
- { path: "/etc/cron.hourly", mode: "0700", state: "directory" }
|
||||
- { path: "/etc/cron.daily", mode: "0700", state: "directory" }
|
||||
- { path: "/etc/cron.weekly", mode: "0700", state: "directory" }
|
||||
- { path: "/etc/cron.monthly", mode: "0700", state: "directory" }
|
||||
- { path: "/etc/cron.d", mode: "0700", state: "directory" }
|
||||
- { path: "/etc/rsyslog.conf", mode: "0640", state: "file" }
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Ensure system files and directories have correct ownership and permissions
|
||||
ansible.builtin.file:
|
||||
path: "{{ file_item.path }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "{{ file_item.mode }}"
|
||||
state: "{{ file_item.state }}"
|
||||
loop: "{{ file_permissions_list }}"
|
||||
loop_control:
|
||||
loop_var: file_item
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
tmp_mount_options: "defaults,nosuid,nodev,noexec,relatime"
|
||||
var_tmp_mount_options: "defaults,nosuid,nodev,noexec,relatime"
|
||||
home_mount_options: "defaults,nosuid,nodev,relatime"
|
||||
dev_shm_mount_options: "defaults,nosuid,nodev,noexec,relatime"
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
# Remount and persist secure options using defaults variables
|
||||
|
||||
- name: Gather mount facts
|
||||
ansible.builtin.setup:
|
||||
filter: ansible_mounts
|
||||
|
||||
- name: Ensure /tmp has secure mount options
|
||||
ansible.builtin.mount:
|
||||
path: /tmp
|
||||
fstype: "{{ ansible_mounts | selectattr('mount','equalto','/tmp') | map(attribute='fstype') | first }}"
|
||||
opts: "{{ tmp_mount_options }}"
|
||||
state: mounted
|
||||
|
||||
- name: Ensure /var/tmp has secure mount options
|
||||
ansible.builtin.mount:
|
||||
path: /var/tmp
|
||||
fstype: "{{ ansible_mounts | selectattr('mount','equalto','/var/tmp') | map(attribute='fstype') | first }}"
|
||||
opts: "{{ var_tmp_mount_options }}"
|
||||
state: mounted
|
||||
|
||||
- name: Ensure /home has secure mount options
|
||||
ansible.builtin.mount:
|
||||
path: /home
|
||||
fstype: "{{ ansible_mounts | selectattr('mount','equalto','/home') | map(attribute='fstype') | first }}"
|
||||
opts: "{{ home_mount_options }}"
|
||||
state: mounted
|
||||
|
||||
- name: Ensure /dev/shm has secure mount options
|
||||
ansible.builtin.mount:
|
||||
path: /dev/shm
|
||||
fstype: tmpfs
|
||||
opts: "{{ dev_shm_mount_options }}"
|
||||
state: mounted
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
- name: restart rsyslog
|
||||
ansible.builtin.service:
|
||||
name: rsyslog
|
||||
state: restarted
|
||||
|
||||
- name: restart auditd
|
||||
ansible.builtin.service:
|
||||
name: auditd
|
||||
state: restarted
|
||||
|
||||
- name: reload systemd
|
||||
ansible.builtin.command: systemctl daemon-reexec
|
||||
|
||||
- name: update initramfs
|
||||
ansible.builtin.command: dracut -f
|
||||
when: ansible_facts['os_family'] == 'RedHat' or ansible_facts['distribution'] in ['Rocky','RedHat','CentOS']
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
# Kernel defaults (override per host if needed)
|
||||
modprobe_blacklist_file: /etc/modprobe.d/cis_blacklist.conf
|
||||
|
||||
# Additional kernel modules to disable
|
||||
blacklist_modules:
|
||||
- tipc
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# blacklist_module.yml
|
||||
- name: Check if module {{ mod }} exists on system
|
||||
ansible.builtin.shell: "modprobe -n -v {{ mod }}"
|
||||
register: modprobe_check
|
||||
ignore_errors: true
|
||||
|
||||
- name: Blacklist module {{ mod }} in {{ modprobe_blacklist_file }}
|
||||
ansible.builtin.lineinfile:
|
||||
path: "{{ modprobe_blacklist_file }}"
|
||||
line: "install {{ mod }} /bin/false"
|
||||
create: yes
|
||||
state: present
|
||||
when: modprobe_check.rc == 0
|
||||
|
||||
- name: Unload module {{ mod }} if currently loaded
|
||||
ansible.builtin.shell: "modprobe -r {{ mod }}"
|
||||
ignore_errors: true
|
||||
when: modprobe_check.rc == 0
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
# Kernel & sysctl hardening (CIS network/kernel items)
|
||||
|
||||
- name: Ensure IPv4 forwarding is disabled
|
||||
ansible.builtin.sysctl:
|
||||
name: net.ipv4.ip_forward
|
||||
value: "0"
|
||||
state: present
|
||||
reload: yes
|
||||
|
||||
- name: Ensure IPv6 forwarding is disabled
|
||||
ansible.builtin.sysctl:
|
||||
name: net.ipv6.conf.all.forwarding
|
||||
value: "0"
|
||||
state: present
|
||||
reload: yes
|
||||
|
||||
- name: Ensure source routed packets are not accepted
|
||||
ansible.builtin.sysctl:
|
||||
name: net.ipv4.conf.all.accept_source_route
|
||||
value: "0"
|
||||
state: present
|
||||
reload: yes
|
||||
|
||||
- name: Ensure ICMP redirects are not accepted
|
||||
ansible.builtin.sysctl:
|
||||
name: net.ipv4.conf.all.accept_redirects
|
||||
value: "0"
|
||||
state: present
|
||||
reload: yes
|
||||
|
||||
- name: Ensure secure ICMP redirects are not accepted
|
||||
ansible.builtin.sysctl:
|
||||
name: net.ipv4.conf.all.secure_redirects
|
||||
value: "0"
|
||||
state: present
|
||||
reload: yes
|
||||
|
||||
- name: Ensure Address Space Layout Randomization (ASLR) is enabled
|
||||
ansible.builtin.sysctl:
|
||||
name: kernel.randomize_va_space
|
||||
value: "2"
|
||||
state: present
|
||||
reload: yes
|
||||
|
||||
- name: Ensure fs.suid_dumpable is 0 (disable core dumps)
|
||||
ansible.builtin.sysctl:
|
||||
name: fs.suid_dumpable
|
||||
value: "0"
|
||||
state: present
|
||||
reload: yes
|
||||
|
||||
- name: Create /etc/modprobe.d/cis_blacklist.conf with common blacklists
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/modprobe.d/cis_blacklist.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
content: |
|
||||
# CIS blacklist
|
||||
install cramfs /bin/true
|
||||
install freevxfs /bin/true
|
||||
install jffs2 /bin/true
|
||||
install hfs /bin/true
|
||||
install hfsplus /bin/true
|
||||
install squashfs /bin/true
|
||||
install udf /bin/true
|
||||
|
||||
- name: Regenerate initramfs if modprobe blacklist changed
|
||||
ansible.builtin.command: dracut -f
|
||||
changed_when: false
|
||||
when: ansible_facts.get('os_family') == 'RedHat' or ansible_facts.get('distribution') in ['Rocky', 'RedHat', 'CentOS']
|
||||
|
||||
- name: Blacklist all modules
|
||||
include_tasks: blacklist_module.yml
|
||||
loop: "{{ blacklist_modules }}"
|
||||
loop_control:
|
||||
loop_var: mod
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
# Password policy defaults
|
||||
pw_minlen: 14
|
||||
pw_minclass: 4
|
||||
pw_max_days: 365
|
||||
pw_min_days: 7
|
||||
pw_warn_age: 7
|
||||
faillock_deny: 5
|
||||
faillock_unlock_time: 900
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
# Password and PAM related hardening
|
||||
|
||||
- name: Ensure password hashing algorithm is SHA512
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: "^ENCRYPT_METHOD"
|
||||
line: "ENCRYPT_METHOD SHA512"
|
||||
create: yes
|
||||
|
||||
- name: Ensure PASS_MAX_DAYS is 365
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: "^PASS_MAX_DAYS"
|
||||
line: "PASS_MAX_DAYS 365"
|
||||
create: yes
|
||||
|
||||
- name: Ensure PASS_MIN_DAYS is 7
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: "^PASS_MIN_DAYS"
|
||||
line: "PASS_MIN_DAYS 7"
|
||||
create: yes
|
||||
|
||||
- name: Ensure PASS_WARN_AGE is 7
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: "^PASS_WARN_AGE"
|
||||
line: "PASS_WARN_AGE 7"
|
||||
create: yes
|
||||
|
||||
- name: Ensure pwquality minlen and requirements
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/security/pwquality.conf
|
||||
regexp: "^minlen"
|
||||
line: "minlen = 14"
|
||||
create: yes
|
||||
|
||||
- name: Ensure pwquality minclass (character classes) is 4
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/security/pwquality.conf
|
||||
regexp: "^minclass"
|
||||
line: "minclass = 4"
|
||||
create: yes
|
||||
|
||||
- name: Ensure pam_faillock preauth is configured
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/system-auth
|
||||
insertafter: "^auth.*required.*pam_unix.so"
|
||||
line: "auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900"
|
||||
state: present
|
||||
backrefs: no
|
||||
|
||||
- name: Ensure pam_faillock authfail/account lines exist
|
||||
ansible.builtin.blockinfile:
|
||||
path: /etc/pam.d/system-auth
|
||||
insertafter: "^auth.*required.*pam_unix.so"
|
||||
block: |
|
||||
auth [default=die] pam_faillock.so authfail deny=5 unlock_time=900 audit
|
||||
account required pam_faillock.so
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
---
|
||||
unused_services:
|
||||
- avahi-daemon
|
||||
- bluetooth
|
||||
- cups
|
||||
- rpcbind
|
||||
- nfslock
|
||||
- rsh.socket
|
||||
- telnet.socket
|
||||
- rsyncd
|
||||
@@ -0,0 +1,10 @@
|
||||
- name: Ensure list of unnecessary services are stopped and masked
|
||||
ansible.builtin.service:
|
||||
name: "{{ svc }}"
|
||||
enabled: no
|
||||
state: stopped
|
||||
masked: yes
|
||||
loop: "{{ unused_services }}"
|
||||
loop_control:
|
||||
loop_var: svc
|
||||
ignore_errors: true
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
---
|
||||
# General settings
|
||||
ssh_protocol: 2
|
||||
ssh_permit_root_login: "no"
|
||||
ssh_password_authentication: "no"
|
||||
ssh_challenge_response_authentication: "no"
|
||||
ssh_kerberos_authentication: "no"
|
||||
ssh_gssapi_authentication: "no"
|
||||
ssh_use_pam: "yes"
|
||||
PrintLastLog: "yes"
|
||||
|
||||
# Session security
|
||||
ssh_x11_forwarding: "no"
|
||||
ssh_allow_tcp_forwarding: "yes"
|
||||
ssh_permit_tunnel: "no"
|
||||
ssh_allow_agent_forwarding: "no"
|
||||
|
||||
# Idle timeout / login attempts
|
||||
ssh_client_alive_interval: 300
|
||||
ssh_client_alive_count_max: 3
|
||||
ssh_login_grace_time: 60
|
||||
ssh_max_auth_tries: 3
|
||||
ssh_max_sessions: 2
|
||||
ssh_max_startups: "10:30:60"
|
||||
|
||||
# SSH access restrictions
|
||||
ssh_allow_users: "adminuser ansibleuser"
|
||||
|
||||
# Strong encryption
|
||||
ssh_ciphers: "aes256-ctr,aes192-ctr,aes128-ctr"
|
||||
ssh_macs: "hmac-sha2-512,hmac-sha2-256"
|
||||
ssh_kex_algorithms: "[email protected],diffie-hellman-group-exchange-sha256"
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
- name: Ensure OpenSSH server is installed
|
||||
ansible.builtin.package:
|
||||
name: openssh-server
|
||||
state: present
|
||||
|
||||
- name: Ensure SSH daemon is enabled and running
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Backup current sshd_config
|
||||
ansible.builtin.copy:
|
||||
src: /etc/ssh/sshd_config
|
||||
dest: /etc/ssh/sshd_config.bak
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
remote_src: true
|
||||
|
||||
- name: Harden SSH configuration
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: "{{ ssh_item.regexp }}"
|
||||
line: "{{ ssh_item.line }}"
|
||||
state: present
|
||||
backrefs: yes
|
||||
loop:
|
||||
- regexp: "^#?Protocol"
|
||||
line: "Protocol {{ ssh_protocol }}"
|
||||
- regexp: "^#?PermitRootLogin"
|
||||
line: "PermitRootLogin {{ ssh_permit_root_login }}"
|
||||
- regexp: "^#?PasswordAuthentication"
|
||||
line: "PasswordAuthentication {{ ssh_password_authentication }}"
|
||||
- regexp: "^#?ChallengeResponseAuthentication"
|
||||
line: "ChallengeResponseAuthentication {{ ssh_challenge_response_authentication }}"
|
||||
- regexp: "^#?KerberosAuthentication"
|
||||
line: "KerberosAuthentication {{ ssh_kerberos_authentication }}"
|
||||
- regexp: "^#?GSSAPIAuthentication"
|
||||
line: "GSSAPIAuthentication {{ ssh_gssapi_authentication }}"
|
||||
- regexp: "^#?UsePAM"
|
||||
line: "UsePAM {{ ssh_use_pam }}"
|
||||
- regexp: "^#?X11Forwarding"
|
||||
line: "X11Forwarding {{ ssh_x11_forwarding }}"
|
||||
- regexp: "^#?AllowTcpForwarding"
|
||||
line: "AllowTcpForwarding {{ ssh_allow_tcp_forwarding }}"
|
||||
- regexp: "^#?PermitTunnel"
|
||||
line: "PermitTunnel {{ ssh_permit_tunnel }}"
|
||||
- regexp: "^#?AllowAgentForwarding"
|
||||
line: "AllowAgentForwarding {{ ssh_allow_agent_forwarding }}"
|
||||
- regexp: "^#?ClientAliveInterval"
|
||||
line: "ClientAliveInterval {{ ssh_client_alive_interval }}"
|
||||
- regexp: "^#?ClientAliveCountMax"
|
||||
line: "ClientAliveCountMax {{ ssh_client_alive_count_max }}"
|
||||
- regexp: "^#?LoginGraceTime"
|
||||
line: "LoginGraceTime {{ ssh_login_grace_time }}"
|
||||
- regexp: "^#?MaxAuthTries"
|
||||
line: "MaxAuthTries {{ ssh_max_auth_tries }}"
|
||||
- regexp: "^#?MaxSessions"
|
||||
line: "MaxSessions {{ ssh_max_sessions }}"
|
||||
- regexp: "^#?MaxStartups"
|
||||
line: "MaxStartups {{ ssh_max_startups }}"
|
||||
- regexp: "^#?Ciphers"
|
||||
line: "Ciphers {{ ssh_ciphers }}"
|
||||
- regexp: "^#?MACs"
|
||||
line: "MACs {{ ssh_macs }}"
|
||||
- regexp: "^#?KexAlgorithms"
|
||||
line: "KexAlgorithms {{ ssh_kex_algorithms }}"
|
||||
- regexp: "^#?PrintLastLog"
|
||||
line: "PrintLastLog {{ PrintLastLog }}"
|
||||
- regexp: '^\s*#?\s*AllowUsers'
|
||||
line: "AllowUsers {{ ssh_allow_users }}"
|
||||
- regexp: "^#?Banner"
|
||||
line: "Banner /etc/issue.net"
|
||||
loop_control:
|
||||
loop_var: ssh_item
|
||||
|
||||
- name: Restart SSH service to apply changes
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
# Parent CIS hardening role: import each subrole with single tag
|
||||
- import_role:
|
||||
name: cis_hardening/kernel_hardening
|
||||
tags: [cis_hardening]
|
||||
|
||||
- import_role:
|
||||
name: cis_hardening/password_policy
|
||||
tags: [cis_hardening]
|
||||
|
||||
- import_role:
|
||||
name: cis_hardening/file_permissions
|
||||
tags: [cis_hardening]
|
||||
|
||||
- import_role:
|
||||
name: cis_hardening/service_disable
|
||||
tags: [cis_hardening]
|
||||
|
||||
- import_role:
|
||||
name: cis_hardening/banners
|
||||
tags: [cis_hardening]
|
||||
|
||||
- import_role:
|
||||
name: cis_hardening/ssh_hardening
|
||||
tags: [cis_hardening]
|
||||
|
||||
- import_role:
|
||||
name: cis_hardening/filesystem_hardening
|
||||
tags: [cis_hardening]
|
||||
@@ -0,0 +1,55 @@
|
||||
# Vault connection
|
||||
vault_addr: "http://VAULT_IP:VAULT_PORT"
|
||||
|
||||
# Directory paths
|
||||
haproxy_certs_dir: "/etc/haproxy/certs"
|
||||
haproxy_cert_path: "{{ haproxy_pem_path }}"
|
||||
haproxy_pem_path: "{{ haproxy_certs_dir }}/haproxy.pem"
|
||||
haproxy_ca_path: "{{ haproxy_certs_dir }}/haproxy_ca.crt"
|
||||
|
||||
# Systemd service names
|
||||
haproxy_service: "haproxy"
|
||||
|
||||
# HAProxy tuning
|
||||
haproxy_global_maxconn: 4096
|
||||
haproxy_timeout_connect_ms: 5000
|
||||
haproxy_timeout_client_ms: 60000
|
||||
haproxy_timeout_server_ms: 60000
|
||||
haproxy_enable_hsts: true
|
||||
|
||||
vault_agent_templates:
|
||||
- src: "haproxy.pem.ctmpl"
|
||||
dest: "{{ haproxy_pem_path }}"
|
||||
service: "haproxy"
|
||||
- src: "haproxy_ca.ctmpl"
|
||||
dest: "{{ haproxy_ca_path }}"
|
||||
service: "haproxy"
|
||||
|
||||
# === Services routed internally through HAProxy ===
|
||||
securemtls_services:
|
||||
vault:
|
||||
hostname: vault.local.DOMAIN_PLACEHOLDER
|
||||
balance: roundrobin
|
||||
mode: http
|
||||
timeout_connect: 5s
|
||||
timeout_server: 30s
|
||||
default_server_inter: 3s
|
||||
default_server_rise: 2
|
||||
default_server_fall: 3
|
||||
backend_servers:
|
||||
- address: VAULT_IP
|
||||
port: VAULT_PORT
|
||||
proto: http
|
||||
headers:
|
||||
- "http-request set-header Host vault.local.DOMAIN_PLACEHOLDER"
|
||||
- "http-request set-header X-Forwarded-Proto https"
|
||||
rewrites:
|
||||
- "http-response replace-header Location ^http://VAULT_IP:VAULT_PORT/(.*) https://vault.local.DOMAIN_PLACEHOLDER/\\1"
|
||||
|
||||
technitium:
|
||||
hostname: "technitium.local.DOMAIN_PLACEHOLDER"
|
||||
backend_mode: http
|
||||
backend_servers:
|
||||
- address: TECHNITIUM_IP
|
||||
port: TECHNITIUM_PORT
|
||||
proto: "http"
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Reload HAProxy
|
||||
systemd:
|
||||
name: haproxy
|
||||
state: reloaded
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
- name: Install HAProxy
|
||||
dnf:
|
||||
name: haproxy
|
||||
state: present
|
||||
|
||||
- name: Ensure firewalld is running and enabled
|
||||
systemd:
|
||||
name: firewalld
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Allow inbound HTTPS (443/tcp)
|
||||
ansible.posix.firewalld:
|
||||
service: https
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
|
||||
- name: Ensure HAProxy certificate directories exist
|
||||
file:
|
||||
path: "{{ cert_dir }}"
|
||||
state: directory
|
||||
owner: haproxy
|
||||
group: haproxy
|
||||
mode: "0750"
|
||||
loop:
|
||||
- "{{ haproxy_certs_dir }}"
|
||||
- "{{ haproxy_certs_dir }}/clients"
|
||||
loop_control:
|
||||
loop_var: cert_dir
|
||||
|
||||
- name: Apply SELinux context for HAProxy certs
|
||||
command: semanage fcontext -a -t etc_t "{{ haproxy_certs_dir }}(/.*)?"
|
||||
become: true
|
||||
register: semanage_fcontext_result
|
||||
failed_when: false
|
||||
changed_when: semanage_fcontext_result.rc == 0
|
||||
|
||||
- name: Restore SELinux context for HAProxy certs
|
||||
command: restorecon -Rv "{{ haproxy_certs_dir }}"
|
||||
become: true
|
||||
changed_when: false
|
||||
|
||||
- name: Allow HAProxy to connect to any port (SELinux)
|
||||
ansible.posix.seboolean:
|
||||
name: haproxy_connect_any
|
||||
state: true
|
||||
persistent: true
|
||||
|
||||
- name: Wait for HAProxy PEM and CA files
|
||||
wait_for:
|
||||
path: "{{ pem_item }}"
|
||||
state: present
|
||||
timeout: 120
|
||||
loop:
|
||||
- "{{ haproxy_pem_path }}"
|
||||
- "{{ haproxy_ca_path }}"
|
||||
loop_control:
|
||||
loop_var: pem_item
|
||||
|
||||
- name: Ensure permissions for HAProxy certs
|
||||
file:
|
||||
path: "{{ cert_item }}"
|
||||
owner: haproxy
|
||||
group: haproxy
|
||||
mode: "0644"
|
||||
loop:
|
||||
- "{{ haproxy_pem_path }}"
|
||||
- "{{ haproxy_ca_path }}"
|
||||
loop_control:
|
||||
loop_var: cert_item
|
||||
|
||||
- name: Deploy HAProxy configuration
|
||||
template:
|
||||
src: haproxy.cfg.j2
|
||||
dest: /etc/haproxy/haproxy.cfg
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Reload HAProxy
|
||||
|
||||
- name: Ensure HAProxy service starts after vault-agent
|
||||
lineinfile:
|
||||
path: /usr/lib/systemd/system/haproxy.service
|
||||
regexp: "^After="
|
||||
line: "After=network-online.target vault-agent.service"
|
||||
backup: yes
|
||||
|
||||
- name: Reload systemd daemon
|
||||
command: systemctl daemon-reload
|
||||
|
||||
- name: Enable and start HAProxy
|
||||
systemd:
|
||||
name: haproxy
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
- name: Check SELinux mode
|
||||
command: getenforce
|
||||
register: selinux_state
|
||||
changed_when: false
|
||||
|
||||
- name: Warn if SELinux is not enforcing
|
||||
debug:
|
||||
msg: "⚠️ SELinux is not enforcing — HAProxy cert contexts may not be correctly applied."
|
||||
when: selinux_state.stdout != "Enforcing"
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# ==========================================
|
||||
# HAProxy global settings
|
||||
# ==========================================
|
||||
global
|
||||
log /dev/log local0
|
||||
log /dev/log local1 notice
|
||||
chroot /var/lib/haproxy
|
||||
pidfile /run/haproxy.pid
|
||||
maxconn {{ haproxy_global_maxconn | default(4096) }}
|
||||
user haproxy
|
||||
group haproxy
|
||||
daemon
|
||||
|
||||
defaults
|
||||
log global
|
||||
option httplog
|
||||
option dontlognull
|
||||
timeout connect {{ haproxy_timeout_connect_ms | default('5000') }}
|
||||
timeout client {{ haproxy_timeout_client_ms | default('60000') }}
|
||||
timeout server {{ haproxy_timeout_server_ms | default('60000') }}
|
||||
option http-server-close
|
||||
option forwardfor
|
||||
retries 3
|
||||
maxconn 2000
|
||||
|
||||
# ==========================================
|
||||
# HAProxy frontends and backends template
|
||||
# ==========================================
|
||||
|
||||
frontend https-in
|
||||
mode http
|
||||
bind *:443 ssl crt {{ haproxy_pem_path }} ca-file {{ haproxy_ca_path }} verify required alpn h2,http/1.1
|
||||
option httplog
|
||||
option forwardfor
|
||||
timeout client 30s
|
||||
|
||||
http-request set-header X-Client-Cert %[ssl_c_der,base64]
|
||||
|
||||
{% for service_name, service in securemtls_services.items() %}
|
||||
acl host_{{ service_name }} hdr(host) -i {{ service.hostname }}
|
||||
use_backend {{ service_name }} if host_{{ service_name }}
|
||||
{% endfor %}
|
||||
|
||||
# ==========================================
|
||||
# Backends
|
||||
# ==========================================
|
||||
{% for service_name, service in securemtls_services.items() %}
|
||||
backend {{ service_name }}
|
||||
balance {{ service.balance | default('roundrobin') }}
|
||||
mode {{ service.mode | default('http') }}
|
||||
timeout connect {{ service.timeout_connect | default('5s') }}
|
||||
timeout server {{ service.timeout_server | default('30s') }}
|
||||
option {{ service.options | default('http-server-close') }}
|
||||
default-server inter {{ service.default_server_inter | default('3s') }}
|
||||
default-server rise {{ service.default_server_rise | default('2') }}
|
||||
default-server fall {{ service.default_server_fall | default('3') }}
|
||||
|
||||
{% for server in service.backend_servers %}
|
||||
{% if server.proto == "https" %}
|
||||
server {{ service_name }}-{{ loop.index }} {{ server.address }}:{{ server.port }} ssl verify none check
|
||||
{% else %}
|
||||
server {{ service_name }}-{{ loop.index }} {{ server.address }}:{{ server.port }} check
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if service.headers is defined %}
|
||||
{% for hdr in service.headers %}
|
||||
{{ hdr }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if service.rewrites is defined %}
|
||||
{% for rewrite in service.rewrites %}
|
||||
{{ rewrite }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
# ==========================================
|
||||
# Vault backend for HAProxy
|
||||
# ==========================================
|
||||
backend vault_backend
|
||||
mode tcp
|
||||
option ssl-hello-chk
|
||||
server vault1 VAULT_IP:VAULT_PORT ssl verify required ca-file {{ haproxy_ca_path }} crt {{ haproxy_cert_path }}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Networking
|
||||
technitium_port: 53
|
||||
technitium_tls_port: 853
|
||||
technitium_admin_port: TECHNITIUM_PORT
|
||||
technitium_bind_address: "0.0.0.0"
|
||||
|
||||
# ==========================================================
|
||||
# Directory paths
|
||||
# ==========================================================
|
||||
technitium_certs_dir: "/etc/technitium/certs"
|
||||
technitium_crt_path: "{{ technitium_certs_dir }}/technitium.crt"
|
||||
technitium_key_path: "{{ technitium_certs_dir }}/technitium.key"
|
||||
technitium_ca_path: "{{ technitium_certs_dir }}/technitium_ca.crt"
|
||||
|
||||
# Install paths
|
||||
technitium_install_dir: "/opt/technitium/dns"
|
||||
technitium_data_dir: "{{ technitium_install_dir }}/data"
|
||||
|
||||
vault_agent_templates:
|
||||
- src: "technitium_cert.ctmpl"
|
||||
dest: "{{ technitium_crt_path }}"
|
||||
service: "technitium"
|
||||
- src: "technitium_key.ctmpl"
|
||||
dest: "{{ technitium_key_path }}"
|
||||
service: "technitium"
|
||||
- src: "technitium_ca.ctmpl"
|
||||
dest: "{{ technitium_ca_path }}"
|
||||
service: "technitium"
|
||||
@@ -0,0 +1,8 @@
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Restart technitium
|
||||
ansible.builtin.systemd:
|
||||
name: technitium
|
||||
state: restarted
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
# ==========================================================
|
||||
# Install and configure Technitium DNS Server
|
||||
# ==========================================================
|
||||
|
||||
- name: Ensure Technitium install directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ technitium_install_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure Technitium data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ technitium_data_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure Technitium TLS directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ technitium_certs_dir }}"
|
||||
state: directory
|
||||
owner: vault
|
||||
group: vault
|
||||
mode: "0700"
|
||||
|
||||
# ==========================================================
|
||||
# Install Technitium if not already installed
|
||||
# ==========================================================
|
||||
- name: Install Technitium DNS Server
|
||||
ansible.builtin.command: bash install.sh
|
||||
args:
|
||||
chdir: /tmp
|
||||
creates: "{{ technitium_install_dir }}/start.sh"
|
||||
environment:
|
||||
INSTALL_URL: "https://download.technitium.com/dns/install.sh"
|
||||
register: technitium_install_result
|
||||
ignore_errors: true
|
||||
changed_when: technitium_install_result.rc == 0
|
||||
|
||||
# ==========================================================
|
||||
# SELinux setup (idempotent)
|
||||
# ==========================================================
|
||||
- name: Apply SELinux context for Technitium certs
|
||||
ansible.builtin.command: semanage fcontext -a -t etc_t '{{ technitium_certs_dir }}(/.*)?'
|
||||
register: selinux_ctx
|
||||
changed_when: selinux_ctx.rc == 0
|
||||
ignore_errors: true
|
||||
|
||||
- name: Restore SELinux context for Technitium certs
|
||||
ansible.builtin.command: restorecon -Rv "{{ technitium_certs_dir }}"
|
||||
changed_when: false
|
||||
|
||||
# ==========================================================
|
||||
# Firewalld setup
|
||||
# ==========================================================
|
||||
- name: Ensure firewalld is running
|
||||
ansible.builtin.systemd:
|
||||
name: firewalld
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Open Technitium DNS and admin ports
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ item.port }}/{{ item.proto }}"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
loop:
|
||||
- { port: 53, proto: "udp" }
|
||||
- { port: 53, proto: "tcp" }
|
||||
- { port: 853, proto: "tcp" }
|
||||
- { port: 443, proto: "tcp" }
|
||||
- { port: 5380, proto: "tcp" }
|
||||
|
||||
# ==========================================================
|
||||
# Systemd service deployment
|
||||
# ==========================================================
|
||||
- name: Deploy Technitium systemd unit
|
||||
ansible.builtin.template:
|
||||
src: "technitium.service.j2"
|
||||
dest: "/etc/systemd/system/technitium.service"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify:
|
||||
- Reload systemd
|
||||
- Restart technitium
|
||||
|
||||
- name: Wait for Vault-Agent to render certificates
|
||||
ansible.builtin.wait_for:
|
||||
path: "{{ cert_path }}"
|
||||
state: present
|
||||
timeout: 60
|
||||
loop:
|
||||
- "{{ technitium_crt_path }}"
|
||||
- "{{ technitium_key_path }}"
|
||||
- "{{ technitium_ca_path }}"
|
||||
loop_control:
|
||||
loop_var: cert_path
|
||||
|
||||
- name: Fix ownership and permissions for Technitium certs
|
||||
ansible.builtin.file:
|
||||
path: "{{ cert.path }}"
|
||||
owner: vault
|
||||
group: vault
|
||||
mode: "{{ cert.mode }}"
|
||||
loop:
|
||||
- { path: "{{ technitium_crt_path }}", mode: "0644" }
|
||||
- { path: "{{ technitium_key_path }}", mode: "0600" }
|
||||
- { path: "{{ technitium_ca_path }}", mode: "0644" }
|
||||
loop_control:
|
||||
loop_var: cert
|
||||
|
||||
# ==========================================================
|
||||
# Start and enable service
|
||||
# ==========================================================
|
||||
- name: Ensure Technitium DNS service is running
|
||||
ansible.builtin.systemd:
|
||||
name: technitium
|
||||
enabled: true
|
||||
state: started
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Technitium DNS Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory={{ technitium_install_dir }}
|
||||
#ExecStart={{ technitium_install_dir }}/start.sh
|
||||
ExecStart={{ technitium_install_dir }}/start.sh --tls-cert /etc/technitium/certs/technitium.crt --tls-key /etc/technitium/certs/technitium.key
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,4 @@
|
||||
technitium_service_name: "technitium"
|
||||
technitium_binary_url: "https://download.technitium.com/dns/DnsServerPortable.tar.gz"
|
||||
technitium_data_dir: "{{ technitium_install_dir }}/data"
|
||||
technitium_conf_file: "{{ technitium_install_dir }}/Technitium.Dns.Server.json"
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
vault_addr: "http://VAULT_IP:VAULT_PORT"
|
||||
|
||||
vault_agent_config_path: "/etc/vault-agent-config.hcl"
|
||||
vault_agent_pid_file: "/var/run/vault-agent.pid"
|
||||
vault_agent_user: "root"
|
||||
vault_agent_service: "vault-agent"
|
||||
vault_agent_systemd_unit: "/etc/systemd/system/vault-agent.service"
|
||||
|
||||
# Paths to role_id / secret_id files (written by bootstrap process)
|
||||
vault_role_id_file: "/etc/vault-agent/role_id"
|
||||
vault_secret_id_file: "/etc/vault-agent/secret_id"
|
||||
|
||||
# Templates
|
||||
vault_templates_dir: "/etc/vault-agent-templates"
|
||||
|
||||
vault_agent_templates:
|
||||
# HAProxy
|
||||
- src: "haproxy.pem.ctmpl"
|
||||
dest: "/etc/haproxy/certs/haproxy.pem"
|
||||
service: haproxy
|
||||
- src: "haproxy_ca.ctmpl"
|
||||
dest: "/etc/haproxy/certs/haproxy_ca.crt"
|
||||
service: haproxy
|
||||
- src: "thesabear-client.ctmpl"
|
||||
dest: "/etc/haproxy/certs/clients/thesabear-client.pem"
|
||||
service: "haproxy"
|
||||
|
||||
# Technitium
|
||||
- src: "technitium_cert.ctmpl"
|
||||
dest: "/etc/technitium/certs/technitium.crt"
|
||||
service: technitium
|
||||
- src: "technitium_key.ctmpl"
|
||||
dest: "/etc/technitium/certs/technitium.key"
|
||||
service: technitium
|
||||
- src: "technitium_ca.ctmpl"
|
||||
dest: "/etc/technitium/certs/technitium_ca.crt"
|
||||
service: technitium
|
||||
|
||||
# Default Vault PKI options (can remain)
|
||||
pki_mount_path: "pki"
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{- with secret "pki/cert/ca" -}}
|
||||
{{ trim .Data.certificate "\n" }}
|
||||
{{- end }}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{% raw %}
|
||||
{{- with secret "pki/issue/{{ wildcard_role_name }}" "common_name={{ common_name }}" -}}
|
||||
{{ .Data.certificate }}
|
||||
{{ .Data.private_key }}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{- end }}
|
||||
{% endraw %}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{{ with secret "pki/issue/securevault-wildcard" "common_name=*.local.DOMAIN_PLACEHOLDER" "ttl=8760h" }}
|
||||
{{ .Data.certificate }}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{ .Data.private_key }}
|
||||
{{ end }}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{- with secret "pki/issue/securevault-wildcard" "common_name=*.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{- end }}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{- with secret "pki_int/issue/securevault-wildcard" "common_name=technitium.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{- end }}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{- with secret "pki_int/issue/securevault-wildcard" "common_name=technitium.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.certificate }}
|
||||
{{- end }}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{- with secret "pki_int/issue/securevault-wildcard" "common_name=technitium.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.private_key }}
|
||||
{{- end }}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{{ with secret "pki_int/issue/mtls-client-cert" "common_name=clientname.local.DOMAIN_PLACEHOLDER" "ttl=8760h" }}
|
||||
{{ .Data.certificate }}
|
||||
{{ .Data.private_key }}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{ end }}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
- name: Reload systemd
|
||||
command: systemctl daemon-reload
|
||||
@@ -0,0 +1,230 @@
|
||||
---
|
||||
# ==========================================================
|
||||
# 1️⃣ Install Vault and Dependencies
|
||||
# ==========================================================
|
||||
- name: Ensure HashiCorp repository (RHEL9)
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/yum.repos.d/hashicorp.repo
|
||||
mode: "0644"
|
||||
content: |
|
||||
[hashicorp]
|
||||
name=HashiCorp Stable Repository
|
||||
baseurl=https://rpm.releases.hashicorp.com/RHEL/9/x86_64/stable/
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=https://rpm.releases.hashicorp.com/gpg
|
||||
|
||||
- name: Install Vault (includes vault-agent)
|
||||
ansible.builtin.dnf:
|
||||
name: vault
|
||||
state: present
|
||||
|
||||
# ==========================================================
|
||||
# 2️⃣ Firewall Configuration
|
||||
# ==========================================================
|
||||
- name: Ensure firewalld is running and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: firewalld
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Allow outbound access to Vault (VAULT_PORT/tcp)
|
||||
ansible.posix.firewalld:
|
||||
rich_rule: >
|
||||
rule family="ipv4"
|
||||
destination address="VAULT_IP"
|
||||
port port="VAULT_PORT" protocol="tcp" accept
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
|
||||
# ==========================================================
|
||||
# 3️⃣ Vault-Agent Directories and Configs
|
||||
# ==========================================================
|
||||
- name: Ensure Vault-Agent directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ vault_dir.path }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "{{ vault_dir.mode | default('0750') }}"
|
||||
loop:
|
||||
- { path: "{{ vault_templates_dir }}" }
|
||||
- { path: "/etc/vault-agent", mode: "0700" }
|
||||
loop_control:
|
||||
loop_var: vault_dir
|
||||
|
||||
- name: Deploy Vault-Agent configuration
|
||||
ansible.builtin.template:
|
||||
src: vault-agent-config.hcl.j2
|
||||
dest: "{{ vault_agent_config_path }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
- name: Install Vault-Agent systemd unit
|
||||
ansible.builtin.template:
|
||||
src: vault-agent.service.j2
|
||||
dest: "{{ vault_agent_systemd_unit }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Reload systemd
|
||||
|
||||
- name: Enable Vault-Agent but do not start yet
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ vault_agent_service }}"
|
||||
enabled: true
|
||||
state: stopped
|
||||
|
||||
# ==========================================================
|
||||
# 4️⃣ Vault-Agent Templates and Cert Paths
|
||||
# ==========================================================
|
||||
- name: Copy Vault-Agent templates
|
||||
ansible.builtin.copy:
|
||||
src: "{{ vault_item.src }}"
|
||||
dest: "{{ vault_templates_dir }}/{{ vault_item.src | basename }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
loop: "{{ vault_agent_templates }}"
|
||||
loop_control:
|
||||
loop_var: vault_item
|
||||
|
||||
- name: Ensure directories for Vault-rendered files exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ vault_template.dest | dirname }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop: "{{ vault_agent_templates }}"
|
||||
loop_control:
|
||||
loop_var: vault_template
|
||||
|
||||
- name: Start Vault-Agent (now that templates exist)
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ vault_agent_service }}"
|
||||
state: started
|
||||
|
||||
# ==========================================================
|
||||
# 5️⃣ AppRole Authentication Setup
|
||||
# ==========================================================
|
||||
- name: Fetch AppRole credentials for this host
|
||||
delegate_to: localhost
|
||||
block:
|
||||
- name: Get role_id
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
vault read -field=role_id auth/approle/role/{{ inventory_hostname }}-role/role-id
|
||||
register: role_id_raw
|
||||
changed_when: false
|
||||
failed_when: role_id_raw.stdout | trim == ""
|
||||
environment:
|
||||
VAULT_ADDR: "http://VAULT_IP:VAULT_PORT"
|
||||
VAULT_TOKEN: "{{ vault_token }}"
|
||||
|
||||
- name: Get secret_id
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
vault write -f -field=secret_id auth/approle/role/{{ inventory_hostname }}-role/secret-id
|
||||
register: secret_id_raw
|
||||
changed_when: false
|
||||
failed_when: secret_id_raw.stdout | trim == ""
|
||||
environment:
|
||||
VAULT_ADDR: "http://VAULT_IP:VAULT_PORT"
|
||||
VAULT_TOKEN: "{{ vault_token }}"
|
||||
|
||||
- name: Write AppRole credentials to Vault-Agent directory
|
||||
ansible.builtin.copy:
|
||||
dest: "/etc/vault-agent/{{ cred.name }}"
|
||||
content: "{{ cred.content }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
loop:
|
||||
- { name: "role_id", content: "{{ role_id_raw.stdout | trim }}" }
|
||||
- { name: "secret_id", content: "{{ secret_id_raw.stdout | trim }}" }
|
||||
loop_control:
|
||||
loop_var: cred
|
||||
label: "{{ cred.name }}"
|
||||
|
||||
# ==========================================================
|
||||
# 6️⃣ Register Service in Vault
|
||||
# ==========================================================
|
||||
- name: Login to Vault with AppRole and register VM
|
||||
delegate_to: localhost
|
||||
block:
|
||||
- name: Login to Vault with AppRole
|
||||
ansible.builtin.command:
|
||||
cmd: >
|
||||
vault write -field=token auth/approle/login
|
||||
role_id={{ role_id_raw.stdout | trim }}
|
||||
secret_id={{ secret_id_raw.stdout | trim }}
|
||||
register: vault_login
|
||||
changed_when: false
|
||||
environment:
|
||||
VAULT_ADDR: "http://VAULT_IP:VAULT_PORT"
|
||||
|
||||
- name: Register VM IP in Vault KV
|
||||
ansible.builtin.uri:
|
||||
url: "http://VAULT_IP:VAULT_PORT/v1/kv/data/services/{{ inventory_hostname }}"
|
||||
method: POST
|
||||
headers:
|
||||
X-Vault-Token: "{{ vault_login.stdout }}"
|
||||
body:
|
||||
data:
|
||||
ip: "{{ ansible_default_ipv4.address }}"
|
||||
body_format: json
|
||||
status_code: [200, 204]
|
||||
|
||||
# ==========================================================
|
||||
# 7️⃣ SELinux Context for Vault Rendered Paths
|
||||
# ==========================================================
|
||||
- name: Check if SELinux is enforcing
|
||||
ansible.builtin.command: getenforce
|
||||
register: selinux_status
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Gather all Vault-Agent template and rendered paths
|
||||
ansible.builtin.set_fact:
|
||||
all_vault_paths: >-
|
||||
{{
|
||||
(
|
||||
(vault_agent_templates | default([]) | map(attribute='dest') | map('dirname') | list)
|
||||
+
|
||||
(vault_agent_templates | default([]) | map(attribute='dest') | list)
|
||||
) | unique
|
||||
}}
|
||||
when: vault_agent_templates is defined and vault_agent_templates | length > 0
|
||||
|
||||
- name: Filter only paths that exist on the host
|
||||
ansible.builtin.find:
|
||||
paths: "{{ all_vault_paths }}"
|
||||
file_type: directory
|
||||
register: existing_selinux_paths
|
||||
when: all_vault_paths is defined and all_vault_paths | length > 0
|
||||
|
||||
- name: Apply SELinux fcontext to all existing Vault paths
|
||||
ansible.builtin.command:
|
||||
cmd: semanage fcontext -a -t etc_t '{{ selinux_path_item.path }}(/.*)?'
|
||||
loop: "{{ existing_selinux_paths.files | default([]) }}"
|
||||
loop_control:
|
||||
loop_var: selinux_path_item
|
||||
ignore_errors: true
|
||||
when: selinux_status.stdout == "Enforcing"
|
||||
|
||||
- name: Restore SELinux context for all existing Vault paths
|
||||
ansible.builtin.command:
|
||||
cmd: restorecon -Rv "{{ selinux_path_item.path }}"
|
||||
loop: "{{ existing_selinux_paths.files | default([]) }}"
|
||||
loop_control:
|
||||
loop_var: selinux_path_item
|
||||
ignore_errors: true
|
||||
when: selinux_status.stdout == "Enforcing"
|
||||
|
||||
- name: Warn if SELinux is not enforcing
|
||||
ansible.builtin.debug:
|
||||
msg: "⚠️ SELinux is not enforcing — Vault-Agent directories may not be properly confined."
|
||||
when: selinux_status.stdout != "Enforcing"
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
pid_file = "{{ vault_agent_pid_file }}"
|
||||
|
||||
auto_auth {
|
||||
method {
|
||||
type = "approle"
|
||||
config = {
|
||||
role_id_file_path = "{{ vault_role_id_file }}"
|
||||
secret_id_file_path = "{{ vault_secret_id_file }}"
|
||||
remove_secret_id_file_after_reading = false
|
||||
}
|
||||
}
|
||||
sink {
|
||||
type = "file"
|
||||
config = {
|
||||
path = "/etc/vault-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{% for tpl in vault_agent_templates if tpl.service == hostvars[inventory_hostname].service %}
|
||||
template {
|
||||
source = "{{ vault_templates_dir }}/{{ tpl.src }}"
|
||||
destination = "{{ tpl.dest }}"
|
||||
perms = "0644"
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
vault {
|
||||
address = "{{ vault_addr }}"
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Vault Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/vault agent -config={{ vault_agent_config_path }}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User={{ vault_agent_user }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,123 @@
|
||||
roles_metadata:
|
||||
# Bootstrap roles
|
||||
base_users:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: false
|
||||
description: "Base user creation tasks"
|
||||
|
||||
disk_resize:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "Disk resizing tasks"
|
||||
|
||||
docker_gitlab_setup:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: false
|
||||
description: "Docker runtime files moving and homelab repo copy"
|
||||
|
||||
endlessh:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: false
|
||||
description: "Endlessh setup"
|
||||
|
||||
firewall_ssh:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "Firewall and SSH configuration"
|
||||
|
||||
hostname_network:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: false
|
||||
description: "Set hostname and networking"
|
||||
|
||||
packages:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "base packages and update tasks"
|
||||
|
||||
portainer_agent:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "Portainer agent setup"
|
||||
|
||||
root_password:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: false
|
||||
description: "Root password setup"
|
||||
|
||||
zsh_custom:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: false
|
||||
description: "Custom Zsh setup"
|
||||
|
||||
# CIS Hardening roles
|
||||
banners:
|
||||
initialization: false
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "Login banners setup"
|
||||
|
||||
file_permissions:
|
||||
initialization: false
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "File permissions hardening"
|
||||
|
||||
filesystem_hardening:
|
||||
initialization: false
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "filesystems hardening"
|
||||
|
||||
kernel_hardening:
|
||||
initialization: false
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "Kernel hardening tasks"
|
||||
|
||||
password_policy:
|
||||
initialization: false
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "Password policy enforcement"
|
||||
|
||||
service_disable:
|
||||
initialization: false
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "Disable unnecessary services"
|
||||
|
||||
ssh_hardening:
|
||||
initialization: false
|
||||
provisioning: false
|
||||
compliance: true
|
||||
description: "SSH hardening"
|
||||
|
||||
# SecureVault roles
|
||||
haproxy:
|
||||
initialization: false
|
||||
provisioning: true
|
||||
compliance: true
|
||||
description: "haproxy deployment"
|
||||
|
||||
vault_agent:
|
||||
initialization: true
|
||||
provisioning: false
|
||||
compliance: false
|
||||
description: "vault_agent deployment"
|
||||
|
||||
technitium:
|
||||
initialization: false
|
||||
provisioning: true
|
||||
compliance: false
|
||||
description: "technitium deployment"
|
||||
Reference in new issue
Block a user