Ansible Playbooks
Ansible playbooks are essential tools in the DevSecOps toolkit, enabling the automation of complex IT tasks while ensuring security and compliance are integral to every step. These playbooks, written in YAML, define a series of tasks to be executed on remote machines, allowing for consistent and repeatable configurations. In the context of DevSecOps, playbooks can automate security tasks such as patch management, vulnerability assessments, and the application of security policies, integrating security measures directly into the development and deployment pipelines. This automation reduces human error, speeds up the deployment process, and ensures that security practices are consistently applied across all environments.
Moreover, Ansible playbooks support the principle of "security as code," where security practices are codified and version-controlled alongside application code. This approach ensures that security configurations are transparent, auditable, and easily updated in response to emerging threats or compliance requirements. By leveraging Ansible’s extensive library of modules, DevSecOps teams can enforce secure configurations, manage firewalls, monitor systems for compliance, and remediate issues automatically. This integration of Ansible playbooks within DevSecOps pipelines not only enhances security posture but also aligns with agile methodologies, promoting rapid and secure delivery of software.
Ansible Inventory Structure
Ansible inventory files define the hosts and groups of hosts on which Ansible commands, modules, and playbooks operate. The inventory can be static (defined in INI or YAML format) or dynamic (using a script or plugin).
INI Format Inventory
Create a file named inventory.ini
:
[webservers]
web1 ansible_host=192.168.1.10 ansible_user=username ansible_ssh_pass=password
web2 ansible_host=192.168.1.11 ansible_user=username ansible_ssh_pass=password
[dbservers]
db1 ansible_host=192.168.1.20 ansible_user=username ansible_ssh_pass=password
db2 ansible_host=192.168.1.21 ansible_user=username ansible_ssh_pass=password
[all:vars]
ansible_python_interpreter=/usr/bin/python3
[webservers]
and[dbservers]
define groups of hosts.ansible_host
,ansible_user
, andansible_ssh_pass
provide the necessary connection details.[all:vars]
defines variables applied to all hosts.
YAML Format Inventory
Create a file named inventory.yml
:
all:
vars:
ansible_python_interpreter: /usr/bin/python3
children:
webservers:
hosts:
web1:
ansible_host: 192.168.1.10
ansible_user: username
ansible_ssh_pass: password
web2:
ansible_host: 192.168.1.11
ansible_user: username
ansible_ssh_pass: password
dbservers:
hosts:
db1:
ansible_host: 192.168.1.20
ansible_user: username
ansible_ssh_pass: password
db2:
ansible_host: 192.168.1.21
ansible_user: username
ansible_ssh_pass: password
all
is the top-level group containing all hosts.vars
defines variables applied to all hosts.children
groups hosts intowebservers
anddbservers
.
Ansible Playbook Structure
An Ansible playbook is a YAML file that defines a series of tasks to be executed on specified hosts. Each playbook consists of one or more plays, and each play targets a group of hosts.
Example Playbook: setup_webserver.yml
---
- name: Setup Web Server
hosts: webservers
become: yes
tasks:
- name: Install Nginx
ansible.builtin.yum:
name: nginx
state: present
- name: Start and enable Nginx
ansible.builtin.systemd:
name: nginx
state: started
enabled: yes
- name: Copy Nginx configuration
ansible.builtin.copy:
src: /path/to/local/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
- name: Restart Nginx
ansible.builtin.systemd:
name: nginx
state: restarted
Playbook Header:
name
: Description of the play.hosts
: Target hosts for the play (e.g.,webservers
).become
: Indicates that tasks should be executed with elevated privileges (yes
forsudo
).
Tasks:
Install Nginx:
Use the
ansible.builtin.yum
module to install Nginx.
Start and enable Nginx:
Use the
ansible.builtin.systemd
module to start and enable the Nginx service.
Copy Nginx configuration:
Use the
ansible.builtin.copy
module to copy the local Nginx configuration file to the remote host.
Restart Nginx:
Use the
ansible.builtin.systemd
module to restart Nginx.
Running the Playbook
To run the playbook, navigate to the directory containing setup_webserver.yml
and execute the following command:
ansible-playbook -i inventory.yml setup_webserver.yml
Replace inventory.yml
with the path to your Ansible inventory file. This command runs the playbook on the hosts specified in the webservers
group of the inventory file. If you use the INI format, the command would be the same, just ensure the inventory file name is correct:
ansible-playbook -i inventory.ini setup_webserver.yml
Ansible Playbook: Linux Kernel Audit
playbook designed to audit the Linux kernel version on remote hosts. This playbook will check the current kernel version and compare it against a specified version to ensure it meets your security or compliance requirements.
Create a playbook named kernel_audit.yml
with the following content:
---
- name: Audit Linux Kernel Version
hosts: all
become: yes
gather_facts: no
vars:
min_kernel_version: "5.4.0"
tasks:
- name: Get current kernel version
ansible.builtin.command: uname -r
register: kernel_version
- name: Print current kernel version
ansible.builtin.debug:
msg: "Current kernel version is {{ kernel_version.stdout }}"
- name: Compare kernel version
ansible.builtin.shell: |
current_version=$(echo "{{ kernel_version.stdout }}" | sed 's/-.*//')
min_version="{{ min_kernel_version }}"
if [ "$(printf '%s\n' "$min_version" "$current_version" | sort -V | head -n1)" = "$min_version" ]; then
exit 0
else
exit 1
fi
register: kernel_comparison
ignore_errors: yes
- name: Fail if kernel version is below minimum required
ansible.builtin.fail:
msg: "Kernel version {{ kernel_version.stdout }} is below the minimum required version {{ min_kernel_version }}"
when: kernel_comparison.rc != 0
Explanation:
Playbook Header:
name
: A description of the playbook.hosts
: Specifies the target hosts (here,all
means it will run on all hosts defined in your inventory).become: yes
: Run tasks with elevated privileges (i.e.,sudo
).gather_facts: no
: Skip gathering facts for faster execution.
Variables:
min_kernel_version
: Define the minimum required kernel version for compliance.
Tasks:
Get current kernel version:
Use the
ansible.builtin.command
module to run theuname -r
command and register the output in thekernel_version
variable.
Print current kernel version:
Use the
ansible.builtin.debug
module to print the current kernel version.
Compare kernel version:
Use the
ansible.builtin.shell
module to compare the current kernel version with the minimum required version.This task uses
sed
to strip any extra suffix from the kernel version (e.g.,-generic
), then compares the versions usingsort -V
for proper version sorting.The
ignore_errors: yes
directive ensures that the playbook continues even if the kernel version is below the required version, allowing the next task to handle the failure condition.
Fail if kernel version is below minimum required:
Use the
ansible.builtin.fail
module to fail the playbook if the kernel version is below the minimum required version.
Ansible Playbook: Nginx Audit
Ansible playbook designed to audit Nginx configurations on remote hosts. This playbook will check for common security settings and best practices in the Nginx configuration file (/etc/nginx/nginx.conf
).
Create a playbook named nginx_audit.yml
with the following content:
---
- name: Audit Nginx Configuration
hosts: webservers
become: yes
gather_facts: no
tasks:
- name: Check if /etc/nginx/nginx.conf exists
ansible.builtin.stat:
path: /etc/nginx/nginx.conf
register: nginx_conf
- name: Read Nginx configuration file
ansible.builtin.command: cat /etc/nginx/nginx.conf
when: nginx_conf.stat.exists
register: nginx_conf_content
- name: Ensure server_tokens is set to off
ansible.builtin.debug:
msg: "server_tokens is set correctly"
when: "'server_tokens off;' in nginx_conf_content.stdout"
- name: Ensure SSL protocols are properly configured
ansible.builtin.debug:
msg: "SSL protocols are configured correctly"
when: "'ssl_protocols TLSv1.2 TLSv1.3;' in nginx_conf_content.stdout"
- name: Ensure HTTP methods are restricted
ansible.builtin.debug:
msg: "HTTP methods are restricted"
when: "'limit_except GET POST {' in nginx_conf_content.stdout"
- name: Print Nginx configuration file
ansible.builtin.debug:
msg: "{{ nginx_conf_content.stdout }}"
when: nginx_conf.stat.exists
- name: Fail if server_tokens is not set to off
ansible.builtin.fail:
msg: "server_tokens is not set to 'off'"
when: nginx_conf.stat.exists and "'server_tokens off;' not in nginx_conf_content.stdout"
- name: Fail if SSL protocols are not properly configured
ansible.builtin.fail:
msg: "SSL protocols are not properly configured (TLSv1.2 and TLSv1.3)"
when: nginx_conf.stat.exists and "'ssl_protocols TLSv1.2 TLSv1.3;' not in nginx_conf_content.stdout"
- name: Fail if HTTP methods are not restricted
ansible.builtin.fail:
msg: "HTTP methods are not restricted (limit_except GET POST)"
when: nginx_conf.stat.exists and "'limit_except GET POST {' not in nginx_conf_content.stdout"
Explanation:
Playbook Header:
name
: A description of the playbook.hosts
: Specifies the target hosts (here,webservers
is used, assuming this group is defined in your inventory).become: yes
: Run tasks with elevated privileges (i.e.,sudo
).gather_facts: no
: Skip gathering facts for faster execution.
Tasks:
Check if
/etc/nginx/nginx.conf
exists:Use the
ansible.builtin.stat
module to check if the Nginx configuration file exists.
Read Nginx configuration file:
Use the
ansible.builtin.command
module to read the contents of/etc/nginx/nginx.conf
and register the output.
Ensure server_tokens is set to off:
Use the
ansible.builtin.debug
module to print a message ifserver_tokens off;
is found in the Nginx configuration.
Ensure SSL protocols are properly configured:
Use the
ansible.builtin.debug
module to print a message ifssl_protocols TLSv1.2 TLSv1.3;
is found in the Nginx configuration.
Ensure HTTP methods are restricted:
Use the
ansible.builtin.debug
module to print a message iflimit_except GET POST {
is found in the Nginx configuration.
Print Nginx configuration file:
Use the
ansible.builtin.debug
module to print the contents of the Nginx configuration file.
Fail if server_tokens is not set to off:
Use the
ansible.builtin.fail
module to fail the playbook ifserver_tokens off;
is not found in the Nginx configuration.
Fail if SSL protocols are not properly configured:
Use the
ansible.builtin.fail
module to fail the playbook ifssl_protocols TLSv1.2 TLSv1.3;
is not found in the Nginx configuration.
Fail if HTTP methods are not restricted:
Use the
ansible.builtin.fail
module to fail the playbook iflimit_except GET POST {
is not found in the Nginx configuration.
Ansible Playbook: SCM (GitLab) Audit
Create a playbook named scm_audit.yml
with the following content:
Ansible playbook designed to audit GitLab or a generic Git server to ensure security and compliance with best practices. This playbook will check for common security settings, such as ensuring that repositories are private, two-factor authentication is enabled, and checking for default branch protection.
---
- name: Audit GitLab Configuration
hosts: scm_servers
become: yes
gather_facts: no
vars:
gitlab_url: "https://gitlab.example.com"
gitlab_api_token: "your_personal_access_token"
tasks:
- name: Check if GitLab API is accessible
ansible.builtin.uri:
url: "{{ gitlab_url }}/api/v4/projects"
method: GET
headers:
PRIVATE-TOKEN: "{{ gitlab_api_token }}"
register: gitlab_projects
- name: Print list of projects
ansible.builtin.debug:
msg: "{{ gitlab_projects.json }}"
- name: Fail if GitLab API is not accessible
ansible.builtin.fail:
msg: "Cannot access GitLab API. Check URL and token."
when: gitlab_projects.status != 200
- name: Check repository settings
ansible.builtin.uri:
url: "{{ gitlab_url }}/api/v4/projects/{{ item.id }}"
method: GET
headers:
PRIVATE-TOKEN: "{{ gitlab_api_token }}"
with_items: "{{ gitlab_projects.json }}"
register: repo_settings
- name: Ensure repositories are private
ansible.builtin.debug:
msg: "Repository {{ item.json.name }} is private"
when: item.json.visibility == "private"
- name: Fail if repository is not private
ansible.builtin.fail:
msg: "Repository {{ item.json.name }} is not private"
when: item.json.visibility != "private"
- name: Ensure 2FA is enabled for all users
ansible.builtin.uri:
url: "{{ gitlab_url }}/api/v4/users"
method: GET
headers:
PRIVATE-TOKEN: "{{ gitlab_api_token }}"
register: gitlab_users
- name: Check if 2FA is enabled for each user
ansible.builtin.uri:
url: "{{ gitlab_url }}/api/v4/users/{{ item.id }}"
method: GET
headers:
PRIVATE-TOKEN: "{{ gitlab_api_token }}"
with_items: "{{ gitlab_users.json }}"
register: user_settings
- name: Fail if any user does not have 2FA enabled
ansible.builtin.fail:
msg: "User {{ item.json.username }} does not have 2FA enabled"
when: not item.json.two_factor_enabled
- name: Ensure default branch protection
ansible.builtin.uri:
url: "{{ gitlab_url }}/api/v4/projects/{{ item.id }}/protected_branches"
method: GET
headers:
PRIVATE-TOKEN: "{{ gitlab_api_token }}"
with_items: "{{ gitlab_projects.json }}"
register: protected_branches
- name: Fail if default branch is not protected
ansible.builtin.fail:
msg: "Default branch of repository {{ item.id }} is not protected"
when: item.json | length == 0
Explanation:
Playbook Header:
name
: A description of the playbook.hosts
: Specifies the target hosts (e.g.,scm_servers
).become: yes
: Run tasks with elevated privileges (i.e.,sudo
).gather_facts: no
: Skip gathering facts for faster execution.
Variables:
gitlab_url
: The base URL for your GitLab instance.gitlab_api_token
: Your GitLab personal access token.
Tasks:
Check if GitLab API is accessible:
Use the
ansible.builtin.uri
module to send a GET request to the GitLab API to list projects.Register the response in
gitlab_projects
.
Print list of projects:
Use the
ansible.builtin.debug
module to print the list of projects retrieved from GitLab.
Fail if GitLab API is not accessible:
Use the
ansible.builtin.fail
module to fail the playbook if the GitLab API is not accessible.
Check repository settings:
Use the
ansible.builtin.uri
module to get settings for each project.Register the response in
repo_settings
.
Ensure repositories are private:
Use the
ansible.builtin.debug
module to print a message if the repository is private.
Fail if repository is not private:
Use the
ansible.builtin.fail
module to fail the playbook if any repository is not private.
Ensure 2FA is enabled for all users:
Use the
ansible.builtin.uri
module to list all users.Register the response in
gitlab_users
.
Check if 2FA is enabled for each user:
Use the
ansible.builtin.uri
module to get settings for each user.Register the response in
user_settings
.
Fail if any user does not have 2FA enabled:
Use the
ansible.builtin.fail
module to fail the playbook if any user does not have 2FA enabled.
Ensure default branch protection:
Use the
ansible.builtin.uri
module to check if the default branch of each project is protected.Register the response in
protected_branches
.
Fail if default branch is not protected:
Use the
ansible.builtin.fail
module to fail the playbook if the default branch of any repository is not protected.
Ansible Playbook: Log Collection and Analysis
Centralizing logs from various sources such as syslog and application logs for security analysis is a crucial task in ensuring comprehensive monitoring and analysis capabilities. Below is an Ansible playbook that demonstrates how to collect logs from remote servers and centralize them on a centralized logging server using rsyslog
.
Create a playbook named log_collection_analysis.yml
with the following content:
---
- name: Log Collection and Analysis
hosts: all
become: yes
tasks:
- name: Install rsyslog
ansible.builtin.package:
name: rsyslog
state: present
- name: Configure rsyslog for centralized logging
ansible.builtin.lineinfile:
path: /etc/rsyslog.conf
regexp: "^#*\\s*\\$ModLoad imtcp"
line: "$ModLoad imtcp"
state: present
backup: yes
- name: Ensure rsyslog listens on TCP port 514
ansible.builtin.lineinfile:
path: /etc/rsyslog.conf
regexp: "^#*\\s*\\$InputTCPServerRun 514"
line: "$InputTCPServerRun 514"
state: present
backup: yes
- name: Restart rsyslog service
ansible.builtin.service:
name: rsyslog
state: restarted
- name: Ensure rsyslog service is enabled
ansible.builtin.service:
name: rsyslog
enabled: yes
state: started
- name: Forward logs to centralized server
hosts: centralized_logging_server
become: yes
tasks:
- name: Install rsyslog
ansible.builtin.package:
name: rsyslog
state: present
- name: Configure rsyslog to receive logs from clients
ansible.builtin.lineinfile:
path: /etc/rsyslog.conf
regexp: "^#*\\s*\$ModLoad imtcp"
line: "$ModLoad imtcp"
state: present
backup: yes
- name: Ensure rsyslog listens on TCP port 514
ansible.builtin.lineinfile:
path: /etc/rsyslog.conf
regexp: "^#*\\s*\$InputTCPServerRun 514"
line: "$InputTCPServerRun 514"
state: present
backup: yes
- name: Restart rsyslog service
ansible.builtin.service:
name: rsyslog
state: restarted
- name: Ensure rsyslog service is enabled
ansible.builtin.service:
name: rsyslog
enabled: yes
state: started
Explanation:
Playbook Structure:
The playbook consists of two plays:
First Play (Log Collection Configuration on All Hosts):
Installs
rsyslog
on all hosts (hosts: all
).Configures
rsyslog
to listen for incoming logs over TCP ($ModLoad imtcp
and$InputTCPServerRun 514
).Restarts and enables the
rsyslog
service to apply the configuration changes.
Second Play (Centralized Logging Server Configuration):
Installs
rsyslog
on the centralized logging server (hosts: centralized_logging_server
).Configures
rsyslog
on the centralized server to receive logs from clients ($ModLoad imtcp
and$InputTCPServerRun 514
).Restarts and enables the
rsyslog
service on the centralized server to apply the configuration changes.
Tasks:
Install rsyslog: Ensures that
rsyslog
is installed on the hosts.Configure rsyslog:
Uses
ansible.builtin.lineinfile
module to modify/etc/rsyslog.conf
to enable TCP logging and specify the TCP port 514 for logging.
Restart and enable rsyslog service: Ensures that the
rsyslog
service is restarted and enabled to apply the configuration changes.
Ansible Playbook: Firewall Rules Management with iptables
Automating firewall rule updates based on security policies and requirements analysis is crucial for maintaining a secure environment. Below is an Ansible playbook that demonstrates how to manage firewall rules on Linux servers using iptables
. Adjustments can be made depending on your specific firewall solution (e.g., firewalld
for CentOS/RHEL, ufw
for Ubuntu).
Create a playbook named firewall_management.yml
with the following content:
---
- name: Firewall Rules Management
hosts: firewall_servers
become: yes
tasks:
- name: Allow SSH (Port 22) from specific IP addresses
ansible.builtin.iptables:
chain: INPUT
protocol: tcp
destination_port: 22
source: "{{ item }}"
jump: ACCEPT
with_items:
- 192.168.1.100 # Replace with your allowed IP addresses
- 192.168.1.101
- 10.0.0.1
- name: Allow HTTP (Port 80) from any IP
ansible.builtin.iptables:
chain: INPUT
protocol: tcp
destination_port: 80
jump: ACCEPT
- name: Allow HTTPS (Port 443) from any IP
ansible.builtin.iptables:
chain: INPUT
protocol: tcp
destination_port: 443
jump: ACCEPT
- name: Deny all other inbound traffic
ansible.builtin.iptables:
chain: INPUT
policy: DROP
- name: Save iptables rules
ansible.builtin.shell: iptables-save > /etc/sysconfig/iptables
Explanation:
Playbook Structure:
The playbook contains a single play targeting
firewall_servers
.The
become: yes
directive ensures that tasks are executed with root privileges.
Tasks:
Allow SSH (Port 22) from specific IP addresses:
Uses the
ansible.builtin.iptables
module to allow SSH (TCP port 22) connections from specified IP addresses (192.168.1.100
,192.168.1.101
,10.0.0.1
).Loops through the list of allowed IP addresses using
with_items
.
Allow HTTP (Port 80) from any IP:
Allows HTTP (TCP port 80) traffic from any IP address.
Allow HTTPS (Port 443) from any IP:
Allows HTTPS (TCP port 443) traffic from any IP address.
Deny all other inbound traffic:
Sets the default policy for inbound traffic to
DROP
, effectively denying all other incoming connections.
Save iptables rules:
Uses the
ansible.builtin.shell
module to save the currentiptables
rules to/etc/sysconfig/iptables
(adjust this path for your distribution).
Ansible Playbook: Backup and Restore Procedures
Automating backup tasks and ensuring robust restoration processes are critical for maintaining data integrity and availability. Below is an Ansible playbook that demonstrates how to automate backup and restore procedures for a directory, including encryption of backups using tar
and gpg
(GNU Privacy Guard).
Create a playbook named backup_restore.yml
with the following content:
---
- name: Backup and Restore Procedures
hosts: backup_server
become: yes
vars:
backup_directory: "/backup"
source_directory: "/path/to/your/source"
encrypted_backup_file: "backup.tar.gz.gpg"
encryption_passphrase: "your_encryption_passphrase"
tasks:
- name: Ensure backup directory exists
ansible.builtin.file:
path: "{{ backup_directory }}"
state: directory
mode: '0755'
- name: Backup directory with encryption
ansible.builtin.shell: |
tar -czf - "{{ source_directory }}" | gpg --symmetric --passphrase "{{ encryption_passphrase }}" -o "{{ backup_directory }}/{{ encrypted_backup_file }}"
args:
warn: no
environment:
GNUPGHOME: "{{ backup_directory }}/.gnupg"
register: backup_result
- name: Report backup status
ansible.builtin.debug:
msg: "Backup task result: {{ backup_result.stdout_lines }}"
- name: Restore Backup
hosts: restore_server
become: yes
vars:
backup_directory: "/backup"
encrypted_backup_file: "backup.tar.gz.gpg"
decryption_passphrase: "your_encryption_passphrase"
restore_directory: "/path/to/restore"
tasks:
- name: Ensure restore directory exists
ansible.builtin.file:
path: "{{ restore_directory }}"
state: directory
mode: '0755'
- name: Decrypt and extract backup
ansible.builtin.shell: |
gpg --decrypt --passphrase "{{ decryption_passphrase }}" "{{ backup_directory }}/{{ encrypted_backup_file }}" | tar -xzf - -C "{{ restore_directory }}"
args:
warn: no
environment:
GNUPGHOME: "{{ backup_directory }}/.gnupg"
register: restore_result
- name: Report restore status
ansible.builtin.debug:
msg: "Restore task result: {{ restore_result.stdout_lines }}"
Explanation:
Playbook Structure:
The playbook is divided into two plays:
Backup Procedure: Executes on
backup_server
to create an encrypted backup of the specified directory.Restore Procedure: Executes on
restore_server
to decrypt and restore the backup to a specified directory.
Tasks:
Ensure backup/restore directory exists: Uses
ansible.builtin.file
module to ensure that the backup or restore directory exists with the correct permissions (mode: '0755'
).Backup directory with encryption: Uses
ansible.builtin.shell
module to create a backup ofsource_directory
usingtar
and encrypt it withgpg
. Theencryption_passphrase
is used to symmetrically encrypt the backup file. TheGNUPGHOME
environment variable ensures the GPG keyring is stored within the backup directory for security.Report backup status: Uses
ansible.builtin.debug
module to print the result of the backup task.Decrypt and extract backup: On the restore server, uses
ansible.builtin.shell
module to decrypt the encrypted backup file usinggpg
and extract it usingtar
to therestore_directory
. Thedecryption_passphrase
is used for decryption.Report restore status: Uses
ansible.builtin.debug
module to print the result of the restore task.
Variables:
backup_directory
: Directory where backups are stored.source_directory
: Directory to be backed up.encrypted_backup_file
: Name of the encrypted backup file.encryption_passphrase
: Passphrase used for encrypting the backup.decryption_passphrase
: Passphrase used for decrypting the backup.
References
Hands-On Security in DevOps by Tony Hsiang-Chih Hsu