From 0261e875679f1bf63c8d689da7fc7e014597885d Mon Sep 17 00:00:00 2001 From: Stonewall Jackson Date: Sat, 4 Feb 2023 01:23:43 -0500 Subject: initial commit --- roles/archive_server/defaults/main.yml | 4 + .../usr/local/libexec/archiver/archive_edgeswitch | 36 ++++++++ .../usr/local/libexec/archiver/archive_opnsense | 12 +++ roles/archive_server/tasks/freeipa.yml | 51 +++++++++++ roles/archive_server/tasks/main.yml | 79 +++++++++++++++++ .../archive_server/templates/etc/archiver.conf.j2 | 16 ++++ .../templates/usr/local/bin/archiver.sh.j2 | 99 ++++++++++++++++++++++ roles/archive_server/vars/main.yml | 22 +++++ 8 files changed, 319 insertions(+) create mode 100644 roles/archive_server/defaults/main.yml create mode 100644 roles/archive_server/files/usr/local/libexec/archiver/archive_edgeswitch create mode 100644 roles/archive_server/files/usr/local/libexec/archiver/archive_opnsense create mode 100644 roles/archive_server/tasks/freeipa.yml create mode 100644 roles/archive_server/tasks/main.yml create mode 100644 roles/archive_server/templates/etc/archiver.conf.j2 create mode 100644 roles/archive_server/templates/usr/local/bin/archiver.sh.j2 create mode 100644 roles/archive_server/vars/main.yml (limited to 'roles/archive_server') diff --git a/roles/archive_server/defaults/main.yml b/roles/archive_server/defaults/main.yml new file mode 100644 index 0000000..150a652 --- /dev/null +++ b/roles/archive_server/defaults/main.yml @@ -0,0 +1,4 @@ +archive_dest_path: /nfs/archive +archive_user: s-archiver +archive_on_calendar: '*-*-* 23:00:00' +archive_retention_days: 365 diff --git a/roles/archive_server/files/usr/local/libexec/archiver/archive_edgeswitch b/roles/archive_server/files/usr/local/libexec/archiver/archive_edgeswitch new file mode 100644 index 0000000..43979de --- /dev/null +++ b/roles/archive_server/files/usr/local/libexec/archiver/archive_edgeswitch @@ -0,0 +1,36 @@ +#!/bin/bash + +set -Eeu -o pipefail + +HOST=$1 +USERNAME=$2 +PASSWORD=$3 + +TIMESTAMP=$(date +%Y%m%d%H%M%S) +COOKIE_JAR=$(mktemp -t archiver-XXXXXX) + +trap 'rm -f "$COOKIE_JAR"' EXIT + +curl -sSfk \ + -o /dev/null \ + -c "$COOKIE_JAR" \ + -H "Referer: https://${HOST}/htdocs/login/login.lsp" \ + --data-urlencode "username=${USERNAME}" \ + --data-urlencode "password=${PASSWORD}" \ + -d 'accept_eula=0&require_eula=0' \ + "https://${HOST}/htdocs/login/login.lua" + +curl -sSfk \ + -o /dev/null \ + -c "$COOKIE_JAR" \ + -b "$COOKIE_JAR" \ + -H "Referer: https://${HOST}/htdocs/pages/base/file_upload_modal.lsp?filetypes=6&protocol=6" \ + --data-urlencode 'file_type_sel[]=config' \ + "https://${HOST}/htdocs/lua/ajax/file_upload_ajax.lua?protocol=6" + +curl -sSfk \ + -o "config-${TIMESTAMP}.scr" \ + -c "$COOKIE_JAR" \ + -b "$COOKIE_JAR" \ + -H "Referer: https://${HOST}/htdocs/pages/base/file_upload_modal.lsp?filetypes=6&protocol=6" \ + "https://${HOST}/htdocs/pages/base/http_download_file.lua?filepath=/mnt/download/TempConfigScript.scr" diff --git a/roles/archive_server/files/usr/local/libexec/archiver/archive_opnsense b/roles/archive_server/files/usr/local/libexec/archiver/archive_opnsense new file mode 100644 index 0000000..a51a068 --- /dev/null +++ b/roles/archive_server/files/usr/local/libexec/archiver/archive_opnsense @@ -0,0 +1,12 @@ +#!/bin/bash + +set -Eeu -o pipefail + +HOST=$1 +KEY=$2 +SECRET=$3 + +URL=https://${HOST}/api/backup/backup/download +TIMESTAMP=$(date +%Y%m%d%H%M%S) + +curl -sSfk -u "${KEY}:${SECRET}" -o "opnsense-${TIMESTAMP}.xml" "$URL" diff --git a/roles/archive_server/tasks/freeipa.yml b/roles/archive_server/tasks/freeipa.yml new file mode 100644 index 0000000..f0920f3 --- /dev/null +++ b/roles/archive_server/tasks/freeipa.yml @@ -0,0 +1,51 @@ +- name: create freeipa user + ipauser: + ipaadmin_principal: '{{ ipa_user }}' + ipaadmin_password: '{{ ipa_pass }}' + name: '{{ archive_user }}' + loginshell: /bin/bash + homedir: '{{ archive_home }}' + givenname: archive + sn: Service Account + state: present + run_once: True + +- name: create archive-clients hostgroup + ipahostgroup: + ipaadmin_principal: '{{ ipa_user }}' + ipaadmin_password: '{{ ipa_pass }}' + name: '{{ archive_clients_hbac_hostgroup }}' + description: Archive Clients + state: present + run_once: True + +- name: create HBAC rule for ssh + ipahbacrule: + ipaadmin_principal: '{{ ipa_user }}' + ipaadmin_password: '{{ ipa_pass }}' + name: archive_ssh_to_archive_clients + description: Allow archive user to ssh to archive clients + user: + - '{{ archive_user }}' + hostgroup: + - '{{ archive_clients_hbac_hostgroup }}' + hbacsvc: sshd + run_once: True + +- name: retrieve user keytab + include_role: + name: freeipa_keytab + vars: + keytab_principal: '{{ archive_user }}' + keytab_path: '{{ archive_keytab }}' + +- name: configure gssproxy for kerberized nfs + include_role: + name: gssproxy_client + vars: + gssproxy_name: archiver + gssproxy_section: service/archiver + gssproxy_keytab: /etc/krb5.keytab + gssproxy_client_keytab: '{{ archive_keytab }}' + gssproxy_cred_usage: initiate + gssproxy_euid: '{{ archive_user }}' diff --git a/roles/archive_server/tasks/main.yml b/roles/archive_server/tasks/main.yml new file mode 100644 index 0000000..d1bed55 --- /dev/null +++ b/roles/archive_server/tasks/main.yml @@ -0,0 +1,79 @@ +- import_tasks: freeipa.yml + +- name: install rsync + dnf: + name: rsync + state: present + +- name: create home directory + file: + path: '{{ archive_home }}' + owner: '{{ archive_user }}' + group: '{{ archive_user }}' + mode: 0700 + state: directory + +- name: create ssh directory + file: + path: '{{ archive_home }}/.ssh' + owner: '{{ archive_user }}' + group: '{{ archive_user }}' + mode: 0700 + state: directory + +- name: copy ssh privkey + copy: + content: '{{ archive_ssh_privkey }}' + dest: "{{ archive_home }}/.ssh/id_{{ archive_ssh_pubkey | regex_replace('^ssh-(\\w+).*', '\\1') }}" + owner: '{{ archive_user }}' + group: '{{ archive_user }}' + mode: 0600 + +- name: generate archiver script + template: + src: '{{ archive_script_path[1:] }}.j2' + dest: '{{ archive_script_path }}' + mode: 0555 + +- name: create plugin directory + file: + path: '{{ archive_plugin_dir }}' + state: directory + +- name: copy plugins + copy: + src: '{{ item.src }}' + dest: '{{ archive_plugin_dir }}/{{ item.path }}' + mode: 0555 + loop: "{{ lookup('filetree', archive_plugin_dir[1:], wantlist=True) }}" + when: item.state == 'file' + +- name: generate configuration + template: + src: '{{ archive_config_path[1:] }}.j2' + dest: '{{ archive_config_path }}' + owner: '{{ archive_user }}' + group: '{{ archive_user }}' + mode: 0440 + +- name: create SELinux policy to avoid logspam + include_role: + name: selinux_policy + apply: + tags: selinux + vars: + selinux_policy_name: ssh_gssproxy + selinux_policy_te: '{{ archive_selinux_policy_te }}' + tags: selinux + +- name: create systemd timer + include_role: + name: systemd_timer + vars: + timer_name: archiver + timer_description: Remote file archiver + timer_after: nss-user-lookup.target network-online.target gssproxy.service + timer_on_calendar: '{{ archive_on_calendar }}' + timer_user: '{{ archive_user }}' + timer_exec: '{{ archive_script_path }}' + timer_persistent: no diff --git a/roles/archive_server/templates/etc/archiver.conf.j2 b/roles/archive_server/templates/etc/archiver.conf.j2 new file mode 100644 index 0000000..d598a39 --- /dev/null +++ b/roles/archive_server/templates/etc/archiver.conf.j2 @@ -0,0 +1,16 @@ +# The format of this file is: +# +# HOST PLUGIN_NAME [ARGS...] +# +# Beware, each line is naively split on whitespace to tokenize the arguments. +# Quoting, escaping, shell characters, etc are NOT supported. + +# opnsense firewalls +{% for host in groups.opnsense_firewalls %} +{{ host }} archive_opnsense {{ hostvars[host].opnsense_backup_api_key }} {{ hostvars[host].opnsense_backup_api_secret }} +{% endfor %} + +# edgeswitches +{% for host in groups.switches %} +{{ host }} archive_edgeswitch {{ hostvars[host].edgeswitch_backup_username }} {{ hostvars[host].edgeswitch_backup_password }} +{% endfor %} diff --git a/roles/archive_server/templates/usr/local/bin/archiver.sh.j2 b/roles/archive_server/templates/usr/local/bin/archiver.sh.j2 new file mode 100644 index 0000000..582b776 --- /dev/null +++ b/roles/archive_server/templates/usr/local/bin/archiver.sh.j2 @@ -0,0 +1,99 @@ +#!/bin/bash + +set -Eeu -o pipefail + +shopt -s dotglob + +CLIENT_HOSTGROUP={{ archive_clients_hbac_hostgroup | quote}} +ARCHIVE_SRC={{ archive_source_path | quote }} +ARCHIVE_DEST={{ archive_dest_path | quote }} +ARCHIVE_PLUGIN_DIR={{ archive_plugin_dir | quote }} +ARCHIVE_CONFIG={{ archive_config_path }} +ARCHIVE_HOME={{ archive_home | quote }} +ARCHIVE_RETENTION_DAYS={{ archive_retention_days | quote }} +DOMAIN={{ ansible_domain }} +{% raw %} +export GSS_USE_PROXY=yes + +RSYNC_ARGS=( + --recursive + --ignore-existing + --links + --perms + --no-group + --chmod=D2770,F440 + --times + --omit-dir-times + --prune-empty-dirs + --remove-source-files + --human-readable + --itemize-changes +) + +FAILED_HOSTS=() + +trap 'rm -rf "$TMPDIR"' EXIT + +############ +# First, archive the /var/spool/archive directory for all hosts in the +# archive clients host group via ssh. +############ +readarray -t HOSTS < <(ipa hostgroup-show "$CLIENT_HOSTGROUP" --raw \ + | awk '$1 == "member:" { match($2, /^fqdn=([^,]+),/, m); print m[1] }') + +for HOST in "${HOSTS[@]}"; do + echo "archiving ${HOST}..." + TMPDIR=$(mktemp -d "${ARCHIVE_HOME}/.archiver-XXXXXX") + rsync "${RSYNC_ARGS[@]}" "${HOST}:${ARCHIVE_SRC}/" "$TMPDIR" && RC=$? || RC=$? + + if (( RC == 0 )); then + mkdir -p "${ARCHIVE_DEST}/${HOST}" + find "$TMPDIR" -mindepth 2 -maxdepth 2 -print0 | xargs -0 -I{} cp -rpn {} "${ARCHIVE_DEST}/${HOST}" + else + FAILED_HOSTS+=("$HOST") + fi + + rm -rf "$TMPDIR" +done + + +############ +# Next, we archive hosts that don't support pull via ssh. For each line in +# $ARCHIVE_CONFIG, we run the plugin command inside of a temporary directory and +# then rsync any created files to the archive directory. +############ +grep -v '^\s*$\|^\s*\#' "$ARCHIVE_CONFIG" | while read -r HOST CMD ARGS; do + echo "archiving ${HOST} via script..." + + TMPDIR=$(mktemp -d "${ARCHIVE_HOME}/.archiver-XXXXXX") + pushd "$TMPDIR" > /dev/null + "${ARCHIVE_PLUGIN_DIR}/${CMD}" "$HOST" ${ARGS:-} && RC=$? || RC=$? + popd > /dev/null + + if [[ $HOST = *.* ]]; then + FQDN=$HOST + else + FQDN="${HOST}.${DOMAIN}" + fi + + if (( RC == 0 )); then + mkdir -p "${ARCHIVE_DEST}/${FQDN}" + rsync "${RSYNC_ARGS[@]}" "${TMPDIR}/" "${ARCHIVE_DEST}/${FQDN}" + else + FAILED_HOSTS+=("$HOST") + fi + + rm -rf "$TMPDIR" +done + + +############ +# Prune old archive files. +############ +find "$ARCHIVE_DEST" -type f -mtime "+${ARCHIVE_RETENTION_DAYS}" -delete + +if (( ${#FAILED_HOSTS[@]} )); then + echo "the following hosts had errors: ${FAILED_HOSTS[*]}" 1>&2 + exit 1 +fi +{% endraw %} diff --git a/roles/archive_server/vars/main.yml b/roles/archive_server/vars/main.yml new file mode 100644 index 0000000..c59fbce --- /dev/null +++ b/roles/archive_server/vars/main.yml @@ -0,0 +1,22 @@ +archive_home: /var/spool/archive +archive_source_path: /var/spool/archive + +archive_keytab: /var/lib/gssproxy/clients/{{ archive_user }}.keytab +archive_clients_hbac_hostgroup: archive_clients +archive_script_path: /usr/local/bin/archiver.sh +archive_config_path: /etc/archiver.conf + +archive_plugin_dir: /usr/local/libexec/archiver + +archive_selinux_policy_te: | + require { + type gssd_t; + type ssh_exec_t; + type gssproxy_t; + class file getattr; + class key read; + } + + #============= gssproxy_t ============== + allow gssproxy_t gssd_t:key read; + allow gssproxy_t ssh_exec_t:file getattr; -- cgit