06/25/2020: Running MindPoint Group RHEL7 STIG On Centos
This is going to be a long post. We’ll start from scratch and develop the ability to run the MindPoint Group RHEL7 STIG on Centos 7. If you want to go farther, there is also discussion of Lynis. Just running the STIG playbook results in the server having a Lynis hardening index of 73. It can be better.
This work is being done at the request of the Enterprise Container Working Group (ECWG) of the Office of Information and Technology (OIT - https://www.oit.va.gov/ at the Department of Veteran Affairs.
Overview
- Provision a server.
- Initial setup so Ansible will run.
- Run Ansible playbook to be STIG compliant.
Provision
Terraform is used to provision an EC2 server. All configuration is done in variables.tf
.
-
Create a
provision
directory. -
Create
variables.tf
. Make sure to update using your own values.
variable "aws_profile" {
default = "ic1"
}
variable "aws_region" {
default = "us-east-1"
}
variable "instance_type" {
default = "t3.medium"
}
variable "pki_private_key" {
default = "KEYPAIR.pem"
}
variable "pki_public_key" {
default = "KEYPAIR.pub"
}
variable "ssh_cidr_block" {
default = "0.0.0.0/0"
}
# The ssh_user variable is used by both Terraform and Ansible.
variable "ssh_user" {
default = "centos"
}
variable "subnet_id" {
default = "subnet-02c78f939d58e2320"
}
variable "vpc_id" {
default = "vpc-04bdc9b68b19472c3"
}
# Ansible Variables
variable "ansible_python_interpreter" {
default = "/bin/python3"
}
variable "banner_text_file" {
default = "file-banner-text.txt"
}
variable "password_max_days" {
default = "90"
}
variable "password_min_days" {
default = "1"
}
variable "sha_crypt_max_rounds" {
default = "10000"
}
variable "sha_crypt_min_rounds" {
default = "5000"
}
- Create
tr_ansible_vars_file.yml.tpl
. This template is used to generatetr_ansible_vars_file.yml
which is read by the Ansible playbook.
ansible_python_interpreter: ${ansible_python_interpreter}
centos_user_password: ${centos_user_password}
- Create
data-sources.tf
.
#
# Find the latest CentOS AMI.
#
data "aws_ami" "centos" {
owners = ["679593333241"]
most_recent = true
filter {
name = "name"
values = ["CentOS Linux 7 x86_64 HVM EBS *"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
}
#
# The password will be created as a resource in another file. After the
# terraform plan is applied, the clear text password is in
# tf_ansible_vars_file.yml. Be careful not to check that file into
# a code repository.
#
data "template_file" "tf_ansible_vars_file" {
template = "${file("./tr_ansible_vars_file.yml.tpl")}"
vars = {
ansible_python_interpreter = var.ansible_python_interpreter
centos_user_password = random_password.centos_user_password.result
}
}
- Create
security-groups.tf
#
# Ingress
#
resource "aws_security_group" "centos_allow_ssh" {
name = "centos_allow_ssh"
description = "Allow SSH"
vpc_id = var.vpc_id
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ var.ssh_cidr_block ]
}
tags = {
Name = "centos_allow_ssh"
}
}
#
# Egress
#
resource "aws_security_group" "centos_allow_any_outbound" {
name = "centos_allow_any_outbound"
description = "Centos Allow Any Outbound"
vpc_id = var.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "centos_allow_any_outbound"
}
}
- Create
infrastructure-as-code.tf
. Here is the heart of the Terraform file. This file creates the EC2 instance. Note that an EIP is allocated so that the instance can be stopped and restarted without a chance in IP address. Aninventory
file is created for Ansible to use. The EC2 name will have a timestamp so that you can create more than one instance. However, if you do provision more than one server you lose the ability to destroy using terraform.
# When a profile is specified, tf will try to use
# ~/.aws/credentials.
provider "aws" {
region = var.aws_region
profile = var.aws_profile
version = "~> 2.66"
}
resource "random_password" "centos_user_password" {
length = 16
special = true
override_special = "_%@"
}
resource "aws_key_pair" "centos" {
public_key = file(var.pki_public_key)
}
resource "aws_instance" "centos" {
ami = data.aws_ami.centos.id
associate_public_ip_address = "true"
instance_type = var.instance_type
key_name = aws_key_pair.centos.key_name
subnet_id = var.subnet_id
vpc_security_group_ids = [
aws_security_group.centos_allow_ssh.id,
aws_security_group.centos_allow_any_outbound.id
]
tags = { Name = "centos-${formatdate("YYYYMMDDhhmmss", timestamp())}" }
}
resource "local_file" "tf_ansible_vars_file" {
content = data.template_file.tf_ansible_vars_file.rendered
filename = "${path.module}/tf_ansible_vars_file.yml"
}
resource "aws_eip" "centos" {
instance = aws_instance.centos.id
vpc = true
tags = { Name = "centos-${formatdate("YYYYMMDDhhmmss", timestamp())}" }
connection {
type = "ssh"
user = var.ssh_user
private_key = file(var.pki_private_key)
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"sudo yum install -y python3"
]
}
provisioner "local-exec" {
command = "ansible-playbook -u ${var.ssh_user} -i '${self.public_ip},' --private-key ${var.pki_private_key} playbook.setup.yml"
environment = {
ANSIBLE_HOST_KEY_CHECKING = "False"
}
# command = "./run-setup-playbook.sh ${var.ssh_user} ${self.public_ip},' ${var.pki_private_key}"
}
}
#
# We need to export the EIP ip address, not the instance's.
#
resource "local_file" "inventory" {
content = "[all]\n${aws_eip.centos.public_ip}\n"
filename = "${path.module}/inventory"
}
- Create the last file needed for provisioning, the
playbook.setup.yml
for Ansible.
---
- hosts: all
gather_facts: false
become: yes
vars_files:
- tf_ansible_vars_file.yml
tasks:
- name: upgrade all packages
yum:
name: '*'
state: latest
vars:
ansible_python_interpreter: /usr/bin/python
#
# The python3-dnf package is not being found. So I am using yum
# instead of dnf.
#
- name: install packages with python2
yum:
name:
- epel-release
state: latest
update_cache: yes
vars:
ansible_python_interpreter: /usr/bin/python
- name: install python selinux bindings so that copy task will work.
yum:
name:
- libselinux-python3
state: latest
update_cache: yes
vars:
ansible_python_interpreter: /usr/bin/python
- name: Create Lynis Yum repository file.
copy:
dest: /etc/yum.repos.d/lynis.repo
content: |
[lynis]
name=CISOfy Software - Lynis package
baseurl=https://packages.cisofy.com/community/lynis/rpm/
enabled=1
gpgkey=https://packages.cisofy.com/keys/cisofy-software-rpms-public.key
gpgcheck=1
priority=2
mode: "644"
- name: install packages with python2
yum:
name:
- aide
- ca-certificates
- curl
- fail2ban
- lynis
- nss
- openssl
- python2-jmespath
- usbguard
state: latest
update_cache: yes
vars:
ansible_python_interpreter: /usr/bin/python
- name: set password for 'centos' user.
user:
name: centos
password: ""
- name: delete no password sudo configuration
file:
path: /etc/sudoers.d/90-cloud-init-users
state: absent
- Let Terraform provision the server.
teraform init
terraform plan
terraform apply --auto-approve
- When the apply is done, you’ll be able to SSH into the server. You don’t need to perform this step. It’s just for general knowlege in case you do.
ssh-add KEYPAIR.pem
IP_ADDRESS=$(cat inventory | tail -n 1)
ssh centos@$IP_ADDRESS
- Make the current directory available to future steps.
export PROVISION_DIR=$(pwd)
Run RHEL7 STIG
- Make sure to have jmespath installed on your local workstation.
pip install jmespath
- Download the STIG project.
cd /data/projects
git clone https://github.com/MindPointGroup/RHEL7-STIG.git
- Connect to the STIG directory.
cd /data/projects/RHEL7-STIG
- Create a playbook to run the STIG.
cat <<EOF > playbook.stig.yml
---
- name: Apply STIG
hosts: all
become: yes
roles:
- role: ""
EOF
- Create a script to run the STIG playbook.
cat <<EOF > run-stig-playbook.sh
#!/bin/bash
# Copy the inventory file generated by Terraform.
cp $PROVISION_DIR/inventory inventory
# Get the SUDO password from the generated Ansible variable file.
ANSIBLE_SUDO_PASS=\$(cat \$PROVISION_DIR/tf_ansible_vars_file.yml | grep centos_user_password | awk '{print \$2}')
python3 \
\$(which ansible-playbook) \
--extra-vars "ansible_sudo_pass=\$ANSIBLE_SUDO_PASS" \
-i inventory \
-u centos \
playbook.stig.yml
EOF
chmod +x run-stig-playbook.sh
- Run the STIG playbook.
./run-stig-playbook.sh | tee stig.out
STIG Results
The results are too long to include directly. See https://gist.github.com/medined/52d814466fa11d4a633561011c29ccf1.
Lynis Results
The results are too long to include directly. See https://gist.github.com/medined/1d15a0c5b599fed8fc2515bcd0c212ad.
Improving Lynis Hardening Index
- Create a script to run the Lynis playbook.
cat <<EOF > run-lynis-playbook.sh
#!/bin/bash
# Copy the inventory file generated by Terraform.
cp $PROVISION_DIR/inventory inventory
# Get the SUDO password from the generated Ansible variable file.
ANSIBLE_SUDO_PASS=\$(cat \$PROVISION_DIR/tf_ansible_vars_file.yml | grep centos_user_password | awk '{print \$2}')
python3 \
\$(which ansible-playbook) \
--extra-vars "ansible_sudo_pass=\$ANSIBLE_SUDO_PASS" \
--extra-vars "ssh_user=centos" \
-i inventory \
-u centos \
playbook.lynis.yml
EOF
chmod +x run-lynis-playbook.sh
- Create
playbook.lynis.yml
---
- hosts: all
gather_facts: false
become: yes
handlers:
- name: restart sshd
service: name=sshd state=restarted
listen: restart sshd
- name: Unconditionally reboot the machine
reboot:
listen: reboot system
tasks:
# ....###....##.....##.########.##.....##
# ...##.##...##.....##....##....##.....##
# ..##...##..##.....##....##....##.....##
# .##.....##.##.....##....##....#########
# .#########.##.....##....##....##.....##
# .##.....##.##.....##....##....##.....##
# .##.....##..#######.....##....##.....##
- name: AUTH-9230 password hashing rounds - min
lineinfile:
path: /etc/login.defs
state: present
regexp: "^SHA_CRYPT_MIN_ROUNDS"
line: "SHA_CRYPT_MIN_ROUNDS 6000"
tags:
- AUTH-9230
- name: AUTH-9230 password hashing rounds - max
lineinfile:
path: /etc/login.defs
state: present
regexp: "^SHA_CRYPT_MAX_ROUNDS"
line: "SHA_CRYPT_MAX_ROUNDS 10000"
tags:
- AUTH-9230
- name: AUTH-9328 - Default umask values
lineinfile:
path: /etc/login.defs
state: present
regexp: "^UMASK"
line: "UMASK 027"
tags:
- AUTH-9328
- name: AUTH-9328 - Default umask values in /etc/login.defs
copy:
dest: /etc/profile.d/umask.sh
content: |
# By default, we want umask to get set. This sets it for login shell
# Current threshold for system reserved uid/gids is 200
# You could check uidgid reservation validity in
# /usr/share/doc/setup-*/uidgid file
if [ $UID -gt 199 ] && [ "`id -gn`" = "`id -un`" ]; then
umask 007
else
umask 027
fi
mode: "644"
tags:
- AUTH-9328
# NIST recommends setting the daemon umask to 027
# (REHL5: http://nvd.nist.gov/scap/content/stylesheet/scap-rhel5-document.htm).
#
- name: AUTH-9328 - does /etc/init.d/functions exist?
stat:
path: /etc/init.d/functions
register: auth9328
- name: AUTH-9328 - Default umask values in /etc/init.d/functions
lineinfile:
path: /etc/init.d/functions
state: present
regexp: "^umask 022"
line: "umask 027"
when: auth9328.stat.exists
tags:
- AUTH-9328
- name: AUTH-9408 (Logging of failed login attempts)
lineinfile:
path: /etc/login.defs
state: present
regexp: "^FAILLOG_ENAB"
line: "FAILLOG_ENAB yes"
tags:
- AUTH-9328
- name: Ensure delay after failed login
lineinfile:
path: /etc/login.defs
state: present
regexp: "^FAIL_DELAY"
line: "FAIL_DELAY 4"
tags:
- "https://www.lisenet.com/2017/centos-7-server-hardening-guide/"
# .########.####.##.......########
# .##........##..##.......##......
# .##........##..##.......##......
# .######....##..##.......######..
# .##........##..##.......##......
# .##........##..##.......##......
# .##.......####.########.########
- name: FILE-6344 proc mount - hidepid
block:
- name: FILE-6344 proc mount - hidepid
lineinfile:
path: /etc/fstab
state: present
regexp: "^#?proc /proc"
line: proc /proc proc rw,nosuid,nodev,noexec,relatime,hidepid=2 0 0
tags:
- FILE-6344
#
# Since /proc is using hidepid, the polkitd can not see
# /proc unless we fix its access.
#
# The next three steps fixes "GDBus.Error:org.freedesktop.PolicyKit1.Error.Failed - Cannot determine user of subject"
#
- name: FILE-6344 proc mount - create group
group:
name: monitor
state: present
- name: FILE-6344 proc mount - add monitor to group polkitd
user:
name: polkitd
groups: monitor
append: yes
- name: FILE-6344 proc mount - get group id
shell: getent group monitor | cut -d':' -f3
register: monitor_group_register
- debug:
var: monitor_group_register.stdout
- name: FILE-6344 proc mount - hidepid
lineinfile:
path: /etc/fstab
state: present
regexp: "^#?proc /proc"
line: "proc /proc proc rw,nosuid,nodev,noexec,relatime,hidepid=2,gid= 0 0"
tags:
- FILE-6344
- name: FILE-6374 mount /dev/shm noexec
lineinfile:
path: /etc/fstab
state: present
regexp: "^#?tmpfs /dev/shm"
line: tmpfs /dev/shm /tmpfs rw,seclabel,nosuid,noexec,nodev,size=2G 0 0
tags:
- FILE-6374
- name: FILE-6374 mount /dev noexec
lineinfile:
path: /etc/fstab
state: present
regexp: "^#?devtmpfs /dev"
line: devtmpfs /dev devtmpfs rw,seclabel,nosuid,noexec,size=2G,nr_inodes=471366,mode=755 0 0
tags:
- FILE-6374
- name: FILE-6374 mount /tmp noexec
lineinfile:
path: /etc/fstab
state: present
regexp: "^#?tmpfs /tmp"
line: tmpfs /tmp tmpfs rw,seclabel,nosuid,noexec,nodev,size=2G 0 0
tags:
- FILE-6374
#
# Some pages on the Internet suggested to use "blacklist <filesystem>"
# instead of the "/bin/true" approach. Empirical testing shows that
# the approach below works. At least as far as Lynis is concerned.
#
- name: FILE-6430 (Disable mounting of some filesystems)
copy:
dest: /etc/modprobe.d/lynis-filesystem-blacklist.conf
content: |
install cramfs /bin/true
install squashfs /bin/true
install udf /bin/true
tags:
- FILE-6430
- CCE-80137-3
# .##.....##....###....########..########..########.##....##
# .##.....##...##.##...##.....##.##.....##.##.......###...##
# .##.....##..##...##..##.....##.##.....##.##.......####..##
# .#########.##.....##.########..##.....##.######...##.##.##
# .##.....##.#########.##...##...##.....##.##.......##..####
# .##.....##.##.....##.##....##..##.....##.##.......##...###
# .##.....##.##.....##.##.....##.########..########.##....##
- name: HRDN-7220 (Check if one or more compilers are installed)
file:
path: /usr/bin/as
state: absent
tags:
- HRDN-7220
# .##....##.########.########..##....##.########.##......
# .##...##..##.......##.....##.###...##.##.......##......
# .##..##...##.......##.....##.####..##.##.......##......
# .#####....######...########..##.##.##.######...##......
# .##..##...##.......##...##...##..####.##.......##......
# .##...##..##.......##....##..##...###.##.......##......
# .##....##.########.##.....##.##....##.########.########
- name: KRNL-5820 - Core dump - ProcessSizeMax
lineinfile:
path: /etc/systemd/coredump.conf
state: present
regexp: "^#?ProcessSizeMax"
line: "ProcessSizeMax=0"
notify: restart sshd
tags:
- KRNL-5820
- name: KRNL-5820 - Core dump - storage
lineinfile:
path: /etc/systemd/coredump.conf
state: present
regexp: "^#?Storage"
line: "Storage=none"
notify: restart sshd
tags:
- KRNL-5820
- name: KRNL-5820 - Core dump - profile
copy:
dest: /etc/profile.d/KRNL-5820.sh
content: |
ulimit -c 0
mode: 644
notify: restart sshd
tags:
- KRNL-5820
- name: KRNL-5820 - Core dump - limits
copy:
dest: /etc/security/limits.d/KRNL-5820.conf
content: |
#<domain> <type> <item> <value>
* hard core 0
mode: 644
notify: restart sshd
tags:
- KRNL-5820
#
# net.ipv6.conf.default.accept_redirects and net.ipv4.conf.all.forwarding are not being set.
#
- name: KRNL-6000 (Check sysctl key pairs in scan profile)
copy:
dest: /etc/sysctl.d/90-lynis.conf
content: |
kernel.dmesg_restrict=1
kernel.kptr_restrict=2
kernel.sysrq=0
kernel.yama.ptrace_scope=1
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.all.forwarding=0
net.ipv4.conf.all.log_martians=1
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.default.log_martians=1
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
notify: reboot system
tags:
KRNL-6000
# ..######..##.....##.########.##.......##......
# .##....##.##.....##.##.......##.......##......
# .##.......##.....##.##.......##.......##......
# ..######..#########.######...##.......##......
# .......##.##.....##.##.......##.......##......
# .##....##.##.....##.##.......##.......##......
# ..######..##.....##.########.########.########
- name: SHLL-6230 umask check - /etc/bashrc 002
lineinfile:
path: /etc/bashrc
state: present
regexp: "^ umask 002"
line: " umask 027"
tags:
- SHLL-6230
- name: SHLL-6230 umask check - /etc/bashrc 022
lineinfile:
path: /etc/bashrc
state: present
regexp: "^ umask 022"
line: " umask 027"
tags:
- SHLL-6230
- name: SHLL-6230 umask check - /etc/csh.cshrc 002
lineinfile:
path: /etc/csh.cshrc
state: present
regexp: "^ umask 002"
line: " umask 027"
tags:
- SHLL-6230
- name: SHLL-6230 umask check - /etc/csh.cshrc 022
lineinfile:
path: /etc/csh.cshrc
state: present
regexp: "^ umask 022"
line: " umask 027"
tags:
- SHLL-6230
- name: SHLL-6230 umask check - /etc/profile 002
lineinfile:
path: /etc/profile
state: present
regexp: "^ umask 002"
line: " umask 027"
tags:
- SHLL-6230
- name: SHLL-6230 umask check - /etc/profile 022
lineinfile:
path: /etc/profile
state: present
regexp: "^ umask 022"
line: " umask 027"
tags:
- SHLL-6230
# ..######..##....##.####.########.....########.########..######..########..######.
# .##....##.##...##...##..##.....##.......##....##.......##....##....##....##....##
# .##.......##..##....##..##.....##.......##....##.......##..........##....##......
# ..######..#####.....##..########........##....######....######.....##.....######.
# .......##.##..##....##..##..............##....##.............##....##..........##
# .##....##.##...##...##..##..............##....##.......##....##....##....##....##
# ..######..##....##.####.##..............##....########..######.....##.....######.
- name: Copy default lynis profile
copy:
src: /etc/lynis/default.prf
dest: /etc/lynis/custom.prf
remote_src: true
#
# Centos does not have a /var/account directory. However,
# we do load the audit package which tracks user actions.
#
- name: Skip ACCT-9622 (Check for available Linux accounting information)
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=ACCT-9622"
line: "skip-test=ACCT-9622"
tags:
ACCT-9622
- name: Skip AUTH-9229 (Check password hashing methods)
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=AUTH-9229"
line: "skip-test=AUTH-9229"
tags:
AUTH-9229
#
# Changing how and where directories are mounted is beyond the scope of this
# project. Ideally /tmp, /home, and /var should be on separate drives.
#
- name: Skip FILE-6310 (Checking /tmp, /home and /var directory)
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=FILE-6310"
line: "skip-test=FILE-6310"
tags:
FILE-6310
# IPTABLES are beyond the scope of this project. I believe include
# defense in depth. However,
#
# 1. Firewall rules are very application-specific.
# 2. EC2 instances use security groups.
#
- name: Skip FIRE-4508 (Check used policies of iptables chains)
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=FIRE-4508"
line: "skip-test=FIRE-4508"
tags:
FIRE-4508
#
# malware scans are too environment specific for a generic
# project like this to resolve.
#
- name: Skip HRDN-7230 (Check for malware scanner)
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=HRDN-7230"
line: "skip-test=HRDN-7230"
tags:
HRDN-7230
# Checking for external logging is beyond the scope of this
# project. There are simply too many ways to enable this
# feature.
#
- name: Skip LOGG-2154 (Checking syslog configuration file)
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=LOGG-2154"
line: "skip-test=LOGG-2154"
tags:
LOGG-2154
# Checking for anti-virus software is beyond the scope of this
# project.
#
- name: Skip MALW-3280 (Check if anti-virus tool is installed)
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=MALW-3280"
line: "skip-test=MALW-3280"
tags:
MALW-3280
- name: Skip PKGS-7420 because servers will be terminated, not updated.
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=PKGS-7420"
line: "skip-test=PKGS-7420"
tags:
- PKGS-7420
#
# SSH-7408 checks to see if the server runs SSH on something other
# than 22 (the default port).
#
# Changing the port is a bit complex in an automated provision.
# - switch to terraform to generate custom security group.
# - connect via 22:
# - change the port number in /etc/ssh/sshd_config.
# - semanage port -a -t ssh_port_t -p tcp 15762
# - sudo systemctl restart sshd
# - change ansible and other scripts to use the new port number.
#
# All of that work is possible but should not be done on a whim.
#
- name: Skip SSH-7408 SSH non-default port
lineinfile:
path: /etc/lynis/custom.prf
state: present
regexp: "^skip-test=SSH-7408:Port"
line: "skip-test=SSH-7408:Port"
tags:
- SSH-7408
# ..######...######..##.....##
# .##....##.##....##.##.....##
# .##.......##.......##.....##
# ..######...######..#########
# .......##.......##.##.....##
# .##....##.##....##.##.....##
# ..######...######..##.....##
- name: SSH-7408 - hardening SSH configuration - AllowAgentForwarding
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?AllowAgentForwarding"
line: "AllowAgentForwarding no"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - AllowTcpForwarding
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?AllowTcpForwarding"
line: "AllowTcpForwarding no"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - ClientAliveCountMax
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?ClientAliveCountMax"
line: "ClientAliveCountMax 2"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - ClientAliveInterval
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?ClientAliveInterval"
line: "ClientAliveInterval 300"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - Compression
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?Compression"
line: "Compression no"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - INFO
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?LogLevel"
line: "LogLevel VERBOSE"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - MaxAuthTries
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?MaxAuthTries"
line: "MaxAuthTries 3"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - MaxSessions
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?MaxSessions"
line: "MaxSessions 2"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - PermitRootLogin
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?PermitRootLogin"
line: "PermitRootLogin no"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
# - name: SSH-7408 - hardening SSH configuration - Port
# lineinfile:
# path: /etc/ssh/sshd_config
# state: present
# regexp: "^#?Port"
# line: "Port "
# validate: sshd -tf %s
# notify: restart sshd
# tags:
# - SSH-7408
- name: SSH-7408 - hardening SSH configuration - TCPKeepAlive
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?TCPKeepAlive"
line: "TCPKeepAlive no"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - UseDNS
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?UseDNS"
line: "UseDNS no"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7408 - hardening SSH configuration - X11Forwarding
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?X11Forwarding"
line: "X11Forwarding no"
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7408
- name: SSH-7440 (Check OpenSSH option AllowUsers and AllowGroups)
lineinfile:
path: /etc/ssh/sshd_config
state: present
regexp: "^#?AllowUsers"
line: "AllowUsers "
validate: sshd -tf %s
notify: restart sshd
tags:
- SSH-7440
# ..######..########..#######..########.....###.....######...########
# .##....##....##....##.....##.##.....##...##.##...##....##..##......
# .##..........##....##.....##.##.....##..##...##..##........##......
# ..######.....##....##.....##.########..##.....##.##...####.######..
# .......##....##....##.....##.##...##...#########.##....##..##......
# .##....##....##....##.....##.##....##..##.....##.##....##..##......
# ..######.....##.....#######..##.....##.##.....##..######...########
- name: STRG-1846 - Check if firewire storage is disabled
copy:
dest: /etc/modprobe.d/firewire.conf
content: |
blacklist firewire-core
tags:
- STRG-1846
# .########..#######...#######..##......
# ....##....##.....##.##.....##.##......
# ....##....##.....##.##.....##.##......
# ....##....##.....##.##.....##.##......
# ....##....##.....##.##.....##.##......
# ....##....##.....##.##.....##.##......
# ....##.....#######...#######..########
- name: TOOL-5104 Fail2ban - create jail
copy:
dest: /etc/fail2ban/jail.local
content: |
[DEFAULT]
bantime = 1800
findtime = 300
maxretry = 3
banaction = iptables-multiport
backend = systemd
[sshd]
enabled = true
tags:
- TOOL-5104
- name: TOOL-5104 Fail2ban - start and enable
systemd:
daemon_reload: yes
enabled: yes
masked: no
name: fail2ban
state: started
# .##.....##..######..########.
# .##.....##.##....##.##.....##
# .##.....##.##.......##.....##
# .##.....##..######..########.
# .##.....##.......##.##.....##
# .##.....##.##....##.##.....##
# ..#######...######..########.
- name: USB-1000 (Check if USB storage is disabled)
copy:
dest: /etc/modprobe.d/lynis-usb-storage-blacklist.conf
content: |
install usb-storage /bin/true
tags:
- USB-1000
- name: USB-3000 (Check for presence of USBGuard)
lineinfile:
path: /etc/usbguard/usbguard-daemon.conf
state: present
regexp: "^PresentControllerPolicy="
line: "PresentControllerPolicy=apply-policy"
tags:
- USB-3000
# .########..########.########...#######...#######..########
# .##.....##.##.......##.....##.##.....##.##.....##....##...
# .##.....##.##.......##.....##.##.....##.##.....##....##...
# .########..######...########..##.....##.##.....##....##...
# .##...##...##.......##.....##.##.....##.##.....##....##...
# .##....##..##.......##.....##.##.....##.##.....##....##...
# .##.....##.########.########...#######...#######.....##...
#
# Lots of changes were made. Let's reboot to make sure
# everything takes effect.
#
- name: Unconditionally reboot the machine
reboot:
- Run the Ansible playbook.
./run-stig-playbook.sh
The log file is /var/log/lynis.log
. It should show a hardening index of 100. Please review the skipped tests. And the suggestions.
06/20/2020: Running OPENSCAP (OSCAP) on Fedora CoreOS
I am exploring how to create a Fedora CoreOS server that can pass as many security checks as possible. However, I am not a security guru. Make sure to vet anything you read here with your own experts.
This work is being done at the request of the Enterprise Container Working Group (ECWG) of the Office of Information and Technology (OIT - https://www.oit.va.gov/) at the Department of Veteran Affairs.
Goal
Run oscap
on a Fedora CoreOS server.
Definitions
Fedora CoreOS is an automatically-updating, minimal operating system for running containerized workloads securely and at scale. However, we’ll probably be replacing virtual servers instead of updating them.
OpenSCAP is an ecosystem providing multiple tools to assist administrators and auditors with assessment, measurement, and enforcement of security baselines. SCAP or Security Content Automation Protocol is a U.S. standard maintained by National Institute of Standards and Technology (NIST).
rpm-ostree - rpm-ostree is a hybrid image/package system. It uses libOSTree as a base image format, and accepts RPM on both the client and server side, sharing code with the dnf project
XCCDF - XCCDF is a specification language for writing security checklists, benchmarks, and related kinds of documents. An XCCDF document represents a structured collection of security configuration rules for some set of target systems.
Installation
The first step is to install the software.
sudo rpm-ostree install openscap-scanner scap-security-guide zip
sudo reboot
Of note is that the installation provides the following files.
$ ls -c1 /usr/share/xml/scap/ssg/content/*fedora*.xml
/usr/share/xml/scap/ssg/content/ssg-fedora-cpe-dictionary.xml
/usr/share/xml/scap/ssg/content/ssg-fedora-cpe-oval.xml
/usr/share/xml/scap/ssg/content/ssg-fedora-ds-1.2.xml
/usr/share/xml/scap/ssg/content/ssg-fedora-ds.xml
/usr/share/xml/scap/ssg/content/ssg-fedora-ocil.xml
/usr/share/xml/scap/ssg/content/ssg-fedora-oval.xml
/usr/share/xml/scap/ssg/content/ssg-fedora-xccdf.xml
I don’t yet know how they are used, but the XCCDF file looks promising. We can find more information. The profiles are relevant, I think.
$ oscap info /usr/share/xml/scap/ssg/content/ssg-fedora-xccdf.xml
Document type: XCCDF Checklist
Checklist version: 1.1
Imported: 1970-01-01T00:00:00
Status: draft
Generated: 2020-06-03
Resolved: true
Profiles:
Title: OSPP - Protection Profile for General Purpose Operating Systems
Id: ospp
Title: PCI-DSS v3 Control Baseline for Fedora
Id: pci-dss
Title: Standard System Security Profile for Fedora
Id: standard
Referenced check files:
ssg-fedora-oval.xml
system: http://oval.mitre.org/XMLSchema/oval-definitions-5
ssg-fedora-ocil.xml
system: http://scap.nist.gov/schema/ocil/2
Run Evaluation
Let’s use the standard
profile to run an evaluation. I change the ownership of the result files so that I can scp
them to my local workstation.
sudo oscap xccdf eval \
--profile standard \
--fetch-remote-resources \
--report xccdf-report.html \
--results ssg-fedora-xccdf-results.xml \
/usr/share/xml/scap/ssg/content/ssg-fedora-xccdf.xml
sudo chown core:core xccdf-report.html ssg-fedora-xccdf-results.xml
Open the HTML file in a web broswer to view the results.
Generate Fixes
oscap
has a wonderful feature to suggest fixes. The following command does this. It does not need to be run as root
. If you prefer a different fix-type, use --help
to learn how.
oscap xccdf generate fix \
--fix-type ansible \
--output playbook-xccdf-fixes.yml \
--profile standard \
ssg-fedora-xccdf-results.xml
Right off the bat, I needed to add to the playbook.
become: yes
vars:
ansible_python_interpreter: '/usr/bin/python3'
These changes allowed the playbook to get started but it wasn’t long before the first task failure. That’s tale for another time.
Generate Guide
oscap xccdf generate guide \
--profile standard \
--output xccdf-guide.html \
ssg-fedora-xccdf-results.xml