commit 6a683a7a3825f581650ebee18aaa835109641c30 Author: Ryan Hamilton Date: Mon Jul 21 14:26:07 2025 -0500 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e910198 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Secrets and sensitive inventory +inventory/*.ini +inventory/host_vars/ +inventory/group_vars/*_vault.yml +.vault_pass.txt + +# SSH keys +*.pem +id_rsa* +id_ed25519* + +# Local config +.ansible.cfg \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..21341d3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# semaphore + +Ansible GUI for infrastructure management \ No newline at end of file diff --git a/playbooks/add-gitea-ssh-key.yml b/playbooks/add-gitea-ssh-key.yml new file mode 100644 index 0000000..beb3f1e --- /dev/null +++ b/playbooks/add-gitea-ssh-key.yml @@ -0,0 +1,20 @@ +--- +- name: Add SSH key from Gitea + hosts: all + become: true + + tasks: + - name: Ensure .ssh directory exists + file: + path: "/home/{{ username }}/.ssh" + state: directory + owner: "{{ username }}" + group: "{{ username }}" + mode: '0700' + + - name: Add public key from Gitea to authorized_keys + ansible.posix.authorized_key: + user: "{{ username }}" + key: "https://gitea.purpleraft.com/{{ username }}.keys" + state: present + manage_dir: false # we already ensured it diff --git a/playbooks/add-usertogroup.yml b/playbooks/add-usertogroup.yml new file mode 100644 index 0000000..33d901c --- /dev/null +++ b/playbooks/add-usertogroup.yml @@ -0,0 +1,29 @@ +--- +- name: Ensure user is in specified group + hosts: all + become: true + gather_facts: false + + vars: + check_user: "{{ check_user }}" + check_group: "{{ check_group }}" + + tasks: + - name: Ensure group exists + group: + name: "{{ check_group }}" + state: present + + - name: Check if user exists + getent: + database: passwd + key: "{{ check_user }}" + register: user_check + changed_when: false + failed_when: user_check.ansible_facts.getent_passwd[check_user] is not defined + + - name: Add user to group (non-destructively) + user: + name: "{{ check_user }}" + groups: "{{ check_group }}" + append: true diff --git a/playbooks/check-reboot-needed.yml b/playbooks/check-reboot-needed.yml new file mode 100644 index 0000000..0f93773 --- /dev/null +++ b/playbooks/check-reboot-needed.yml @@ -0,0 +1,24 @@ +- name: Check if reboot is required + hosts: all + gather_facts: false + become: true + vars: + reboot_machines: false # default, override via --extra-vars + + tasks: + - name: Check for /var/run/reboot-required + stat: + path: /var/run/reboot-required + register: reboot_flag + + - name: Display reboot status + debug: + msg: "{{ inventory_hostname }} {{ 'REQUIRES' if reboot_flag.stat.exists else 'does NOT require' }} a reboot." + + - name: Reboot if required and reboot_machines flag is true + reboot: + msg: "Rebooting due to /var/run/reboot-required" + pre_reboot_delay: 5 + when: + - reboot_flag.stat.exists + - reboot_machines | bool diff --git a/playbooks/check-time-sync.yml b/playbooks/check-time-sync.yml new file mode 100644 index 0000000..d8e9208 --- /dev/null +++ b/playbooks/check-time-sync.yml @@ -0,0 +1,32 @@ +- name: Quick check of time offset using ntpdate + hosts: all + become: true + gather_facts: false + vars: + ntp_check_target: "pool.ntp.org" + + tasks: + + - name: Ensure ntpdate is installed + apt: + name: ntpdate + state: present + update_cache: true + + - name: Query time offset from {{ ntp_check_target }} + command: "ntpdate -q {{ ntp_check_target }}" + register: ntp_offset + changed_when: false + failed_when: ntp_offset.rc != 0 + + - name: Try to extract final offset line + set_fact: + ntp_offset_summary: "{{ ntp_offset.stdout_lines | select('search', 'adjust time') | list | first | default('No offset line found') }}" + + - name: Show full ntpdate output per host + debug: + msg: | + [{{ inventory_hostname }}] + Offset summary: {{ ntp_offset_summary }} + Raw output: + {{ ntp_offset.stdout_lines | join('\n') }} diff --git a/playbooks/check-useringroup.yml b/playbooks/check-useringroup.yml new file mode 100644 index 0000000..5fc89c6 --- /dev/null +++ b/playbooks/check-useringroup.yml @@ -0,0 +1,31 @@ +--- +- name: Check if user is in specified group + hosts: all + gather_facts: false + become: true + + vars_prompt: + - name: check_user + prompt: "Enter the username to check" + private: no + + - name: check_group + prompt: "Enter the group to verify membership" + private: no + + tasks: + - name: Get groups for specified user + ansible.builtin.command: "id -nG {{ check_user }}" + register: user_groups + changed_when: false + failed_when: user_groups.rc != 0 + + - name: Set fact if user is in group + set_fact: + user_in_group: "{{ check_group in user_groups.stdout.split() }}" + + - name: Report user group membership + debug: + msg: > + User '{{ check_user }}' {{ 'IS' if user_in_group else 'IS NOT' }} + in the '{{ check_group }}' group on {{ inventory_hostname }}. diff --git a/playbooks/check_unallocated_disk.yml b/playbooks/check_unallocated_disk.yml new file mode 100644 index 0000000..d28826c --- /dev/null +++ b/playbooks/check_unallocated_disk.yml @@ -0,0 +1,46 @@ +--- +- name: Check unallocated disk space + hosts: all + become: true + gather_facts: false + + tasks: + + - name: Get block device sizes + command: lsblk -b -o NAME,SIZE,TYPE -dn + register: lsblk_output + + - name: Parse lsblk output into structured facts + set_fact: + disks: "{{ disks | default({}) | combine({ item.split()[0]: item.split()[1]|int }) }}" + loop: "{{ lsblk_output.stdout_lines }}" + when: "'disk' in item" + + - name: Get partition sizes for each disk + command: lsblk -b -o NAME,SIZE,TYPE -n + register: partition_output + + - name: Initialize disk usage map + set_fact: + used_space: "{{ used_space | default({}) }}" + + - name: Sum partition sizes under each disk + set_fact: + used_space: >- + {{ + used_space | combine({ + (item.0): (used_space[item.0]|default(0)) + (item.1.split()[1]|int) + }) + }} + with_nested: + - "{{ disks.keys() | list }}" + - "{{ partition_output.stdout_lines }}" + when: item.1.startswith(item.0) and 'part' in item.1 + + - name: Show disk usage summary + debug: + msg: >- + Disk /dev/{{ item.key }}: Total {{ item.value|int // 1024**3 }} GB, + Used {{ used_space[item.key]|default(0)|int // 1024**3 }} GB, + Unallocated {{ (item.value|int - used_space[item.key]|default(0)|int) // 1024**3 }} GB + loop: "{{ disks|dict2items }}" diff --git a/playbooks/configure-chrony.yml b/playbooks/configure-chrony.yml new file mode 100644 index 0000000..3f3686b --- /dev/null +++ b/playbooks/configure-chrony.yml @@ -0,0 +1,51 @@ +- name: Configure Chrony NTP Client + hosts: all + become: true + tasks: + - name: Ensure chrony is installed + apt: + name: chrony + state: present + update_cache: true + + - name: Disable systemd-timesyncd if present + systemd: + name: systemd-timesyncd + enabled: false + state: stopped + ignore_errors: true + + - name: Ensure sources.d directory exists + file: + path: /etc/chrony/sources.d + state: directory + mode: "0755" + + - name: Deploy atomic source + copy: + src: templates/ntp/atomic.sources + dest: /etc/chrony/sources.d/atomic.sources + owner: root + group: root + mode: "0644" + + - name: Deploy navy source + copy: + src: templates/ntp/navy.sources + dest: /etc/chrony/sources.d/navy.sources + owner: root + group: root + mode: "0644" + + - name: Deploy fallback pool + copy: + src: templates/ntp/fallback.sources + dest: /etc/chrony/sources.d/fallback.sources + owner: root + group: root + mode: "0644" + + - name: Reload NTP sources + command: chronyc reload sources + register: reload_output + changed_when: "'Sources reloaded' in reload_output.stdout" diff --git a/playbooks/create-user.yml b/playbooks/create-user.yml new file mode 100644 index 0000000..95d0762 --- /dev/null +++ b/playbooks/create-user.yml @@ -0,0 +1,35 @@ +--- +- name: Create a user with SSH access and optional groups + hosts: all + become: true + gather_facts: false + + vars: + username: "{{ username }}" + authorized_key: "{{ authorized_key }}" + extra_groups: "{{ extra_groups | default('') }}" + extra_groups_list: "{{ extra_groups.split(',') | map('trim') | list if extra_groups else [] }}" + default_shell: "{{ default_shell | default('/bin/bash') }}" + + tasks: + - name: Ensure each extra group exists + ansible.builtin.group: + name: "{{ item }}" + state: present + loop: "{{ extra_groups_list }}" + when: extra_groups_list | length > 0 + + - name: Ensure user account exists + ansible.builtin.user: + name: "{{ username }}" + shell: "{{ default_shell }}" + groups: "{{ extra_groups_list }}" + append: true + create_home: true + state: present + + - name: Set authorized SSH key + ansible.builtin.authorized_key: + user: "{{ username }}" + key: "{{ authorized_key }}" + state: present diff --git a/playbooks/deploy_sshd_config.yml b/playbooks/deploy_sshd_config.yml new file mode 100644 index 0000000..9378457 --- /dev/null +++ b/playbooks/deploy_sshd_config.yml @@ -0,0 +1,43 @@ +--- +- name: Deploy complete SSH server configuration + hosts: all + become: true + gather_facts: false + + tasks: + - name: Deploy base /etc/ssh/sshd_config file + template: + src: templates/sshd/sshd_config.j2 + dest: /etc/ssh/sshd_config + owner: root + group: root + mode: "0644" + notify: Reload SSH + + - name: Deploy hardened global ssh config include + template: + src: templates/sshd/00-global.conf.j2 + dest: /etc/ssh/sshd_config.d/00-global.conf + owner: root + group: root + mode: "0644" + notify: Reload SSH + + - name: Deploy LAN password bypass config include + template: + src: templates/sshd/99-lan-bypass.conf.j2 + dest: /etc/ssh/sshd_config.d/99-lan-bypass.conf + owner: root + group: root + mode: "0644" + notify: Reload SSH + + - name: Validate sshd configuration syntax + command: sshd -t + changed_when: false + + handlers: + - name: Reload SSH + service: + name: ssh + state: reloaded diff --git a/playbooks/enable_user_password_reset.yml b/playbooks/enable_user_password_reset.yml new file mode 100644 index 0000000..ff0cf98 --- /dev/null +++ b/playbooks/enable_user_password_reset.yml @@ -0,0 +1,18 @@ +--- +- name: Allow user to set their own password + hosts: all + become: true + gather_facts: false + + vars: + username: "{{ username }}" + + tasks: + - name: Unlock password so user can run passwd + ansible.builtin.command: "passwd -d {{ username }}" + when: not ansible_check_mode + + - name: Ensure user is not locked + ansible.builtin.user: + name: "{{ username }}" + password_lock: false diff --git a/playbooks/install-essential-tools.yml b/playbooks/install-essential-tools.yml new file mode 100644 index 0000000..2c56d9f --- /dev/null +++ b/playbooks/install-essential-tools.yml @@ -0,0 +1,22 @@ +--- +- name: Ensure essential tools are installed on all hosts + hosts: all + become: true + + vars: + essential_packages: + - curl + - git + - jq + - htop + - unzip + - ca-certificates + - net-tools + - acl + + tasks: + - name: Install common tools + apt: + name: "{{ essential_packages }}" + state: present + update_cache: yes diff --git a/playbooks/install-standard-docker.yml b/playbooks/install-standard-docker.yml new file mode 100644 index 0000000..a0cd09b --- /dev/null +++ b/playbooks/install-standard-docker.yml @@ -0,0 +1,146 @@ +--- +- name: Install Docker using official Docker documentation steps and set up /opt/docker and /srv/docker + hosts: docker + become: true + gather_facts: true + + vars: + docker_keyring_path: /etc/apt/keyrings/docker.asc + docker_repo_list_path: /etc/apt/sources.list.d/docker.list + docker_acl_path: /opt/docker + srv_docker_path: /srv/docker + docker_data_user: dockeruser + docker_data_group: dockerdata + docker_data_uid: 2011 + docker_data_gid: 2011 + + tasks: + # --- Prereqs --- + - name: Ensure required packages are installed + apt: + name: + - ca-certificates + - curl + - acl # Required for setfacl + state: present + update_cache: yes + + - name: Ensure keyring directory exists + file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + + - name: Download Docker's official GPG key + get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: "{{ docker_keyring_path }}" + mode: "0644" + register: docker_key_download + + - name: Get native architecture (dpkg --print-architecture) + command: dpkg --print-architecture + register: dpkg_arch_result + changed_when: false + + - name: Add Docker repository to Apt sources + copy: + dest: "{{ docker_repo_list_path }}" + content: | + deb [arch={{ dpkg_arch_result.stdout }} signed-by={{ docker_keyring_path }}] https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable + notify: Update apt cache + + - name: Flush handlers to update apt cache before install + meta: flush_handlers + + # --- Docker Install --- + - name: Install Docker packages + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + update_cache: no + + - name: Ensure docker group exists + group: + name: docker + state: present + + - name: Ensure Docker service is enabled and running + systemd: + name: docker + enabled: true + state: started + + # --- ACL & Folder Standardization --- + - name: Ensure Docker base folder exists with correct ownership + file: + path: "{{ docker_acl_path }}" + state: directory + owner: root + group: docker + mode: "0775" + + - name: Set setgid bit on /opt/docker so group is inherited + file: + path: "{{ docker_acl_path }}" + mode: "2775" + + - name: Check for existing default ACL on Docker folder + command: getfacl --access --default {{ docker_acl_path }} + register: facl_check + changed_when: false + failed_when: false + + - name: Set default ACL for docker group if not already present + command: setfacl -d -m g:docker:rwx {{ docker_acl_path }} + when: "'group:docker:rwx' not in facl_check.stdout" + + # --- New: Dedicated Docker Data User/Group and /srv/docker Setup --- + - name: Create docker data group with fixed GID + group: + name: "{{ docker_data_group }}" + gid: "{{ docker_data_gid }}" + state: present + system: yes + + - name: Create docker data user with fixed UID and GID + user: + name: "{{ docker_data_user }}" + uid: "{{ docker_data_uid }}" + group: "{{ docker_data_group }}" + shell: /usr/sbin/nologin + create_home: no + system: yes + state: present + + - name: Ensure /srv/docker exists with correct ownership + file: + path: "{{ srv_docker_path }}" + state: directory + owner: "{{ docker_data_user }}" + group: "{{ docker_data_group }}" + mode: "0770" + + - name: Set setgid bit on /srv/docker so group is inherited + file: + path: "{{ srv_docker_path }}" + mode: "2770" + + - name: Set default ACL for dockerdata group on /srv/docker + ansible.posix.acl: + path: "{{ srv_docker_path }}" + entity: "{{ docker_data_group }}" + etype: group + permissions: rwx + default: yes + state: present + + handlers: + - name: Update apt cache + apt: + update_cache: yes diff --git a/playbooks/ntpdig-check.yml b/playbooks/ntpdig-check.yml new file mode 100644 index 0000000..3e95b35 --- /dev/null +++ b/playbooks/ntpdig-check.yml @@ -0,0 +1,39 @@ +- name: Check time synchronization using ntpdig (modern method) + hosts: all + become: true + gather_facts: false + vars: + ntp_check_target: "pool.ntp.org" + + tasks: + + - name: Remove legacy ntpdate package (if present) + apt: + name: ntpdate + state: absent + + - name: Ensure ntpsec-ntpdate is installed (provides ntpdig) + apt: + name: ntpsec-ntpdate + state: present + update_cache: true + + - name: Query time offset using ntpdig from {{ ntp_check_target }} + command: "ntpdig {{ ntp_check_target }}" + register: ntpdig_output + changed_when: false + failed_when: ntpdig_output.rc != 0 + + - name: Parse correct offset and error from ntpdig output + set_fact: + ntpdig_offset: "{{ ntpdig_output.stdout | regex_search('[+-][0-9]+\\.[0-9]+(?=\\s+\\+/-)', '\\0') | default('N/A') }}" + ntpdig_error: "{{ ntpdig_output.stdout | regex_search('\\+/-\\s+([0-9]+\\.[0-9]+)', '\\1') | default('N/A') }}" + + - name: Show parsed ntpdig result + debug: + msg: | + [{{ inventory_hostname }}] + Offset: {{ ntpdig_offset }} sec + Estimated error: ±{{ ntpdig_error }} sec + Raw output: + {{ ntpdig_output.stdout_lines | join('\n') }} diff --git a/playbooks/reboot.yml b/playbooks/reboot.yml new file mode 100644 index 0000000..562b2d6 --- /dev/null +++ b/playbooks/reboot.yml @@ -0,0 +1,9 @@ +--- +- name: Reboot system immediately + hosts: all + become: true + tasks: + - name: Reboot the machine + ansible.builtin.reboot: + msg: "Rebooting" + reboot_timeout: 600 diff --git a/playbooks/requirements.yml b/playbooks/requirements.yml new file mode 100644 index 0000000..0d35806 --- /dev/null +++ b/playbooks/requirements.yml @@ -0,0 +1,2 @@ +# Empty role list +roles: [] diff --git a/playbooks/templates/ntp/atomic.sources b/playbooks/templates/ntp/atomic.sources new file mode 100644 index 0000000..d330dfe --- /dev/null +++ b/playbooks/templates/ntp/atomic.sources @@ -0,0 +1 @@ +server atomicmidnight.internal.purpleraft.com iburst prefer diff --git a/playbooks/templates/ntp/fallback.sources b/playbooks/templates/ntp/fallback.sources new file mode 100644 index 0000000..0368180 --- /dev/null +++ b/playbooks/templates/ntp/fallback.sources @@ -0,0 +1,3 @@ +pool 0.pool.ntp.org iburst +pool 1.pool.ntp.org iburst +pool 2.pool.ntp.org iburst diff --git a/playbooks/templates/ntp/navy.sources b/playbooks/templates/ntp/navy.sources new file mode 100644 index 0000000..db3de96 --- /dev/null +++ b/playbooks/templates/ntp/navy.sources @@ -0,0 +1,2 @@ +server tick.usno.navy.mil iburst prefer +server tock.usno.navy.mil iburst diff --git a/playbooks/templates/sshd/00-global.conf.j2 b/playbooks/templates/sshd/00-global.conf.j2 new file mode 100644 index 0000000..0d25644 --- /dev/null +++ b/playbooks/templates/sshd/00-global.conf.j2 @@ -0,0 +1,25 @@ +Port 22 +AddressFamily inet +PermitRootLogin no + +PasswordAuthentication no +KbdInteractiveAuthentication no +ChallengeResponseAuthentication no +PermitEmptyPasswords no + +UsePAM yes +AllowGroups {{ ssh_access_group | default('sshusers') }} + +PubkeyAuthentication yes +AuthorizedKeysFile .ssh/authorized_keys + +X11Forwarding no +PrintMotd no +PrintLastLog yes + +LoginGraceTime 30s +MaxAuthTries 3 +MaxSessions 2 + +AcceptEnv LANG LC_* +Subsystem sftp /usr/lib/openssh/sftp-server diff --git a/playbooks/templates/sshd/99-lan-bypass.conf.j2 b/playbooks/templates/sshd/99-lan-bypass.conf.j2 new file mode 100644 index 0000000..bcea2d8 --- /dev/null +++ b/playbooks/templates/sshd/99-lan-bypass.conf.j2 @@ -0,0 +1,11 @@ +Match Address 10.0.0.0/8 + PasswordAuthentication yes + +Match Address 192.168.0.0/16 + PasswordAuthentication yes + +Match Address 206.202.209.9/32 + PasswordAuthentication yes + +Match Address 100.64.0.0/10 + PasswordAuthentication yes \ No newline at end of file diff --git a/playbooks/templates/sshd/sshd_config.j2 b/playbooks/templates/sshd/sshd_config.j2 new file mode 100644 index 0000000..5fefd2d --- /dev/null +++ b/playbooks/templates/sshd/sshd_config.j2 @@ -0,0 +1,4 @@ +# Base sshd_config — managed by Ansible +# Delegates all settings to config fragments + +Include /etc/ssh/sshd_config.d/*.conf diff --git a/playbooks/test.yml b/playbooks/test.yml new file mode 100644 index 0000000..bbc855e --- /dev/null +++ b/playbooks/test.yml @@ -0,0 +1,13 @@ +- name: Semaphore connection test + hosts: all + gather_facts: false + become: true + + tasks: + - name: Print the hostname + command: hostname + register: result + + - name: Show result + debug: + msg: "Connected to {{ inventory_hostname }} (hostname: {{ result.stdout }})" diff --git a/playbooks/update-upgrade.yml b/playbooks/update-upgrade.yml new file mode 100644 index 0000000..32b0b3d --- /dev/null +++ b/playbooks/update-upgrade.yml @@ -0,0 +1,13 @@ +- name: Update APT packages + hosts: all + become: true + tasks: + - name: Update package cache + apt: + update_cache: yes + + - name: Upgrade packages + apt: + upgrade: safe + autoremove: yes + autoclean: yes