“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.

FieldValueMeaning
Minute*/5Every 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.

TypeCommandLocationScope
Userpip install --user~/.local/binCurrent user only
Globalsudo pip install/usr/local/binAll 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-id is 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.


← Previous: Days 1-4 | Next: Days 9-12 →