Ansible in Jail mit Cisco Nexus und IOSXE Backup und SSH Authentication und Sicherung in Gitea

Möchte man mit Ansible seine Cisco Nexus Konfigurationsdaten sichern, sollte man den Gedanken Security nicht außer acht lassen. Welche Anforderungen haben wir also?

  • Ansible Nutzer darf auf dem Wirtssystem keine Root Rechte haben
  • Ansible Nutzer darf keine Login Shell haben, um Angriffe von Außen zu eliminieren
  • Ansible Nutzer muss eine keybasierte Authentifizierung auf unseren Nexus Switches haben
  • Ansible Nutzer darf auf unseren Switches nur bestimmte Show Commands ausführen
  • Ansible Nutzer muss nach erfolgter Sicherung die Konfiguration in ein GIT Repository schreiben

Auf unserem Linux System sind die folgenden Schritte notwendig. Wir gehen in unserem Beispiel davon aus das die Ansible Directory Struktur sich unterhalb von /opt/ansible befindet.

Anlegen der Keys für unsere Switches. Wir erzeugen für jeden Switch ein eigenes Public/Private Keypair.

mkdir -p /opt/ansible/keys
ssh-keygen -t ed25519 -f /opt/ansible/keys/SW001_ed25519 -C "ansible_backup@SW001"
ssh-keygen -t ed25519 -f /opt/ansible/keys/SW002_ed25519 -C "ansible_backup@SW002"
ssh-keygen -t ed25519 -f /opt/ansible/keys/SW003_ed25519 -C "ansible_backup@SW003"
ssh-keygen -t ed25519 -f /opt/ansible/keys/SW004_ed25519 -C "ansible_backup@SW004"

Cisco IOSXE hat sich ein wenig scheckig wenn es um die Keylänge geht, hier können wir nur mit 1024 Bit Länge arbeiten.

ssh-keygen -t rsa -b 1024 -f /opt/ansible/keys/SW007_ansible_rsa -C "ansible_backup@Ansible-Device"

Anlegen eines Backup User für unseren Ansible Backup Job

