KodeKloud Days 5-8: SELinux, Cron Jobs, and the Great Ansible Adventure

Table of Contents
“Before going deeper into kernel-level work, I went back to Linux fundamentals. This is what runs under every K8s cluster and eBPF probe â owning it isn’t optional.”
This series is a deliberate return to Linux fundamentals before going deeper into kernel-level work â CentOS, SELinux, Ansible, SSH. The stuff that runs silently under every K8s cluster and eBPF probe. Owning it isn’t optional.
Four problems this week: SELinux configuration on a RHEL-based system, cron job automation, passwordless SSH across multiple servers, and Ansible version management for global availability. All documented here as a reference.
Day 5: SELinux Configuration on CentOS Stream#
The Problem#
The lab required disabling AppArmor, installing SELinux, and configuring it. The catch: the environment was CentOS Stream 9, not Ubuntu. Every tutorial, bootcamp, and lab I’d done before this used Ubuntu. The muscle memory runs deep.
# First attempt
sudo apt update
sudo apt install -y selinux-basics selinux-policy-default auditd
Command not found. Always check your OS before running anything.
cat /etc/os-release
NAME="CentOS Stream"
VERSION="9"
ID="centos"
ID_LIKE="rhel fedora"
RHEL-based. dnf, not apt. Different paths, different package names, different everything.
Resolution#
sudo dnf install -y selinux-policy selinux-policy-targeted policycoreutils
sudo vi /etc/selinux/config
# SELINUX=enforcing â SELINUX=disabled
Verify#
getenforce
sestatus
Expected: Disabled
Why This Matters#
In practice you’ll hit Ubuntu in most cloud and container contexts, and RHEL derivatives in enterprise infrastructure â banks, telecoms, legacy corporate environments. The tooling is different enough that assuming one means breaking the other. Check /etc/os-release before running anything. Always.
Diagnostic reference:
# Identify distro before touching anything
cat /etc/os-release
# Check SELinux state
getenforce
sestatus
# View current policy
cat /etc/selinux/config
Day 6: Cron Job Automation#
The Task#
Echo “hello” to /tmp/cron_text every 5 minutes across three app servers.
Setup#
sudo dnf install -y cronie
sudo systemctl enable --now crond
The Wrong Way#
# Attempted direct write without root
echo "*/5 * * * * echo hello > /tmp/cron_text" >> /var/spool/cron/root
# Permission denied
The Right Way#
sudo su -
echo "*/5 * * * * echo hello > /tmp/cron_text" >> /var/spool/cron/root
crontab -l
Cron Syntax Reference#
*/5 * * * * â every 5 minutes, every hour, every day, every month, every weekday.
| Field | Value | Meaning |
|---|---|---|
| Minute | */5 | Every 5 minutes |
| Hour | * | Every hour |
| Day of month | * | Every day |
| Month | * | Every month |
| Day of week | * | Every weekday |
Common patterns worth keeping:
# Every hour at minute 0
0 * * * * command
# Every day at midnight
0 0 * * * command
# Every Monday at 9 AM
0 9 * * 1 command
# Every 15 minutes
*/15 * * * * command
Use crontab.guru to validate expressions before deploying them.
What a Production Version Needs#
The above works. A production version needs timestamped output, log rotation, and alerting on failure â not a bare echo redirect. This is a lab task; don’t ship lab patterns to production.
Day 7: Passwordless SSH#
The Task#
Passwordless SSH from jump host (thor) to all three app servers. Required before any automation can run unattended.
Setup#
# Generate key pair on jump host
ssh-keygen -t rsa -b 4096
# Copy public key to each server
ssh-copy-id tony@stapp01
ssh-copy-id steve@stapp02
ssh-copy-id banner@stapp03
# Verify â must return without password prompt
ssh tony@stapp01 hostname
When ssh-copy-id Fails#
# Manual fallback
cat ~/.ssh/id_rsa.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Permission Requirements#
SSH silently falls back to password auth if permissions are wrong. This is the failure mode that wastes the most time because there’s no error â it just prompts for a password and you don’t know why.
~/.ssh/ â 700
~/.ssh/authorized_keys â 600
~/.ssh/id_rsa â 600
~/.ssh/id_rsa.pub â 644
Set these before testing. If auth isn’t working, check permissions before anything else.
# Full diagnostic
ls -la ~/.ssh/
namei -l ~/.ssh/authorized_keys
Diagnostic Reference#
# Verbose SSH output â shows exactly where auth fails
ssh -vvv user@host
# Check authorized_keys content
cat ~/.ssh/authorized_keys
# Verify permissions in one pass
stat ~/.ssh ~/.ssh/authorized_keys ~/.ssh/id_rsa
Day 8: Ansible â Version Management and Global Installation#
The Task#
Install a specific version of Ansible and make it available globally across all servers.
Pre-flight#
python3 -m pip -V
If pip isn’t available, install it before proceeding. Don’t assume it’s there.
User vs Global Installation#
# User install â only available to current user
python3 -m pip install --user "ansible==4.8.0"
# Verify location
which ansible
# ~/.local/bin/ansible â not globally available
The lab required global availability. User install doesn’t cover that.
Global Installation#
# Remove user install first
python3 -m pip uninstall -y ansible ansible-core
rm -f ~/.local/bin/ansible ~/.local/bin/ansible-playbook
# Install globally
sudo python3 -m pip install "ansible==4.8.0"
# Verify
which ansible
# /usr/local/bin/ansible
ansible --version
Remove both ansible and ansible-core when uninstalling. ansible depends on ansible-core and pip gets inconsistent state if you only remove one.
| Type | Command | Location | Scope |
|---|---|---|---|
| User | pip install --user | ~/.local/bin | Current user only |
| Global | sudo pip install | /usr/local/bin | All users |
Version Pinning#
Always quote version specifiers:
# Correct
sudo python3 -m pip install "ansible==4.8.0"
# Shell expansion can break this
sudo python3 -m pip install ansible==4.8.0
# Verify installed version
ansible --version
python3 -m pip list | grep ansible
What I’d Do Differently#
- Ansible: Use a dedicated virtualenv per project instead of global pip installs. Global installs create dependency conflicts across tools on the same host.
- SSH key management: In a real environment, use a secrets manager or certificate authority instead of manually copying keys to
authorized_keys.ssh-copy-idis fine for labs, not for fleets. - Cron: Use a proper job scheduler (systemd timers, or a distributed scheduler like Airflow/Temporal) for anything beyond single-host tasks. Bare cron has no visibility, no retry logic, and no alerting.
- SELinux: Don’t disable it in production. Configure the policy to allow what you need. Disabling it is the lab shortcut, not the production answer.
Diagnostic Reference#
# OS identification
cat /etc/os-release
# SELinux
getenforce
sestatus
cat /etc/selinux/config
# Cron
crontab -l
systemctl status crond
grep CRON /var/log/syslog
# SSH auth debugging
ssh -vvv user@host
stat ~/.ssh ~/.ssh/authorized_keys
ls -la ~/.ssh/
# Ansible
ansible --version
python3 -m pip list | grep ansible
which ansible
Tags#
#Linux #Infrastructure #Ansible #SELinux #SSH #Automation #CentOS
About the Author#
Elijah Udom (elijahu) is an Infrastructure & Cloud Engineer based in Lagos, Nigeria. AWS, Kubernetes, eBPF security, AI/ML infrastructure. Building in the open.