Restructure compose mirror: stacks/ grouped by purpose, not by host
Replaces rocky-docker-lxc/, caddy-vm/, monitoring-vm/, and automation/ with a single stacks/ tree grouped by function (identity-and-security, platform, media, productivity, dev-tools) - 55 stacks total, every one a real directory in the private repos today. Resolves all outstanding drift: adds Wazuh (previously entirely missing), Firefly III and Postiz (never mirrored), the internal-zone Caddy instance and the Cron Scheduler's separate NocoDB instance (both previously conflated with their counterparts under one shared folder); renames Calibre-Web to calibre-web-automated and splits Kodi-MariaDB into kodi + mariadb-latest to match what the private repos actually run now; drops the decommissioned NetBox stack. Also removes automation/iac/old-content/ and automation/ansible/old-playbooks/ in the same pass - both folders were entirely stale content describing a superseded architecture (GitLab instead of Gitea, HAProxy+mTLS SSO instead of the current Caddy+Authentik setup), now replaced by docs/iac-pipeline.md.
This commit is contained in:
155 files changed
+500
-3519
No files matched your search
@@ -1,57 +0,0 @@
|
||||
---
|
||||
- hosts: Docker
|
||||
become: true
|
||||
become_method: sudo
|
||||
become_user: root
|
||||
vars:
|
||||
GOTIFY_IP: xxx #[Your gotify host IP goes here]
|
||||
GOTIFY_PORT: xxx #[Your gotify port goes here]
|
||||
GOTIFY_TOKEN: xxx #[Your app token goes here]
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
tasks:
|
||||
|
||||
- name: prune non-dangling images
|
||||
community.docker.docker_prune:
|
||||
containers: false
|
||||
images: true
|
||||
images_filters:
|
||||
dangling: false
|
||||
networks: false
|
||||
volumes: false
|
||||
builder_cache: false
|
||||
|
||||
- name: Check if reboot is required
|
||||
stat:
|
||||
path: /var/run/reboot-required
|
||||
register: reboot_required_file
|
||||
|
||||
- name: Show Output of required reboots
|
||||
debug: msg="{{ reboot_required_file.stat.exists == true }}"
|
||||
|
||||
- name: Rebooting machine
|
||||
shell: sleep 2 && shutdown -r now "Ansible updates triggered"
|
||||
async: 1
|
||||
poll: 0
|
||||
ignore_errors: true
|
||||
when: reboot_required_file.stat.exists
|
||||
|
||||
- name: Waiting for the machine to come back
|
||||
local_action: wait_for host={{ ansible_host }} state=started port=22 delay=30
|
||||
become: false
|
||||
when: reboot_required_file.stat.exists
|
||||
|
||||
- name: notify via Gotify
|
||||
uri:
|
||||
url: 'http://{{ GOTIFY_IP }}:{{ GOTIFY_PORT }}/message?token={{ GOTIFY_TOKEN }}'
|
||||
method: POST
|
||||
body:
|
||||
title:
|
||||
content: clean docker hosts
|
||||
message:
|
||||
content: The clean docker playbook has completed for {{ ansible_host }} and is now updated. A required reboot is {{ reboot_required_file.stat.exists == true }}.
|
||||
priority:
|
||||
content: '5'
|
||||
body_format: form-multipart
|
||||
register: result
|
||||
...
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
- name: ping test
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
tasks:
|
||||
- name: test ping
|
||||
ansible.builtin.ping:
|
||||
@@ -1,65 +0,0 @@
|
||||
### This is just a sample showing the way of removing apps from a VM using Ansible
|
||||
---
|
||||
- hosts: all
|
||||
become: yes
|
||||
vars:
|
||||
GOTIFY_IP: xxx #[Your gotify host IP goes here]
|
||||
GOTIFY_PORT: xxx #[Your gotify port goes here]
|
||||
GOTIFY_TOKEN: xxx #[Your app token goes here]
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Ansible apt remove crowdsec
|
||||
apt:
|
||||
name: crowdsec
|
||||
state: absent
|
||||
|
||||
- name: Ansible apt remove zabbix
|
||||
apt:
|
||||
name: zabbix
|
||||
state: absent
|
||||
|
||||
- name: Ansible apt remove zabbix-agent
|
||||
apt:
|
||||
name: zabbix-agent
|
||||
state: absent
|
||||
|
||||
- name: Remove dependencies that are no longer required and purge their configuration files
|
||||
ansible.builtin.apt:
|
||||
autoremove: yes
|
||||
purge: true
|
||||
|
||||
- name: Check if reboot is required
|
||||
stat:
|
||||
path: /var/run/reboot-required
|
||||
register: reboot_required_file
|
||||
|
||||
- name: Show Output of required reboots
|
||||
debug: msg="{{ reboot_required_file.stat.exists == true }}"
|
||||
|
||||
- name: Rebooting machine
|
||||
shell: sleep 2 && shutdown -r now "Ansible updates triggered"
|
||||
async: 1
|
||||
poll: 0
|
||||
ignore_errors: true
|
||||
when: reboot_required_file.stat.exists
|
||||
|
||||
- name: Waiting for the machine to come back
|
||||
local_action: wait_for host={{ ansible_host }} state=started port=22 delay=30
|
||||
become: no
|
||||
when: reboot_required_file.stat.exists
|
||||
|
||||
- name: notify via Gotify
|
||||
uri:
|
||||
url: 'http://{{ GOTIFY_IP }}:{{ GOTIFY_PORT }}/message?token={{ GOTIFY_TOKEN }}'
|
||||
method: POST
|
||||
body:
|
||||
title:
|
||||
content: APT Packages Update
|
||||
message:
|
||||
content: The remove apps playbook has completed for {{ ansible_host }} and is now updated. A required reboot is {{ reboot_required_file.stat.exists == true }}.
|
||||
priority:
|
||||
content: '5'
|
||||
body_format: form-multipart
|
||||
register: result
|
||||
...
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- hosts: all
|
||||
become: true
|
||||
tasks:
|
||||
- name: Remove specified repository from sources list
|
||||
ansible.builtin.apt_repository:
|
||||
repo: xxx #put the reponame of the repo you no longer want here
|
||||
state: absent
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
- name: Patching and Updating Linux Systems
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
GOTIFY_IP: xxx #[Your gotify host IP goes here]
|
||||
GOTIFY_PORT: xxx #[Your gotify port goes here]
|
||||
GOTIFY_TOKEN: xxx #[Your app token goes here]
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
tasks:
|
||||
- name: Update Debian-based Systems
|
||||
when: ansible_os_family == 'Debian' or ansible_os_family == 'Ubuntu'
|
||||
apt:
|
||||
update_cache: yes
|
||||
upgrade: dist
|
||||
|
||||
- name: Update RHEL-based Systems
|
||||
when: ansible_os_family == 'RedHat'
|
||||
yum:
|
||||
name: '*'
|
||||
state: latest
|
||||
|
||||
- name: List installed and updated packages
|
||||
shell: grep -E "^$(date +%Y-%m-%d).+ (install|upgrade) " /var/log/dpkg.log |cut -d " " -f 3-5
|
||||
register: result
|
||||
|
||||
- name: Check if Docker is installed
|
||||
command: which docker
|
||||
register: docker_installed
|
||||
ignore_errors: true
|
||||
changed_when: false
|
||||
|
||||
- name: Upgrade Python pip modules for Docker support (only if Docker is installed)
|
||||
when: docker_installed.rc == 0 and ansible_os_family == 'RedHat'
|
||||
pip:
|
||||
name:
|
||||
- docker
|
||||
- requests
|
||||
state: latest
|
||||
executable: pip3
|
||||
|
||||
- name: Show Output of updated files
|
||||
debug: msg="{{ result.stdout_lines }}"
|
||||
|
||||
- name: Check if reboot is required
|
||||
stat:
|
||||
path: /var/run/reboot-required
|
||||
register: reboot_required_file
|
||||
|
||||
- name: Show Output of required reboots
|
||||
debug: msg="{{ reboot_required_file.stat.exists == true }}"
|
||||
|
||||
- name: Rebooting machine
|
||||
shell: sleep 2 && shutdown -r now "Ansible updates triggered"
|
||||
async: 1
|
||||
poll: 0
|
||||
ignore_errors: true
|
||||
when: reboot_required_file.stat.exists
|
||||
|
||||
- name: Waiting for the machine to come back
|
||||
local_action: wait_for host={{ ansible_host }} state=started port=22 delay=30
|
||||
become: false
|
||||
when: reboot_required_file.stat.exists
|
||||
###
|
||||
- name: Remove DNF dependencies that are no longer required.
|
||||
when: ansible_os_family == 'RedHat'
|
||||
ansible.builtin.dnf:
|
||||
autoremove: yes
|
||||
|
||||
- name: Remove APT dependencies that are no longer required.
|
||||
when: ansible_os_family == 'Debian' or ansible_os_family == 'Ubuntu'
|
||||
ansible.builtin.apt:
|
||||
autoremove: yes
|
||||
|
||||
- name: notify via Gotify
|
||||
uri:
|
||||
url: 'http://{{ GOTIFY_IP }}:{{ GOTIFY_PORT }}/message?token={{ GOTIFY_TOKEN }}'
|
||||
method: POST
|
||||
body:
|
||||
title:
|
||||
content: All Packages Update
|
||||
message:
|
||||
content: The upgrade-packages playbook has completed for {{ ansible_host }} and is now updated. A required reboot is {{ reboot_required_file.stat.exists == true }}.
|
||||
priority:
|
||||
content: '5'
|
||||
body_format: form-multipart
|
||||
register: result
|
||||
...
|
||||
@@ -1,112 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: netbox-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- /path/to/netbox/postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -q -t 2 -d $$POSTGRES_DB -U $$POSTGRES_USER
|
||||
start_period: 20s
|
||||
timeout: 30s
|
||||
interval: 10s
|
||||
retries: 5
|
||||
networks:
|
||||
- netbox-net
|
||||
|
||||
redis:
|
||||
image: valkey/valkey:9.0-alpine
|
||||
container_name: netbox-redis
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- valkey-server --appendonly yes --requirepass $$REDIS_PASSWORD
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- /path/to/netbox/redis_data:/data
|
||||
networks:
|
||||
- netbox-net
|
||||
|
||||
redis-cache:
|
||||
image: valkey/valkey:9.0-alpine
|
||||
container_name: netbox-redis-cache
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- valkey-server --requirepass $$REDIS_PASSWORD
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_CACHE_PASSWORD}
|
||||
volumes:
|
||||
- /path/to/netbox/redis_cache:/data
|
||||
networks:
|
||||
- netbox-net
|
||||
|
||||
netbox: &netbox
|
||||
image: netboxcommunity/netbox:latest
|
||||
container_name: netbox
|
||||
user: "netbox:root"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- redis-cache
|
||||
env_file: .env
|
||||
ports:
|
||||
- ${HTTP_PORT}:8080
|
||||
environment:
|
||||
SECRET_KEY: ${NETBOX_SECRET_KEY}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_USER: ${POSTGRES_USER}
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_HOST: postgres
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
REDIS_DATABASE: 0
|
||||
REDIS_CACHE_HOST: redis-cache
|
||||
REDIS_CACHE_PORT: 6379
|
||||
REDIS_CACHE_PASSWORD: ${REDIS_CACHE_PASSWORD}
|
||||
REDIS_CACHE_DATABASE: 1
|
||||
SUPERUSER_NAME: ${SUPERUSER_NAME}
|
||||
SUPERUSER_PASSWORD: ${SUPERUSER_PASSWORD}
|
||||
SUPERUSER_EMAIL: ${SUPERUSER_EMAIL}
|
||||
volumes:
|
||||
- /path/to/netbox/netbox_media:/opt/netbox/netbox/media
|
||||
- /path/to/netbox/netbox_reports:/opt/netbox/netbox/reports
|
||||
- /path/to/netbox/netbox_scripts:/opt/netbox/netbox/scripts
|
||||
- ./configuration/extra.py:/etc/netbox/config/extra.py:ro
|
||||
healthcheck:
|
||||
test: curl -f http://localhost:8080/login/ || exit 1
|
||||
start_period: 90s
|
||||
timeout: 3s
|
||||
interval: 15s
|
||||
networks:
|
||||
- netbox-net
|
||||
|
||||
netbox-worker:
|
||||
<<: *netbox
|
||||
container_name: netbox-worker
|
||||
ports: []
|
||||
depends_on:
|
||||
netbox:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- /opt/netbox/venv/bin/python
|
||||
- /opt/netbox/netbox/manage.py
|
||||
- rqworker
|
||||
healthcheck:
|
||||
test: ps -aux | grep -v grep | grep -q rqworker || exit 1
|
||||
start_period: 20s
|
||||
timeout: 3s
|
||||
interval: 15s
|
||||
|
||||
networks:
|
||||
netbox-net:
|
||||
external: true
|
||||
@@ -1 +0,0 @@
|
||||
asupersecretpasswordfor ansiblevaultunlock
|
||||
@@ -1,3 +0,0 @@
|
||||
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.
|
||||
@@ -1,8 +0,0 @@
|
||||
[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
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
roles_metadata: "{{ lookup('file', 'roles_metadata.yml') | from_yaml }}"
|
||||
@@ -1,14 +0,0 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
64373439343462336430396135326665346633343564656532636363356339326561643161353635
|
||||
3636363336663638373932643830646634366336366363370a616235646239386166343964636336
|
||||
66303364663462396235626566613266313561376466646631346464653564623630333137383536
|
||||
3630363935333333390a633633333264323834323863623035626662326639663731323938633534
|
||||
65303062303837636430346435316538336434386464373836336538633933633465376162333138
|
||||
63626433643734633634363433666139653736306630393031653566666362343833363936383265
|
||||
38656136636434653135633561323839656337623963393731663762333836343665323239356536
|
||||
35663433316535313063323264316338353761323537393035373338376634656265363361623037
|
||||
32323935333262373437373662646538613665626265626534333862616138373361666639376437
|
||||
36613238336332313734316235363265636164363566326636646534646561363666333637323737
|
||||
63313336623130306636383362646138656130383661633738623365663762393761306164333536
|
||||
39356564636164306332333763663461343462653562343330623862363131653863333363333332
|
||||
3162
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
all:
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
hosts:
|
||||
haproxy:
|
||||
ansible_host: xx.xx.xx.xx
|
||||
ansible_port: 22
|
||||
service: haproxy
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
- 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
|
||||
}}
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
- 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
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
- 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
|
||||
}}
|
||||
@@ -1,9 +0,0 @@
|
||||
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
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
- 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
|
||||
@@ -1,28 +0,0 @@
|
||||
- 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
@@ -1,23 +0,0 @@
|
||||
---
|
||||
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
@@ -1,5 +0,0 @@
|
||||
---
|
||||
- name: restart docker
|
||||
service:
|
||||
name: docker
|
||||
state: restarted
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
---
|
||||
# 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
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
endlessh_port: 22
|
||||
endlessh_repo: https://github.com/skeeto/endlessh.git
|
||||
endlessh_clone_path: /usr/local/src/endlessh
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,20 +0,0 @@
|
||||
[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
@@ -1,5 +0,0 @@
|
||||
Port {{ endlessh_port }}
|
||||
Delay 10000
|
||||
MaxLineLength 32
|
||||
MaxClients 4096
|
||||
LogLevel 1
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
---
|
||||
# Customize if needed; this defaults to port 22 for endlessh
|
||||
endlessh_port: 22
|
||||
# The SSH port will be dynamically computed as: 10000 + (vmid % 100)
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,29 +0,0 @@
|
||||
---
|
||||
- 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
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
base_packages:
|
||||
- firewalld
|
||||
- policycoreutils-python-utils
|
||||
- git
|
||||
- make
|
||||
- gcc
|
||||
- cloud-utils-growpart
|
||||
- lvm2
|
||||
- xfsprogs
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,39 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,2 +0,0 @@
|
||||
---
|
||||
root_password_plain: "{{ lookup('env', 'ROOT_PASSWORD') }}"
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
- 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 }}"
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,21 +0,0 @@
|
||||
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
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
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. *
|
||||
*********************************************************************
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
# 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 }}"
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
# 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
@@ -1,16 +0,0 @@
|
||||
---
|
||||
# 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
@@ -1,11 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,4 +0,0 @@
|
||||
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
@@ -1,34 +0,0 @@
|
||||
---
|
||||
# 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
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,7 +0,0 @@
|
||||
---
|
||||
# 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
@@ -1,18 +0,0 @@
|
||||
# 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
@@ -1,78 +0,0 @@
|
||||
---
|
||||
# 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
@@ -1,9 +0,0 @@
|
||||
---
|
||||
# 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
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
# 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
@@ -1,10 +0,0 @@
|
||||
---
|
||||
unused_services:
|
||||
- avahi-daemon
|
||||
- bluetooth
|
||||
- cups
|
||||
- rpcbind
|
||||
- nfslock
|
||||
- rsh.socket
|
||||
- telnet.socket
|
||||
- rsyncd
|
||||
@@ -1,10 +0,0 @@
|
||||
- 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
@@ -1,32 +0,0 @@
|
||||
---
|
||||
# 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"
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
- 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
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
# 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]
|
||||
@@ -1,55 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
- name: Reload HAProxy
|
||||
systemd:
|
||||
name: haproxy
|
||||
state: reloaded
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
- 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
@@ -1,86 +0,0 @@
|
||||
# ==========================================
|
||||
# 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 }}
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,8 +0,0 @@
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Restart technitium
|
||||
ansible.builtin.systemd:
|
||||
name: technitium
|
||||
state: restarted
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
# ==========================================================
|
||||
# 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
@@ -1,16 +0,0 @@
|
||||
[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
|
||||
@@ -1,4 +0,0 @@
|
||||
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"
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
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
@@ -1,3 +0,0 @@
|
||||
{{- with secret "pki/cert/ca" -}}
|
||||
{{ trim .Data.certificate "\n" }}
|
||||
{{- end }}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{% raw %}
|
||||
{{- with secret "pki/issue/{{ wildcard_role_name }}" "common_name={{ common_name }}" -}}
|
||||
{{ .Data.certificate }}
|
||||
{{ .Data.private_key }}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{- end }}
|
||||
{% endraw %}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{{ with secret "pki/issue/securevault-wildcard" "common_name=*.local.DOMAIN_PLACEHOLDER" "ttl=8760h" }}
|
||||
{{ .Data.certificate }}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{ .Data.private_key }}
|
||||
{{ end }}
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{{- with secret "pki/issue/securevault-wildcard" "common_name=*.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{- end }}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{{- with secret "pki_int/issue/securevault-wildcard" "common_name=technitium.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.issuing_ca }}
|
||||
{{- end }}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{{- with secret "pki_int/issue/securevault-wildcard" "common_name=technitium.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.certificate }}
|
||||
{{- end }}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{{- with secret "pki_int/issue/securevault-wildcard" "common_name=technitium.local.DOMAIN_PLACEHOLDER" -}}
|
||||
{{ .Data.private_key }}
|
||||
{{- end }}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{{ 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 }}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
- name: Reload systemd
|
||||
command: systemctl daemon-reload
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
# ==========================================================
|
||||
# 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
@@ -1,30 +0,0 @@
|
||||
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
@@ -1,13 +0,0 @@
|
||||
[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
|
||||
@@ -1,123 +0,0 @@
|
||||
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"
|
||||
@@ -1,20 +0,0 @@
|
||||
Coming Soon...
|
||||
|
||||
A description of how I use a minimal ISO to built out create a base image with defaults configurations which is perfect for its use as a base for creating Proxmox templates from.
|
||||
|
||||
|
||||
|
||||
|
||||
#ROCKY10-CIS
|
||||
cd /mnt/storage/template/iso
|
||||
qm create 5000 --memory 2048 --core 2 --name rocky10-cis --net0 virtio,bridge=vmbr0
|
||||
qm importdisk 5000 packer-rocky10.iso storage --format qcow2
|
||||
qm set 5000 \
|
||||
--scsihw virtio-scsi-pci \
|
||||
--scsi0 storage:5000/vm-5000-disk-0.qcow2,discard=on,ssd=1 \
|
||||
--cpu host \
|
||||
--numa 1 \
|
||||
--balloon 0
|
||||
qm set 5000 --agent enabled=1,freeze-fs-on-backup=1,fstrim_cloned_disks=1
|
||||
qm set 5000 --boot c --bootdisk scsi0
|
||||
qm set 5000 --serial0 socket --vga serial0
|
||||
@@ -1 +0,0 @@
|
||||
SuperSecretPasswordToUnlock.vault_pass.txtFile
|
||||
@@ -1,82 +0,0 @@
|
||||
#version=RHEL10
|
||||
lang en_US.UTF-8
|
||||
keyboard us
|
||||
timezone Europe/Sofia --utc
|
||||
authselect --enablefeatures=with-faillock
|
||||
selinux --enforcing
|
||||
firewall --disabled
|
||||
services --enabled=sshd,qemu-guest-agent
|
||||
firstboot --disable
|
||||
network --bootproto=dhcp --device=link --onboot=on --hostname=rocky10 --activate
|
||||
ignoredisk --only-use=vda
|
||||
rootpw --plaintext thisisatemporarypassword
|
||||
reboot
|
||||
|
||||
# Clear all partitions and create new MBR label
|
||||
clearpart --all --initlabel --drives=vda --disklabel=msdos
|
||||
|
||||
# /boot with ext4
|
||||
part /boot --fstype="ext4" --size=1024 --ondisk=vda --label=BOOT
|
||||
|
||||
# LVM physical volume on rest of disk
|
||||
part pv.01 --fstype="lvmpv" --ondisk=vda --grow --size=1 --label=PV01
|
||||
volgroup rocky pv.01
|
||||
|
||||
# Logical volumes
|
||||
logvol /tmp --fstype=xfs --size=2048 --name=lv_tmp --vgname=rocky --fsoptions="defaults,nodev,nosuid"
|
||||
logvol /home --fstype=xfs --size=2048 --name=lv_home --vgname=rocky --fsoptions="defaults,nodev"
|
||||
logvol /var --fstype=xfs --size=4096 --name=lv_var --vgname=rocky --fsoptions="defaults,nodev"
|
||||
logvol /var/tmp --fstype=xfs --size=1024 --name=lv_vartmp --vgname=rocky --fsoptions="defaults,nodev,nosuid,noexec"
|
||||
logvol /var/log --fstype=xfs --size=2048 --name=lv_varlog --vgname=rocky --fsoptions="defaults,nodev,noexec,nosuid"
|
||||
logvol /var/log/audit --fstype=xfs --size=2048 --name=lv_audit --vgname=rocky --fsoptions="defaults,nodev,noexec,nosuid"
|
||||
logvol / --fstype=xfs --name=lv_root --vgname=rocky --grow --size=512 --fsoptions="defaults"
|
||||
|
||||
# BIOS-compatible bootloader
|
||||
bootloader --location=mbr --boot-drive=vda
|
||||
|
||||
# Repositories
|
||||
repo --name="base" --baseurl=https://download.rockylinux.org/pub/rocky/10/BaseOS/x86_64/os/
|
||||
repo --name="appstream" --baseurl=https://download.rockylinux.org/pub/rocky/10/AppStream/x86_64/os/
|
||||
|
||||
%packages
|
||||
@^minimal-environment
|
||||
openssh-server
|
||||
openssh-clients
|
||||
qemu-guest-agent
|
||||
nano
|
||||
zsh
|
||||
git
|
||||
curl
|
||||
sudo
|
||||
util-linux-user
|
||||
rsync
|
||||
tree
|
||||
nmap-ncat
|
||||
%end
|
||||
|
||||
%post --erroronfail
|
||||
|
||||
# Enable Docker repository
|
||||
dnf install -y epel-release dnf-plugins-core
|
||||
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
|
||||
|
||||
# Install Docker fastfetch jq
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin fastfetch jq
|
||||
|
||||
# Enable services
|
||||
systemctl enable sshd
|
||||
systemctl enable docker
|
||||
systemctl restart docker
|
||||
systemctl restart sshd
|
||||
|
||||
# Enable password authentication for SSH
|
||||
cat > /etc/ssh/sshd_config.d/99-password-login.conf <<EOF
|
||||
PermitRootLogin yes
|
||||
PasswordAuthentication yes
|
||||
Subsystem sftp /usr/libexec/openssh/sftp-server
|
||||
EOF
|
||||
|
||||
# Optional: enable LVM resize service (if available)
|
||||
systemctl enable resize-lvm.service || true
|
||||
|
||||
%end
|
||||
@@ -1,105 +0,0 @@
|
||||
packer {
|
||||
required_plugins {
|
||||
qemu = {
|
||||
version = ">=1.0.0"
|
||||
source = "github.com/hashicorp/qemu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "admin_username" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "admin_password_hash" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "admin_ssh_key" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ansible_username" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ansible_ssh_key" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "iso_url" {
|
||||
type = string
|
||||
default = "https://download.rockylinux.org/pub/rocky/10/isos/x86_64/Rocky-10-latest-x86_64-minimal.iso"
|
||||
}
|
||||
|
||||
variable "iso_checksum" {
|
||||
type = string
|
||||
default = "sha256:de75c2f7cc566ea964017a1e94883913f066c4ebeb1d356964e398ed76cadd12"
|
||||
}
|
||||
|
||||
source "qemu" "rocky10" {
|
||||
iso_url = var.iso_url
|
||||
iso_checksum = var.iso_checksum
|
||||
output_directory = "output-rocky10"
|
||||
shutdown_command = "shutdown -h now"
|
||||
format = "qcow2"
|
||||
disk_size = 20480
|
||||
http_directory = "http"
|
||||
communicator = "ssh"
|
||||
ssh_username = "root"
|
||||
ssh_password = "HaveATemporaryPasswordHere"
|
||||
ssh_timeout = "20m"
|
||||
boot_wait = "20s"
|
||||
boot_command = [
|
||||
"<up><wait>",
|
||||
"e<wait>", # edit boot entry
|
||||
"<down><down><end><wait>", # go to linux line end
|
||||
" inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg",
|
||||
" console=tty0 console=ttyS0,115200n8",
|
||||
" ip=dhcp",
|
||||
"<f10><wait>" # boot with edited entry
|
||||
]
|
||||
|
||||
headless = true
|
||||
qemu_binary = "/usr/bin/qemu-system-x86_64"
|
||||
accelerator = "kvm"
|
||||
cpus = 2
|
||||
memory = 2048
|
||||
|
||||
qemuargs = [
|
||||
["-serial", "file:serial.log"],
|
||||
["-cpu", "host"],
|
||||
["-display", "none"]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
build {
|
||||
sources = ["source.qemu.rocky10"]
|
||||
|
||||
provisioner "file" {
|
||||
source = "scripts/setup-admin-user.sh"
|
||||
destination = "/tmp/setup-admin-user.sh"
|
||||
}
|
||||
|
||||
provisioner "file" {
|
||||
source = "scripts/setup-ansible-user.sh"
|
||||
destination = "/tmp/setup-ansible-user.sh"
|
||||
}
|
||||
|
||||
# Run setup-admin-user.sh
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"chmod +x /tmp/setup-admin-user.sh",
|
||||
"/tmp/setup-admin-user.sh \"${var.admin_username}\" \"${var.admin_password_hash}\" \"${var.admin_ssh_key}\""
|
||||
]
|
||||
}
|
||||
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"chmod +x /tmp/setup-ansible-user.sh",
|
||||
"/tmp/setup-ansible-user.sh \"${var.ansible_username}\" \"${var.ansible_ssh_key}\""
|
||||
]
|
||||
}
|
||||
}
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
USERNAME="$1"
|
||||
HASHED_PASSWORD="$2"
|
||||
SSH_KEY="$3"
|
||||
|
||||
echo "Creating admin user: $USERNAME"
|
||||
|
||||
# --- Install dependencies early
|
||||
if command -v dnf &>/dev/null; then
|
||||
dnf install -y zsh git curl sudo util-linux-user
|
||||
elif command -v apt-get &>/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y zsh git curl sudo
|
||||
else
|
||||
echo "Unsupported package manager"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Create user if not exists
|
||||
if ! id "$USERNAME" &>/dev/null; then
|
||||
useradd -m -s /bin/zsh "$USERNAME"
|
||||
echo "${USERNAME}:${HASHED_PASSWORD}" | chpasswd -e
|
||||
# Disable forced password change and expiration
|
||||
chage -M 99999 -I -1 -E -1 "$USERNAME"
|
||||
else
|
||||
echo "User $USERNAME already exists, skipping creation"
|
||||
fi
|
||||
|
||||
USER_HOME=$(eval echo "~$USERNAME")
|
||||
|
||||
# --- Create fastfetch config
|
||||
mkdir -p "$USER_HOME/.config/fastfetch"
|
||||
cat <<'EOF' > "$USER_HOME/.config/fastfetch/config.jsonc"
|
||||
{
|
||||
"displayMode": "default",
|
||||
"modules": [
|
||||
"os", "host", "kernel", "uptime", "packages", "shell",
|
||||
"cpu", "memory",
|
||||
{
|
||||
"type": "disk",
|
||||
"folders": ["/", "/home", "/tmp", "/var", "/var/log", "/var/log/audit", "/var/tmp"]
|
||||
},
|
||||
"locale"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
chown -R "$USERNAME:$USERNAME" "$USER_HOME/.config"
|
||||
chmod -R 700 "$USER_HOME/.config"
|
||||
|
||||
# --- SSH Setup
|
||||
mkdir -p "$USER_HOME/.ssh"
|
||||
echo "$SSH_KEY" > "$USER_HOME/.ssh/authorized_keys"
|
||||
chmod 700 "$USER_HOME/.ssh"
|
||||
chmod 600 "$USER_HOME/.ssh/authorized_keys"
|
||||
chown -R "$USERNAME:$USERNAME" "$USER_HOME/.ssh"
|
||||
|
||||
# --- Groups and Sudo
|
||||
if ! getent group docker >/dev/null; then
|
||||
groupadd docker
|
||||
fi
|
||||
|
||||
if getent group sudo >/dev/null; then
|
||||
usermod -aG sudo,docker "$USERNAME"
|
||||
elif getent group wheel >/dev/null; then
|
||||
usermod -aG wheel,docker "$USERNAME"
|
||||
else
|
||||
echo "No sudo-equivalent group found, skipping"
|
||||
fi
|
||||
|
||||
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/99_$USERNAME"
|
||||
chmod 440 "/etc/sudoers.d/99_$USERNAME"
|
||||
|
||||
# --- Install Oh My Zsh and config as the user
|
||||
runuser -l "$USERNAME" -c 'export RUNZSH=no CHSH=no KEEP_ZSHRC=yes && \
|
||||
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended'
|
||||
|
||||
# --- Install autosuggestions plugin
|
||||
runuser -l "$USERNAME" -c "git clone https://github.com/zsh-users/zsh-autosuggestions \
|
||||
~/.oh-my-zsh/custom/plugins/zsh-autosuggestions"
|
||||
|
||||
# --- Generate custom .zshrc
|
||||
cat << 'EOF' > "$USER_HOME/.zshrc"
|
||||
export ZSH="\$HOME/.oh-my-zsh"
|
||||
ZSH_THEME="robbyrussell"
|
||||
plugins=(git zsh-autosuggestions)
|
||||
source \$ZSH/oh-my-zsh.sh
|
||||
|
||||
PS1='%F{51}%n%F{255}@%F{227}%m %F{84}%~%f %F{15}$ '
|
||||
fastfetch
|
||||
EOF
|
||||
|
||||
chown "$USERNAME:$USERNAME" "$USER_HOME/.zshrc"
|
||||
chmod 644 "$USER_HOME/.zshrc"
|
||||
|
||||
# --- Set shell (do this last to avoid breaking script if user shell changes mid-run)
|
||||
chsh -s "$(command -v zsh)" "$USERNAME"
|
||||
|
||||
# --- Deny admin from using Ansible
|
||||
cat <<EOF > /etc/sudoers.d/99_no_ansible_for_admin
|
||||
$USERNAME ALL=(ALL) !/usr/bin/ansible, !/usr/bin/ansible-playbook
|
||||
EOF
|
||||
|
||||
chmod 440 /etc/sudoers.d/99_no_ansible_for_admin
|
||||
|
||||
echo "✅ User $USERNAME configured successfully"
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
USERNAME="$1"
|
||||
SSH_KEY="$2"
|
||||
|
||||
echo "Creating ansible user: $USERNAME"
|
||||
|
||||
# --- Create user if not exists
|
||||
if ! id "$USERNAME" &>/dev/null; then
|
||||
useradd -m "$USERNAME"
|
||||
mkdir -p "/home/$USERNAME/.ssh"
|
||||
echo "$SSH_KEY" > "/home/$USERNAME/.ssh/authorized_keys"
|
||||
chmod 700 "/home/$USERNAME/.ssh"
|
||||
chmod 600 "/home/$USERNAME/.ssh/authorized_keys"
|
||||
chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh"
|
||||
else
|
||||
echo "User $USERNAME already exists, skipping creation"
|
||||
fi
|
||||
|
||||
# --- Harden the ansible user
|
||||
|
||||
# Disable password (no password login)
|
||||
passwd -d "$USERNAME"
|
||||
passwd -l "$USERNAME"
|
||||
|
||||
# Allow full passwordless sudo (needed for playbook tasks)
|
||||
cat <<EOF > "/etc/sudoers.d/99_${USERNAME}"
|
||||
${USERNAME} ALL=(ALL) NOPASSWD: ALL
|
||||
EOF
|
||||
chmod 0440 "/etc/sudoers.d/99_${USERNAME}"
|
||||
|
||||
echo "✅ User $USERNAME hardened for automation-only access"
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VAULT_FILE=".vault_pass.txt"
|
||||
VARS_FILE="terraform.pkrvar.hcl"
|
||||
PACKER_TEMPLATE="packer.pkr.hcl"
|
||||
|
||||
# Check if vault password file exists
|
||||
if [[ ! -f "$VAULT_FILE" ]]; then
|
||||
echo "❌ Vault password file '$VAULT_FILE' not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Decrypt the vars file temporarily
|
||||
echo "🔓 Decrypting $VARS_FILE..."
|
||||
ansible-vault decrypt "$VARS_FILE" --vault-password-file "$VAULT_FILE"
|
||||
|
||||
# Run packer build
|
||||
echo "🏗️ Running Packer build..."
|
||||
packer build -var-file="$VARS_FILE" "$PACKER_TEMPLATE"
|
||||
|
||||
# Re-encrypt the vars file
|
||||
echo "🔒 Re-encrypting $VARS_FILE..."
|
||||
ansible-vault encrypt "$VARS_FILE" --vault-password-file "$VAULT_FILE"
|
||||
|
||||
echo "✅ Done!"
|
||||
@@ -1,27 +0,0 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
37323332646365326338303234343138326439366364323631663634653136363935363865636334
|
||||
6534666264666666326235393861356362353932633630390a366334646166623237623233663763
|
||||
32323438646339353263366361323632646433643134663666616631316262363539373330633536
|
||||
3966623765323965650a613664306466353931643234333837376237303131643932306563346532
|
||||
65363634643964613936376661386639303431623534373464636638333238626130316664663666
|
||||
33356661313632366531646564366465366530663366326135326133313662323939393566653333
|
||||
62393839316330303164646131393162373138613764303864616562666130663035323864383031
|
||||
31303536346332393164306162343062616436303138643138653734633630666462633565326234
|
||||
66636562663165333863373563373535666533396334363865303237373331633761633838653961
|
||||
38383634326530333531656438346238336133613636356238653264303137333839313233656339
|
||||
38323730626234643963636662326663353737346332366263373139353962663239663363623736
|
||||
38656434386261316136323130646235666139643131313132396536393234373263323735336631
|
||||
37623537613164346663363138343238386362646533343837363165353539636539323434393839
|
||||
32323633636534623738356338336561653033633939366537653662643430396364383033343663
|
||||
37306666353432643636353464333362343162306435363236623866366639663339363339353736
|
||||
64663332386662353864303634653539323831396136383565363462316638613530623236623038
|
||||
63613262393035303730376164656239646336343133363931613136393362383734363235336637
|
||||
34613830633136636130303737636339306332393062656538616133393638613564326534336233
|
||||
35623764366232366538363464383334386539333963373066373065323466313033663735626466
|
||||
30366430323565333365343036343566663133316236663431393335353930323731633934346636
|
||||
38653039653930356633666336333365663833316635353866313965653632363365303165393538
|
||||
30666431663838633434363637656365613133323834343630383133396435333061373832633361
|
||||
30393730653963303933303934626562373663623265356562376633366238323563623462343162
|
||||
32626164663630393831316136303835613362663534383938353965366538653838343539663263
|
||||
65663833343834356137366263623331373236323065376536633237613030346135663563333763
|
||||
6539613565366337613162366434373430643634393665326666
|
||||
@@ -1,15 +0,0 @@
|
||||
variable "admin_username" {
|
||||
default = "admin"
|
||||
}
|
||||
|
||||
variable "admin_password_hash" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "admin_ssh_key" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ansible_username" {}
|
||||
variable "ansible_ssh_key" {}
|
||||
@@ -1,176 +0,0 @@
|
||||
# <span style="color:#4DA3FF;">Infrastructure as Code</span>
|
||||
|
||||
This repository contains the full Infrastructure-as-Code stack that builds, configures, and manages my homelab environment. It brings together **Packer**, **Terraform**, and **Ansible** into a single, structured workflow that provisions VM templates, deploys infrastructure on Proxmox, and configures each service automatically.
|
||||
|
||||
[](https://gitlab.com/)
|
||||
[](https://www.terraform.io/)
|
||||
[](https://www.packer.io/)
|
||||
[](https://www.ansible.com/)
|
||||
|
||||
The goal is to maintain everything as code: reproducible, version-controlled, and modular.
|
||||
|
||||
---
|
||||
|
||||
## <span style="color:#E0A800;">🧱 Repository Structure</span>
|
||||
|
||||
```
|
||||
Infrastrucutre-as-Code/
|
||||
├── Packer
|
||||
├── Terraform
|
||||
├── Ansible
|
||||
├── SSO
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
##  **Packer**
|
||||
Packer
|
||||
├── rocky10-proxmox-template
|
||||
│ ├── http
|
||||
│ │ └── ks.cfg
|
||||
│ ├── scripts
|
||||
│ │ ├── setup-admin-user.sh
|
||||
│ │ └── setup-ansible-user.sh
|
||||
│ ├── packer.pkr.hcl
|
||||
│ ├── secure-packer-build.sh
|
||||
│ ├── terraform.pkrvar.hcl
|
||||
│ └── variables.pkr.hcl
|
||||
└── README.md
|
||||
|
||||
Packer is used to create the base OS templates that all my virtual machines are built from.
|
||||
This includes:
|
||||
|
||||
- Automated OS installation (using a minimal Rocky10 base image)
|
||||
- Hardening steps during image creation
|
||||
- Preconfigured admin and automation users and passwordless SSH access for them
|
||||
- Preparation for provisioning tools such as Ansible
|
||||
|
||||
Each Packer build lives in its own directory containing:
|
||||
|
||||
- `packer.pkr.hcl` build files
|
||||
- Kickstart / cloud-init automation
|
||||
- Hardening & configuration scripts
|
||||
|
||||
These ISO's are uploaded to Proxmox as templates to be used by Terraform and form the baseline for the rest of the IaC pipeline.
|
||||
|
||||
---
|
||||
|
||||
##  **Terraform**
|
||||
Terraform
|
||||
├── host_tags.yml.tpl
|
||||
├── main.tf
|
||||
├── providers.tf
|
||||
├── README.md
|
||||
├── terraform.tfvars
|
||||
└── variables.tf
|
||||
|
||||
Terraform handles the infrastructure provisioning layer.
|
||||
It manages all Proxmox-related resources, including:
|
||||
|
||||
- VM creation from Packer templates
|
||||
- Network, firewall, and storage definitions
|
||||
- Infrastructure for identity and trust components
|
||||
- Modular, reusable Terraform modules
|
||||
|
||||
Terraform defines **what the homelab *is***.
|
||||
|
||||
---
|
||||
|
||||
##  **Ansible**
|
||||
Ansible
|
||||
├── group_vars
|
||||
│ ├── all
|
||||
│ │ └── vault_token.yml
|
||||
│ └── all.yml
|
||||
├── host_vars
|
||||
│ ├── docker-host
|
||||
│ │ └── host_tags.yml
|
||||
│ └── haproxy-vm
|
||||
│ └── host_tags.yml
|
||||
├── playbooks
|
||||
│ ├── compliance.yml
|
||||
│ ├── initialization.yml
|
||||
│ └── provisioning.yml
|
||||
├── roles
|
||||
│ ├── bootstrap_roles
|
||||
│ │ ├── base_users
|
||||
│ │ ├── disk_resize
|
||||
│ │ ├── docker_gitlab_setup
|
||||
│ │ ├── endlessh
|
||||
│ │ ├── firewall_ssh
|
||||
│ │ ├── hostname_network
|
||||
│ │ ├── packages
|
||||
│ │ ├── portainer_agent
|
||||
│ │ ├── root_password
|
||||
│ │ └── zsh_custom
|
||||
│ ├── cis_hardening
|
||||
│ │ ├── banners
|
||||
│ │ ├── defaults
|
||||
│ │ ├── file_permissions
|
||||
│ │ ├── filesystem_hardening
|
||||
│ │ ├── handlers
|
||||
│ │ ├── kernel_hardening
|
||||
│ │ ├── password_policy
|
||||
│ │ ├── service_disable
|
||||
│ │ ├── ssh_hardening
|
||||
│ │ └── tasks
|
||||
│ └── secure_vault
|
||||
│ ├── haproxy
|
||||
│ ├── technitium
|
||||
│ └── vault_agent
|
||||
├── ansible.cfg
|
||||
├── inventory_initialization.yml
|
||||
├── inventory.yml
|
||||
├── README.md
|
||||
├── requirements.yml
|
||||
└── roles_metadata.yml
|
||||
|
||||
Ansible configures the systems after Terraform brings them online.
|
||||
|
||||
Roles and playbooks in this layer include:
|
||||
|
||||
- Service installation and configuration
|
||||
- Certificate lifecycle management using Hashicrp Vault
|
||||
- mTLS enforcement for internal services
|
||||
- Service deployment and automation
|
||||
- Modular, reusable roles
|
||||
|
||||
Ansible defines **how the homelab *behaves***.
|
||||
|
||||
---
|
||||
|
||||
##  **SSO**
|
||||
|
||||
This directory contains everything related to the Single Sign-On stack.
|
||||
|
||||
This includes:
|
||||
|
||||
- Identity provider access to all locally routed applications and services behind HAProxy
|
||||
- Integrations with Vault, HAProxy and mTLS layers for true SSO once mTLS trust is verified
|
||||
|
||||
The goal is to centralize and standardize authentication across the environment.
|
||||
|
||||
---
|
||||
|
||||
## <span style="color:#F2921D;">🔧 Workflow Overview</span>
|
||||
|
||||
The full workflow follows a simple, layered model:
|
||||
|
||||
1. **Packer** → build secure, repeatable VM templates
|
||||
2. **Terraform** → instantiate infrastructure from those templates
|
||||
3. **Ansible** → configure, secure, and deploy services
|
||||
|
||||
Each layer owns a specific responsibility, keeping the stack clean, modular, and easy to reason about.
|
||||
|
||||
---
|
||||
|
||||
## <span style="color:#17A2B8;">📌 Notes</span>
|
||||
|
||||
- Everything here is designed to be reproducible.
|
||||
- No secrets live in this repository — Vault injects them during provisioning.
|
||||
- Each component stands alone while participating in the overall pipeline.
|
||||
|
||||
---
|
||||
|
||||
If you're exploring, a good path is **Packer → Terraform → Ansible** to see how everything connects.
|
||||
@@ -1,7 +0,0 @@
|
||||
Coming Soon...
|
||||
|
||||
This is the final piece of the IaC pipeline I need to construct. I have tried and tested Authelia, Authentik and Keycloak and none of them can do or fit in with my needs, which are automatically logging in once the mTLS layer provided via HAproxy is verified and access to the local domain granted, for the sign-in to be done.
|
||||
|
||||
I have done some research and it looks like a small python script can be made which will allow for a pre-auth proxy service that sits between HAProxy and the backend apps.
|
||||
|
||||
I have yet to get this fully working and in production. Once it is tested and working this readme and folder will be updated with all the relevant code and information for deployment.
|
||||
@@ -1,3 +0,0 @@
|
||||
Coming Soon...
|
||||
|
||||
A full explanation of the files in the Terraform folder and how they are laid out in order to consume the Packer built ISO, which has been uploaded into a Proxmox template, for the construction of a VM with specific parameters.
|
||||
@@ -1,20 +0,0 @@
|
||||
# VM metadata from Terraform provisioning
|
||||
vmid: ${vmid}
|
||||
vm_name: ${name}
|
||||
target_node: ${target_node}
|
||||
template: ${template}
|
||||
cores: ${cores}
|
||||
sockets: ${sockets}
|
||||
memory: ${memory}
|
||||
bridge: ${bridge}
|
||||
macaddr: ${macaddr}
|
||||
disk_size: ${disk_size}GB
|
||||
disk_storage: ${disk_storage}
|
||||
ip: ${ip}
|
||||
vlan: ${vlan}
|
||||
vm_tags:
|
||||
%{ for tag in tags ~}
|
||||
- ${tag}
|
||||
%{ endfor ~}
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
locals {
|
||||
ansible_repo_path = var.ansible_repo_path != "" ? var.ansible_repo_path : "${path.module}/../ansible"
|
||||
}
|
||||
|
||||
resource "proxmox_vm_qemu" "vm" {
|
||||
for_each = var.vms
|
||||
agent = 1
|
||||
skip_ipv6 = true
|
||||
vmid = each.value.vmid
|
||||
name = each.value.name
|
||||
target_node = each.value.target_node
|
||||
clone = each.value.template
|
||||
onboot = each.value.onboot
|
||||
os_type = "other"
|
||||
|
||||
cpu {
|
||||
type = "host"
|
||||
numa = true
|
||||
cores = each.value.cores
|
||||
sockets = each.value.sockets
|
||||
}
|
||||
|
||||
memory = each.value.memory
|
||||
balloon = 0
|
||||
scsihw = "virtio-scsi-pci"
|
||||
bootdisk = "scsi0"
|
||||
|
||||
disk {
|
||||
slot = "scsi0"
|
||||
type = "disk"
|
||||
storage = each.value.disk_storage
|
||||
size = each.value.disk_size
|
||||
format = "qcow2"
|
||||
discard = true
|
||||
}
|
||||
|
||||
network {
|
||||
id = 0
|
||||
bridge = each.value.bridge
|
||||
model = "virtio"
|
||||
macaddr = each.value.macaddr
|
||||
tag = each.value.vlan != 0 ? each.value.vlan : null
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
usb,
|
||||
]
|
||||
}
|
||||
|
||||
serial {
|
||||
id = 0
|
||||
}
|
||||
|
||||
tags = length(each.value.tags) > 0 ? join(",", each.value.tags) : null
|
||||
}
|
||||
|
||||
resource "null_resource" "wait_for_ssh" {
|
||||
for_each = var.vms
|
||||
depends_on = [proxmox_vm_qemu.vm]
|
||||
|
||||
connection {
|
||||
type = "ssh"
|
||||
host = each.value.ip
|
||||
user = "ansible"
|
||||
private_key = file("~/.ssh/auto")
|
||||
timeout = "5m"
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
command = <<EOT
|
||||
echo "Waiting for SSH on ${each.value.ip}..."
|
||||
for i in {1..30}; do
|
||||
if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i ~/.ssh/auto ansible@${each.value.ip} 'echo SSH Ready' >/dev/null 2>&1; then
|
||||
echo "✅ SSH is up!"
|
||||
exit 0
|
||||
fi
|
||||
echo "Retry $i: SSH not ready yet..."
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ SSH did not become available on ${each.value.ip} after timeout."
|
||||
exit 1
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
resource "local_file" "host_tags" {
|
||||
for_each = var.vms
|
||||
|
||||
content = templatefile("${path.module}/host_tags.yml.tpl", {
|
||||
vmid = each.value.vmid
|
||||
name = each.value.name
|
||||
target_node = each.value.target_node
|
||||
template = each.value.template
|
||||
cores = each.value.cores
|
||||
sockets = each.value.sockets
|
||||
memory = each.value.memory
|
||||
bridge = each.value.bridge
|
||||
macaddr = each.value.macaddr
|
||||
disk_size = each.value.disk_size
|
||||
disk_storage = each.value.disk_storage
|
||||
ip = each.value.ip
|
||||
vlan = each.value.vlan
|
||||
tags = each.value.tags
|
||||
})
|
||||
|
||||
filename = "${local.ansible_repo_path}/host_vars/${each.value.name}/host_tags.yml"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 0.14"
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "telmate/proxmox"
|
||||
version = "3.0.2-rc03"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "proxmox" {
|
||||
pm_tls_insecure = true
|
||||
pm_api_url = "https://host:port/api2/json"
|
||||
pm_api_token_secret = "pm_api_token_secret"
|
||||
pm_api_token_id = "pm_api_token_id"
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
vms = {
|
||||
haproxy = {
|
||||
vmid = id number here
|
||||
name = "vm name"
|
||||
target_node = "hypervisor node name"
|
||||
template = "template-name"
|
||||
onboot = true
|
||||
cores = 2
|
||||
sockets = 1
|
||||
memory = 2048
|
||||
bridge = "vmbr0"
|
||||
macaddr = "xx:x:x:x:x:x"
|
||||
disk_size = 20
|
||||
disk_storage = "storage name here"
|
||||
ip = "xx.xx.xx.x"
|
||||
gateway = "x.x.x.x"
|
||||
netmask = "24"
|
||||
vlan = "0"
|
||||
startup = {
|
||||
order = 3
|
||||
up = 10
|
||||
down = 10
|
||||
}
|
||||
tags = [
|
||||
"packages",
|
||||
"hostname_network",
|
||||
"root_password",
|
||||
"disk_resize",
|
||||
"base_users",
|
||||
"firewall_ssh",
|
||||
"endlessh",
|
||||
"portainer_agent",
|
||||
"zsh_custom",
|
||||
"cis_hardening"
|
||||
]
|
||||
}
|
||||
docker = {
|
||||
vmid = id number here
|
||||
name = "vm name"
|
||||
target_node = "hypervisor node name"
|
||||
template = "template-name"
|
||||
onboot = true
|
||||
cores = 2
|
||||
sockets = 1
|
||||
memory = 8192
|
||||
bridge = "vmbr0"
|
||||
macaddr = "xx:x:x:x:x:x"
|
||||
disk_size = 20
|
||||
disk_storage = "storage name here"
|
||||
ip = "xx.xx.xx.x"
|
||||
gateway = "x.x.x.x"
|
||||
netmask = "24"
|
||||
vlan = "0"
|
||||
startup = {
|
||||
order = 3
|
||||
up = 10
|
||||
down = 20
|
||||
}
|
||||
tags = [
|
||||
"packages",
|
||||
"hostname_network",
|
||||
"root_password",
|
||||
"disk_resize",
|
||||
"base_users",
|
||||
"firewall_ssh",
|
||||
"endlessh",
|
||||
"portainer_agent",
|
||||
"zsh_custom",
|
||||
"vault_agent",
|
||||
"cis_hardening",
|
||||
"docker_gitlab_setup"
|
||||
"s_nail",
|
||||
"lynis"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ansible_repo_path = ""
|
||||
@@ -1,32 +0,0 @@
|
||||
variable "vms" {
|
||||
description = "Map of VMs to create"
|
||||
type = map(object({
|
||||
vmid = number
|
||||
name = string
|
||||
target_node = string
|
||||
template = string
|
||||
onboot = bool
|
||||
cores = number
|
||||
sockets = number
|
||||
memory = number
|
||||
bridge = string
|
||||
macaddr = string
|
||||
disk_size = number
|
||||
disk_storage = string
|
||||
ip = string
|
||||
gateway = string
|
||||
netmask = string
|
||||
vlan = string
|
||||
startup = object({
|
||||
order = number
|
||||
up = number
|
||||
down = number
|
||||
})
|
||||
tags = list(string)
|
||||
}))
|
||||
}
|
||||
|
||||
variable "ansible_repo_path" {
|
||||
description = "Path to the ansible repo"
|
||||
type = string
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
# Truncated, sanitized example - real Caddyfile has one block per app.
|
||||
# Shows the pattern: mTLS + Authentik forward-auth in front of every
|
||||
# reverse-proxied service.
|
||||
|
||||
{
|
||||
servers {
|
||||
protocols h1 h2
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------
|
||||
# mTLS
|
||||
# -------------------------
|
||||
(local_mtls) {
|
||||
tls /etc/caddy/certs/example.com.crt /etc/caddy/certs/example.com.key {
|
||||
client_auth {
|
||||
mode require_and_verify
|
||||
|
||||
trust_pool file {
|
||||
pem_file /etc/caddy/certs/example.com.ca.crt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------
|
||||
# Authentik forward auth
|
||||
# -------------------------
|
||||
(authentik_forward_auth) {
|
||||
forward_auth http://authentik.internal:9054 {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
|
||||
trusted_proxies private_ranges
|
||||
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Host {host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
|
||||
copy_headers {
|
||||
X-Authentik-Username
|
||||
X-Authentik-Email
|
||||
X-Authentik-Groups
|
||||
X-Authentik-Uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -------------------------
|
||||
# Example app entries
|
||||
# -------------------------
|
||||
app1.example.com {
|
||||
import local_mtls
|
||||
import authentik_forward_auth
|
||||
reverse_proxy http://app1.internal:8080
|
||||
}
|
||||
|
||||
app2.example.com {
|
||||
import local_mtls
|
||||
reverse_proxy http://app2.internal:9000 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Host {host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
services:
|
||||
hawser:
|
||||
image: ghcr.io/finsys/hawser:latest
|
||||
container_name: hawser
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HTTP_PORT}:2376"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /path/to/hawser/hawser_stacks:/data/stacks
|
||||
environment:
|
||||
- TOKEN=${TOKEN}
|
||||
- TZ=${TZ}
|
||||
networks:
|
||||
- hawser-net
|
||||
|
||||
networks:
|
||||
hawser-net:
|
||||
external: true
|
||||
@@ -1,19 +0,0 @@
|
||||
services:
|
||||
hawser:
|
||||
image: ghcr.io/finsys/hawser:latest
|
||||
container_name: hawser
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HTTP_PORT}:2376"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /path/to/hawser/hawser_stacks:/data/stacks
|
||||
environment:
|
||||
- TOKEN=${TOKEN}
|
||||
- TZ=${TZ}
|
||||
networks:
|
||||
- hawser-net
|
||||
|
||||
networks:
|
||||
hawser-net:
|
||||
external: true
|
||||
@@ -1,32 +0,0 @@
|
||||
services:
|
||||
mariadb:
|
||||
image: lscr.io/linuxserver/mariadb:latest
|
||||
container_name: kodi-mariadb
|
||||
environment:
|
||||
- PUID=${PUID}
|
||||
- PGID=${PGID}
|
||||
- TZ=${TZ}
|
||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||||
- MYSQL_USER=${MYSQL_USER}
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- /path/to/kodi-mariadb:/config
|
||||
ports:
|
||||
- ${HTTP_PORT}:3306
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"mysqladmin ping -h 127.0.0.1 -u root -p${MYSQL_ROOT_PASSWORD} --silent",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- media-net
|
||||
|
||||
networks:
|
||||
media-net:
|
||||
external: true
|
||||
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user