useradd -r -m -d /var/lib/ansible_backup -s /usr/sbin/nologin ansible_backup
chown ansible_backup:ansible_backup -R /opt/ansible/*
mkdir -p /opt/ansible/keys
chmod 700 /opt/ansible/keys

/opt/ansible/inventory.cfg

all:
  children:
    switches:
      hosts:
         SW001:
          ansible_connection: network_cli
          ansible_network_os: cisco.nxos.nxos
          ansible_host: 192.168.1.11
          ansible_user: ansible_backup
          ansible_ssh_private_key_file: /opt/ansible/keys/SW001_ansible_ed25519
         SW002:
          ansible_connection: network_cli
          ansible_network_os: cisco.nxos.nxos
          ansible_host: 192.168.1.12
          ansible_user: ansible_backup
          ansible_ssh_private_key_file: /opt/ansible/keys/SW002_ansible_ed25519
        SW003:
          ansible_connection: network_cli
          ansible_network_os: cisco.nxos.nxos
          ansible_host: 192.168.1.13
          ansible_user: ansible_backup
          ansible_ssh_private_key_file: /opt/ansible/keys/SW003_ansible_ed25519
        SW004:
          ansible_connection: network_cli
          ansible_network_os: cisco.nxos.nxos
          ansible_host: 192.168.1.14
          ansible_user: ansible_backup
          ansible_ssh_private_key_file: /opt/ansible/keys/SW004_ansible_ed25519

/opt/ansible/playbooks/netzwerk_backup.yml

---
- name: Backup Nexus Config
  hosts: switches
  gather_facts: no
  vars:
    ansible_network_os: cisco.nxos.nxos
    ansible_connection: network_cli

    backup_dir: /opt/backup-config
    backup_timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H-%M-%S') }}"
    backup_keep: 60

  tasks:
    - name: Backup running config
      cisco.nxos.nxos_config:
        backup: yes
        backup_options:
          filename: "{{ inventory_hostname }}.cfg"
          dir_path: "/opt/ansible/playbooks/files"
    - name: Remove dynamic NX-OS headers from config
      ansible.builtin.replace:
        path: "/opt/ansible/playbooks/files/{{ inventory_hostname }}.cfg"
        regexp: '^!(Time|Running configuration last done at|Last configuration change at|NVRAM config last updated at|Command|Device):.*$'
        replace: ''


- name: Check files into Gitea via HTTP + Token
  hosts: localhost
  vars:
    gitea_domain: "192.168.10.30:3000"
    repo_name: "konfigurationssicherung"
    repo_user: "config"
    gitea_token: "1234567890"
    repo_url: "http://{{ gitea_domain }}/{{ repo_user }}/{{ repo_name }}.git"
    repo_dest: "/opt/backup-config/{{ repo_name }}"
    git_branch: "master"
    commit_msg: "Automated commit via Ansible"
    files_to_add:
      - { src: "SW001.cfg", dest: "SW001.cfg" }
      - { src: "SW002.cfg", dest: "SW002.cfg" }
      - { src: "SW003.cfg", dest: "SW003.cfg" }
      - { src: "SW004.cfg", dest: "SW004.cfg" }

  tasks:
    - name: Ensure git is installed
      ansible.builtin.package:
        name: git
        state: present

    - name: Check if Git repo already exists
      ansible.builtin.stat:
        path: "{{ repo_dest }}/.git"
      register: git_repo_present

    - name: Clone repo if missing
      git:
        repo: "{{ repo_url }}"
        dest: "{{ repo_dest }}"
        version: "{{ git_branch }}"
        force: no
      when: not git_repo_present.stat.exists

    - name: Set git user.name
      command: git config user.name "pfconfig"
      args:
        chdir: "{{ repo_dest }}"

    - name: Set git user.email
      command: git config user.email "pfconfig@lkosl.local"
      args:
        chdir: "{{ repo_dest }}"

    - name: Copy backup files
      copy:
        src: "{{ item.src }}"
        dest: "{{ repo_dest }}/{{ item.dest }}"
      loop: "{{ files_to_add }}"

    - name: Git add files
      command: git add .
      args:
        chdir: "{{ repo_dest }}"

    - name: Commit changes (if any)
      command: git commit -m "Automated commit via Ansible"
      args:
        chdir: "{{ repo_dest }}"
      register: git_commit
      failed_when: git_commit.rc != 0 and "'nothing to commit'" not in git_commit.stderr

    - name: Ensure branch matches remote
      command: git branch -M {{ git_branch }}
      args:
        chdir: "{{ repo_dest }}"

    - name: Ensure origin remote uses token authentication
      ansible.builtin.command: >
        git remote set-url origin {{ repo_url }}
      args:
        chdir: "{{ repo_dest }}"

    - name: Push changes to Gitea using token header
      ansible.builtin.command: >
        git -c http.extraHeader="Authorization: Bearer {{ gitea_token }}"
        push -u origin {{ git_branch }}
      args:
        chdir: "{{ repo_dest }}"

Auf unserem Nexus Switch:

! Anlegen einer Rolle mit restriktiven Commands
role name ansible_backup
  rule 10 permit command terminal length 0
  rule 20 permit command terminal width 511
  rule 30 permit command show running-config
  rule 40 permit command show version
  rule 50 permit command show hostname
  rule 60 permit command read
  rule 70 permit command show inventory

username ansible_backup password 1234567890 role ansible_backup
username ansible_backup sshkey ssh-dsa ...

Auf unserem IOSXE Switch:

username ansible_backup secret XXXXXXXX
conf t
ip ssh pubkey-chain
 username ansible_backup
  key-string
   ssh-rsa 5AQ5pQCZEopy5ErOUD/fzf1g2BZIPOBqHB18otNLy5uU6lfJCV78agChymfmeP77GrFdXMpewj0fH9EPoPp59FSHmoZeScGqJ+HrmJEgTkp/yU4FNps0uH1YyrOBJdXBEXf90BKva76RUzaM8Cdp8S/dhNk73E365I+zgl6Q== ansible_backup@Ansible-Device
  exit
 exit
end