<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nnamdi Felix Ibe</title>
    <description>The latest articles on DEV Community by Nnamdi Felix Ibe (@ndcodes).</description>
    <link>https://dev.to/ndcodes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3994627%2F3a0c042d-7d46-4d4d-9a1e-32b1833b0bee.png</url>
      <title>DEV Community: Nnamdi Felix Ibe</title>
      <link>https://dev.to/ndcodes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ndcodes"/>
    <language>en</language>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 8: Ansible's 'ping' Isn't a Ping, and the AWS Stop Flag Reads Backwards</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 11 Jul 2026 06:59:42 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-8-ansibles-ping-isnt-a-ping-and-the-aws-stop-flag-1a0c</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-8-ansibles-ping-isnt-a-ping-and-the-aws-stop-flag-1a0c</guid>
      <description>&lt;p&gt;Ansible's whole pitch is that it manages servers without installing an agent on any of them. That single design choice is most of why it took over, and Day 8 started with getting it running and proving it could actually reach a host.&lt;/p&gt;

&lt;p&gt;Two tasks today. Install Ansible on a Linux control node and confirm it can communicate with a managed server, then enable stop protection for an EC2 instance so nobody can accidentally knock it offline. The tasks come from the KodeKloud Engineer platform, as with the rest of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ansible: install it, then prove it can reach a host
&lt;/h2&gt;

&lt;p&gt;Ansible is a configuration and automation tool. You describe the state you want a server to be in, and it makes the server match. The part that made it win is that it is agentless. There is no daemon to install on the machines you manage, it just needs SSH access and a working Python on the far end. You install it in one place, the control node, and reach everything else from there.&lt;/p&gt;

&lt;p&gt;Ansible is written in Python, so Python and pip come first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update packages, then install Python and pip&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum update &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3 python3-pip

&lt;span class="c"&gt;# Confirm what you have&lt;/span&gt;
python3 &lt;span class="nt"&gt;--version&lt;/span&gt;
pip3 &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install Ansible itself. In the lab, I pinned an exact version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install a specific Ansible version, system-wide&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pip3 &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;ansible&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2.9.27 &lt;span class="nt"&gt;--prefix&lt;/span&gt; /usr/local
ansible &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning the version is the right instinct. An automation tool that silently upgrades underneath you is how a playbook that worked last week breaks today, so naming the version keeps runs reproducible.&lt;/p&gt;

&lt;p&gt;Two honest caveats, because this is where the lab and the real world split.&lt;/p&gt;

&lt;p&gt;First, that version is old. Ansible 2.9 is years behind now. On a fresh setup, you would reach for the current release, which ships as a package called &lt;code&gt;ansible-core&lt;/code&gt;, the modern engine that the RHEL package manager provides in the 2.16 and up range.&lt;/p&gt;

&lt;p&gt;Second, Red Hat does not recommend installing Ansible with &lt;code&gt;sudo pip&lt;/code&gt; system-wide, because pip does not respect what the system package manager owns and can quietly clash with it. The supported routes are &lt;code&gt;sudo dnf install ansible-core&lt;/code&gt; for a system install, or pip inside a Python virtual environment when you want the newest upstream version without touching system packages. The lab wanted the pip method, so that is what I ran, but on anything real, I would use dnf or a venv.&lt;/p&gt;

&lt;p&gt;With Ansible installed, it needs to know what it is managing. That is the inventory file, a plain list of hosts grouped under a label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create an inventory file&lt;/span&gt;
vi inventory

&lt;span class="c"&gt;# [servers]&lt;/span&gt;
&lt;span class="c"&gt;# 192.168.1.10&lt;/span&gt;
&lt;span class="c"&gt;# 192.168.1.11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the moment of truth, the ping module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check Ansible can reach every host in the 'servers' group&lt;/span&gt;
ansible &lt;span class="nt"&gt;-i&lt;/span&gt; inventory servers &lt;span class="nt"&gt;-m&lt;/span&gt; ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the trap hiding in the name. Ansible's ping is not an ICMP ping. It does not bounce a packet off the network. It logs into each host over SSH, checks that a usable Python is there, and returns &lt;code&gt;pong&lt;/code&gt; only if all of that worked. So a green pong is proof of a full working connection, SSH and Python both, not just that the box is reachable. It is a stronger signal than a normal ping, and it fails for different reasons too, usually broken SSH auth or a missing Python rather than a dropped packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  EC2 stop protection: a safety switch with a backwards name
&lt;/h2&gt;

&lt;p&gt;Over on AWS, the task was to protect an important instance from being stopped by accident. AWS calls this stop protection, and turning it on is a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable stop protection&lt;/span&gt;
aws ec2 modify-instance-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-1234567890abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disable-api-stop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that flag twice, because it catches everyone. &lt;code&gt;--disable-api-stop&lt;/code&gt; sounds like it switches stopping off in some harmful way. It is actually the switch that turns protection on. It disables the stop API for that one instance, so the instance cannot be stopped until you remove the protection. To turn it back off later, you use the mirror flag, &lt;code&gt;--no-disable-api-stop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The note I was working from verified this by trying to stop the instance and watching the call fail. That works, but it is a nervous way to check, because if the protection had not actually applied, you would have just stopped your box. The calmer check is to ask AWS directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm protection is on without risking a stop&lt;/span&gt;
aws ec2 describe-instance-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-1234567890abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute&lt;/span&gt; disableApiStop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns true, protection is on, and any &lt;code&gt;stop-instances&lt;/code&gt; call will now be refused with an error instead of quietly taking the instance down.&lt;/p&gt;

&lt;p&gt;A few things worth knowing before you rely on it.&lt;/p&gt;

&lt;p&gt;Stop protection and termination protection are two separate switches. Stop uses &lt;code&gt;--disable-api-stop&lt;/code&gt;, termination uses &lt;code&gt;--disable-api-termination&lt;/code&gt;. Turning on one does nothing for the other, so an instance you want fully locked down needs both.&lt;/p&gt;

&lt;p&gt;It also does not apply everywhere. You cannot enable stop protection on an instance with an instance-store root volume or on a Spot Instance. For the standard EBS-backed on-demand instances most people run, you are covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Both tasks were about trust, pointing in opposite directions. Ansible's ping earns your trust by doing more than the name suggests, a real login instead of a network poke. AWS's stop flag tests your trust by being named the opposite of what it does. The habit that carries you through both is the same one: read what a command actually does instead of what its name implies.&lt;/p&gt;

&lt;p&gt;So here is Day 8's question. Would you rather assume a command does what it sounds like and find out the hard way in production, or spend the extra minute confirming what it really does before you depend on it?&lt;/p&gt;

&lt;p&gt;Day 8 down. Ninety-two to go&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ansible</category>
      <category>aws</category>
      <category>linux</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 7: Kill the SSH Password, and Resize EC2 the Right Way</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Wed, 08 Jul 2026 06:01:24 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-7-kill-the-ssh-password-and-resize-ec2-the-right-way-2m86</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-7-kill-the-ssh-password-and-resize-ec2-the-right-way-2m86</guid>
      <description>&lt;p&gt;A password is a secret you type. An SSH key is a secret you never send. That gap is the whole reason key-based login beats passwords, and Day 7 was where I set it up properly instead of halfway.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Configure passwordless SSH with a key pair, and change the type of an existing EC2 instance. Both are routine. Both have one detail that turns routine into broken if you skip it. The tasks come from the KodeKloud Engineer platform, the same as the rest of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSH keys: log in without ever sending a secret
&lt;/h2&gt;

&lt;p&gt;Key-based SSH uses a pair. A private key that stays on your machine and never leaves it, and a public key you can hand out freely. The server keeps the public key, and when you connect, the two sides prove they match without the private key ever crossing the wire. A password does the opposite, it gets sent to the server on every login, which is one more thing that can be captured, guessed, or brute-forced.&lt;/p&gt;

&lt;p&gt;Start by generating the pair on your own machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a key pair (modern OpenSSH defaults to ed25519)&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare &lt;code&gt;ssh-keygen&lt;/code&gt; works too. On current OpenSSH, it now defaults to ed25519, a fast modern key type, though older setups defaulted to RSA. I name the type with &lt;code&gt;-t ed25519&lt;/code&gt; so there is no guessing about what I ended up with. Accept the default file location, and add a passphrase if you want a second layer of protection on the key itself. Then push the public half to the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy the PUBLIC key to the target server&lt;/span&gt;
ssh-copy-id user@hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ssh-copy-id&lt;/code&gt; handles the fiddly part for you. It appends your public key to &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; on the server and sets the permissions correctly, which SSH is famously strict about. Do this by hand, and one wrong permission bit will make the server silently ignore your key while you wonder why it keeps asking for a password. Now test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should log you straight in, no password prompt&lt;/span&gt;
ssh user@hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the part the task hints at but most people skip. Setting up a key does not disable passwords. Until you actually turn password login off, the server still accepts the weaker method, and anything on the internet can keep hammering it with guesses. Finish the job on the server side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In /etc/ssh/sshd_config, set:&lt;/span&gt;
PasswordAuthentication no

&lt;span class="c"&gt;# Then reload the SSH daemon&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One warning that has locked plenty of people out of their own servers. Confirm your key login works in a second terminal before you disable passwords. If the key is not actually working and you switch passwords off, you have just removed your only way back in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resizing EC2: stop, modify, start, and mind the IP
&lt;/h2&gt;

&lt;p&gt;The AWS task was to change an instance's type, say from a small t2.micro to a heavier t3.medium. You cannot do it live. The instance has to be stopped first because the type is tied to the physical host it runs on, and changing it means the instance gets placed somewhere new. Start by grabbing the ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get the instance ID by its Name tag&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=my-instance"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Reservations[*].Instances[*].InstanceId"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storing the ID in a variable keeps the next few commands clean and stops you from pasting the wrong &lt;code&gt;i-0abc123&lt;/code&gt; halfway through. Then stop it and wait for the stop to finish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop it, then block until it is fully stopped&lt;/span&gt;
aws ec2 stop-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;
aws ec2 &lt;span class="nb"&gt;wait &lt;/span&gt;instance-stopped &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;wait&lt;/code&gt; line is underrated. Instead of running &lt;code&gt;describe-instances&lt;/code&gt; over and over to check whether it stopped yet, the waiter blocks until the state is actually &lt;code&gt;stopped&lt;/code&gt;, then hands control back to your script. It is the difference between automation that works and automation that races ahead and fails on the next line. Now change the type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Change the instance type while it is stopped&lt;/span&gt;
aws ec2 modify-instance-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-type&lt;/span&gt; &lt;span class="s1"&gt;'{"Value": "t3.medium"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target type has to be compatible with the AMI's architecture. Moving between two x86 types, like t2 to t3, is fine. Jumping to a Graviton (ARM) type would need an image built for that architecture, so those are not interchangeable. Start it back up and confirm the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start it again and verify the new type&lt;/span&gt;
aws ec2 start-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;
aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Reservations[*].Instances[*].InstanceType"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the detail that catches people. A stop and start is not a reboot. When you stop an instance and start it again, it usually comes back on a different public IPv4 address, because the old one gets released back into the pool. The private IP inside the VPC stays the same, but any SSH session, DNS record, or firewall rule pinned to the old public IP is now pointing at nothing. If you need a public address that survives a stop and start, that is exactly the problem an Elastic IP solves.&lt;/p&gt;

&lt;p&gt;One more worth knowing. If the instance had any instance-store (ephemeral) volumes, stopping it wipes that data. EBS root volumes are safe, which covers most default setups, but check before you stop something that keeps scratch data on local disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Both tasks look finished the moment the command returns success. They are not. SSH is not hardened until passwords are switched off. A resize is not safe until you have accounted for the public IP that just changed underneath you. The command completing and the job being done are two separate moments, and the gap between them is where real operations actually live.&lt;/p&gt;

&lt;p&gt;So here is Day 7's question. Would you rather run the command and trust that success means safe, or know exactly what changed the second it returns, so nothing surprises you a week later?&lt;/p&gt;

&lt;p&gt;Day 7 down. Ninety-three to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>security</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 6: Cron's Sneaky OR, and the EC2 Key You Only Get Once</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 06 Jul 2026 21:10:28 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-day-6-crons-sneaky-or-and-the-ec2-key-you-only-get-once-5fpp</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-day-6-crons-sneaky-or-and-the-ec2-key-you-only-get-once-5fpp</guid>
      <description>&lt;p&gt;Automation is the part of DevOps that actually earns the title. Day 6 was two of the most everyday automation tasks there are, and both had a trap sitting in plain sight.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Schedule a recurring job with cron, and launch an EC2 instance from the AWS CLI. The tasks come from the KodeKloud Engineer platform if you want the same lab to work through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cron: five fields, one rule that trips everyone
&lt;/h2&gt;

&lt;p&gt;Cron is the scheduler that has quietly run Linux automation for decades. You hand it a job and a schedule, and its daemon, crond, wakes up every minute to check whether anything is due. If that daemon is not running, nothing fires, so the first move is making sure it is installed and enabled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Become root&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;su -

&lt;span class="c"&gt;# Install the cron daemon if it is missing&lt;/span&gt;
yum &lt;span class="nb"&gt;install &lt;/span&gt;cronie &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Enable it at boot, start it now, and confirm it is alive&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;crond.service
systemctl start crond.service
systemctl status crond.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cronie&lt;/code&gt; is the package name on RHEL-family distros. &lt;code&gt;enable&lt;/code&gt; makes the service survive a reboot, &lt;code&gt;start&lt;/code&gt; brings it up right now, and &lt;code&gt;status&lt;/code&gt; is how you confirm it is actually running instead of assuming it is. Now edit the crontab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Edit the current user's crontab&lt;/span&gt;
crontab &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# minute  hour  day-of-month  month  day-of-week   command&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /path/to/script.sh

&lt;span class="c"&gt;# List what is currently scheduled&lt;/span&gt;
crontab &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line runs the script every day at 2am. The five fields, in order, are minute, hour, day of month, month, and day of week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minute: 0 to 59&lt;/li&gt;
&lt;li&gt;hour: 0 to 23&lt;/li&gt;
&lt;li&gt;day of month: 1 to 31&lt;/li&gt;
&lt;li&gt;month: 1 to 12&lt;/li&gt;
&lt;li&gt;day of week: 0 to 7, where both 0 and 7 mean Sunday&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the rule that quietly breaks schedules. When you set both the day-of-month field and the day-of-week field to something other than a star, cron treats them as OR, not AND. So &lt;code&gt;0 0 13 * 5&lt;/code&gt; does not mean midnight on Friday the 13th. It means midnight on every 13th of the month, and also every Friday, whichever lands first. If you actually want the AND, you restrict one field in cron and check the other one inside your script.&lt;/p&gt;

&lt;p&gt;Two more things bite people here.&lt;/p&gt;

&lt;p&gt;Cron does not run with your shell environment. It uses a bare, minimal PATH and never reads your &lt;code&gt;.bashrc&lt;/code&gt; or profile. A command that runs fine in your terminal can fail silently under cron because it cannot find the binary. Use absolute paths, both to your script and to anything the script calls.&lt;/p&gt;

&lt;p&gt;Cron also says nothing when a job fails. By default, it tries to email the output to the local user, which on most boxes goes precisely nowhere. Redirect the output yourself so you have a trail to read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Capture normal output and errors so failures are visible&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /path/to/script.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /var/log/myjob.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Launching EC2: gather first, launch once
&lt;/h2&gt;

&lt;p&gt;The AWS task looks like a single &lt;code&gt;run-instances&lt;/code&gt; command, but the command is the easy part. It fails the instant you feed it an argument it does not have, so the real work is gathering four things first: an AMI ID, a security group ID, a subnet ID, and a key pair.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List the 5 newest Amazon-owned AMIs in the region&lt;/span&gt;
aws ec2 describe-images &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--owners&lt;/span&gt; amazon &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'reverse(sort_by(Images,&amp;amp;CreationDate))[:5].{id:ImageId,date:CreationDate}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AMI IDs are region-specific, and they change often as Amazon publishes new images, so never hardcode one you found in a blog post, this one included. Look up a current ID in your own region every time. Next, find a security group and a subnet to launch into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The firewall (security group) and the network placement (subnet)&lt;/span&gt;
aws ec2 describe-security-groups
aws ec2 describe-subnets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The security group is the instance's firewall, and the subnet decides which VPC and availability zone it lands in. You need one ID from each. Then create the key pair, and this is the step to slow down on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a key pair and save the private key locally&lt;/span&gt;
aws ec2 create-key-pair &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-name&lt;/span&gt; my-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'KeyMaterial'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; my-key.pem

&lt;span class="c"&gt;# Lock the permissions down&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 my-key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS shows you the private key exactly once, at creation. There is no re-download and no recovery. Lose that &lt;code&gt;.pem&lt;/code&gt; and the key pair is dead weight, you delete it and generate a new one. The &lt;code&gt;chmod 400&lt;/code&gt; is not optional either, SSH refuses to use a private key that other users on the box could read. Now launch, feeding in everything you gathered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch the instance&lt;/span&gt;
aws ec2 run-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-id&lt;/span&gt; ami-xxxxxxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--count&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-type&lt;/span&gt; t2.micro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-name&lt;/span&gt; my-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; sg-xxxxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; subnet-xxxxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=my-ec2}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap in the real AMI, security group, and subnet IDs you just looked up. &lt;code&gt;t2.micro&lt;/code&gt; is the small, free-tier-friendly size, &lt;code&gt;--count 1&lt;/code&gt; launches a single instance, and the tag block names it so you are not squinting at raw instance IDs later. Then confirm it came up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify the instance state&lt;/span&gt;
aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=my-ec2"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Reservations[*].Instances[*].{Id:InstanceId,State:State.Name}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right after launch the state reads &lt;code&gt;pending&lt;/code&gt;, then flips to &lt;code&gt;running&lt;/code&gt; within a minute or so. That tag filter is exactly why tagging at launch pays off, you query by name instead of digging for an ID.&lt;/p&gt;

&lt;p&gt;One thing the task does not mention, but your wallet will. A running instance bills until you stop or terminate it. When the lab is done, terminate it. Learning to launch is only half the skill, cleaning up is the other half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Both tasks reward the same habit, and it is not typing speed. Cron punishes you for assuming the schedule means what it looks like. EC2 punishes you for launching before you have gathered what it needs. The people who get burned are the ones who sprint to the final command and skip the boring prep and the fine print.&lt;/p&gt;

&lt;p&gt;So the choice for Day 6. Would you rather memorise the &lt;code&gt;run-instances&lt;/code&gt; line and hope the environment matches, or understand every argument well enough to fix it yourself when it doesn't?&lt;/p&gt;

&lt;p&gt;Day 6 down. Ninety-four to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>beginners</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 5: SELinux Isn't Your Enemy, and GP3 Is Your Default</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 04 Jul 2026 06:42:59 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-day-5-selinux-isnt-your-enemy-and-gp3-is-your-default-55e1</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-day-5-selinux-isnt-your-enemy-and-gp3-is-your-default-55e1</guid>
      <description>&lt;p&gt;Most people meet SELinux for the first time when something breaks, and their first move is to switch it off. I get the reflex. But "disabled" is the laziest of the three options SELinux hands you, and Day 5 of this challenge is where that finally clicked for me.&lt;/p&gt;

&lt;p&gt;Two tasks today, one on Linux and one on AWS. Install and configure SELinux on a RHEL-style box, and create a GP3 EBS volume from the AWS CLI. Both look like one-liners. Both hide a second layer that only shows up when you slow down and ask why. The tasks come from the KodeKloud Engineer platform if you want to follow along on the same setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  SELinux: three modes, and why "off" should be last
&lt;/h2&gt;

&lt;p&gt;SELinux stands for Security-Enhanced Linux, a mandatory access control layer baked straight into the kernel. Regular Linux permissions decide what a user is allowed to do. SELinux determines what a process is allowed to do, even when the user technically has the necessary permissions. That second gate is the entire point.&lt;/p&gt;

&lt;p&gt;Picture a web server that gets compromised. With standard permissions alone, the attacker inherits whatever the web server user can touch. With SELinux in enforcing mode, that same hijacked process is boxed into exactly the files and ports its policy allows, so it cannot wander off across the filesystem. That is a security layer worth understanding before you rip it out.&lt;/p&gt;

&lt;p&gt;Here are the installation steps. On RHEL, CentOS, Rocky and AlmaLinux, these packages are usually present already, but the task wanted it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the SELinux packages&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;selinux-policy selinux-policy-targeted policycoreutils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;policycoreutils&lt;/code&gt; is the package that gives you &lt;code&gt;sestatus&lt;/code&gt;, &lt;code&gt;setenforce&lt;/code&gt; and the rest of the tooling. Next, set the mode permanently by editing the config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Set the mode permanently&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;vi /etc/selinux/config
&lt;span class="c"&gt;# change SELINUX=enforcing to SELINUX=disabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is the source of truth for what the box boots into. The task asked for disabled, so that is what went in. Then confirm the live status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check the current status&lt;/span&gt;
sestatus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the part the one-liner hides. SELinux has three modes, and they are not interchangeable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;enforcing&lt;/code&gt;: rules are applied, violations are blocked and logged. This is what you want in production.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;permissive&lt;/code&gt;: nothing is blocked, but every violation is logged. This is your debugging mode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;disabled&lt;/code&gt;: SELinux is off. No enforcement, no logs, no safety net.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a service suddenly cannot read a file it clearly owns, the instinct is to jump straight to disabled. Fight it. Flip to permissive instead, reproduce the problem, and read the logs it generates. You get your answer without tearing out the whole security layer. Plenty of quickstart guides tell you to disable SELinux just to remove a variable, and that is fine on a throwaway lab box. It is a bad habit to carry into anything real.&lt;/p&gt;

&lt;p&gt;Two things will catch you here.&lt;/p&gt;

&lt;p&gt;First, switching to or from disabled needs a reboot. Editing the config file does not change the running mode on its own. Before you reboot, &lt;code&gt;sestatus&lt;/code&gt; still reports the old state, which fools a lot of people into thinking the command failed.&lt;/p&gt;

&lt;p&gt;Second, going from disabled back to enforcing is not free. The filesystem has to be relabeled so every file gets its SELinux context back. You trigger it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Relabel the whole filesystem on the next boot, then reboot&lt;/span&gt;
&lt;span class="nb"&gt;sudo touch&lt;/span&gt; /.autorelabel
&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip that step, and you can boot into a machine where half your services refuse to start, which is a miserable way to learn what relabeling actually does.&lt;/p&gt;

&lt;h2&gt;
  
  
  GP3: the volume type you should default to
&lt;/h2&gt;

&lt;p&gt;Over on AWS, the task was to create a GP3 EBS volume in a specific availability zone and tag it so you can find it later. One command does it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a 2 GiB GP3 volume in us-east-1a&lt;/span&gt;
aws ec2 create-volume &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--availability-zone&lt;/span&gt; us-east-1a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--volume-type&lt;/span&gt; gp3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--size&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=volume,Tags=[{Key=Name,Value=my-volume}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading it left to right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--availability-zone&lt;/code&gt;: a volume lives in exactly one AZ. It cannot cross zones, ever.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--volume-type gp3&lt;/code&gt;: the current-generation general-purpose SSD.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--size 2&lt;/code&gt;: the size in GiB, not GB. AWS measures and bills EBS in gigabytes, so 2 means 2 GiB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--tag-specifications&lt;/code&gt;: tags the volume at creation time so you are not later hunting for a nameless &lt;code&gt;vol-0a1b2c3d&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That tag block is the fiddly part. The &lt;code&gt;ResourceType=volume&lt;/code&gt; prefix is required, and the quoting matters, so copy it carefully. Then verify the volume exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm it was created&lt;/span&gt;
aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=my-volume"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows the volume with a &lt;code&gt;State&lt;/code&gt; of &lt;code&gt;available&lt;/code&gt;, meaning it is created but not yet attached to anything.&lt;/p&gt;

&lt;p&gt;So why default to GP3 over the older GP2? Performance you can actually reason about. GP3 gives every volume a flat baseline of 3,000 IOPS and 125 MiB/s of throughput no matter how small it is, and it lets you dial those numbers up independently when you need more (per the AWS EBS documentation).&lt;/p&gt;

&lt;p&gt;GP2 ties performance to size. It gives you 3 IOPS per GiB, so a small GP2 volume is also a slow one, and the only way to speed it up is to make it bigger. GP3 breaks that link and usually costs less per GiB on top of it. For most general workloads, GP3 is the sensible default, which is exactly why AWS made it the default.&lt;/p&gt;

&lt;p&gt;One catch, and it rhymes with the SELinux lesson: placement matters. Because this volume is pinned to us-east-1a, you can only attach it to an EC2 instance sitting in that same zone. Get the AZ wrong, and the volume is dead weight until you snapshot it and rebuild it elsewhere. Check your instance's AZ before you create the volume, not after.&lt;/p&gt;

&lt;p&gt;Creating the volume is only step one. The real next move is attaching it with &lt;code&gt;aws ec2 attach-volume&lt;/code&gt;, then formatting and mounting it on the instance. The task stopped at creation, though, so I will keep this honest and stop there too rather than pretend I did more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Both tasks were technically a single command. Both had a second layer that only appears when you ask why instead of copying the line and moving on. SELinux is not a switch to flip off; it is a set of modes with a purpose. GP3 is not a random SSD; it is a deliberate default with numbers behind it. That gap, between running a command and understanding it, is the whole difference between collecting completed tasks and owning the skill.&lt;/p&gt;

&lt;p&gt;So here is the choice I would put to you if you are learning this too. Would you rather rack up a hundred green checkmarks you cannot explain, or ten you could rebuild from memory and defend in an interview?&lt;/p&gt;

&lt;p&gt;Day 5 down. Ninety-five to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>linux</category>
      <category>100daysofdevops</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 4: Permissions That Actually Matter and Why S3 Versioning Shouldn't Be Optional</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 29 Jun 2026 22:04:05 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-day-4-permissions-that-actually-matter-and-why-s3-versioning-shouldnt-be-2b7k</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-day-4-permissions-that-actually-matter-and-why-s3-versioning-shouldnt-be-2b7k</guid>
      <description>&lt;p&gt;Not every mistake breaks a system immediately.&lt;/p&gt;

&lt;p&gt;Some mistakes just sit there quietly until the wrong person gets access, or someone deletes the wrong file, or a deployment goes sideways. Today's tasks were about putting guardrails in place before those moments happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task 1 (Linux): Set Script Execution Permissions
&lt;/h2&gt;

&lt;p&gt;A script isn't useful if it can't be executed. But making everything executable for everyone isn't the answer either. Linux permissions exist so you can decide exactly who gets to do what.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# SSH into the target server via the jump server&lt;/span&gt;
ssh user@hostname

&lt;span class="c"&gt;# Navigate to the script location&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/script

&lt;span class="c"&gt;# Grant execute permission&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;755 /path/to/script.sh

&lt;span class="c"&gt;# Verify permissions&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /path/to/script.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;br&gt;
&lt;code&gt;chmod 755&lt;/code&gt; means the owner gets read, write, and execute permissions, while the group and everyone else can only read and execute.&lt;br&gt;
Running &lt;code&gt;ls -l&lt;/code&gt; should show something like &lt;code&gt;-rwxr-xr-x&lt;/code&gt;. The leading simply tells you it's a regular file.&lt;br&gt;
&lt;code&gt;ls -la&lt;/code&gt; is useful when you also want to see hidden files in the directory.&lt;/p&gt;

&lt;p&gt;One thing I've started appreciating is that Linux permissions aren't just about making something work—they're about limiting what doesn't need to happen. If a script only needs one person to modify it, there's no reason to let everyone else have write access.&lt;/p&gt;

&lt;p&gt;Least privilege sounds like a security buzzword until someone accidentally edits a production script.&lt;/p&gt;
&lt;h2&gt;
  
  
  Task 2 (AWS): Enable Versioning on an S3 Bucket
&lt;/h2&gt;

&lt;p&gt;Deleting the wrong file is inevitable.&lt;/p&gt;

&lt;p&gt;Whether it's human error, a faulty deployment, or an application bug, objects eventually disappear or get overwritten. S3 Versioning exists so those mistakes don't automatically become disasters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check current versioning status&lt;/span&gt;
aws s3api get-bucket-versioning &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket-name

&lt;span class="c"&gt;# Enable versioning&lt;/span&gt;
aws s3api put-bucket-versioning &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket-name &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled

&lt;span class="c"&gt;# Verify&lt;/span&gt;
aws s3api get-bucket-versioning &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;br&gt;
Once versioning is enabled, it can't be completely turned off—it can only be suspended.&lt;br&gt;
Every version of every object is stored separately, so storage costs increase over time.&lt;br&gt;
Versioning is required for S3 Replication and is commonly expected in backup and compliance environments.&lt;/p&gt;

&lt;p&gt;The interesting thing is that versioning doesn't stop people from making mistakes.&lt;/p&gt;

&lt;p&gt;It just gives you a way back.&lt;/p&gt;

&lt;p&gt;Recovery is often more valuable than prevention because nobody gets everything right all the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Day 4 Is Really About
&lt;/h2&gt;

&lt;p&gt;Today's tasks looked unrelated.&lt;/p&gt;

&lt;p&gt;One was about Linux file permissions.&lt;/p&gt;

&lt;p&gt;The other was about object storage.&lt;/p&gt;

&lt;p&gt;But they're solving the same problem: reducing the impact of human error.&lt;/p&gt;

&lt;p&gt;Permissions stop the wrong people from changing things.&lt;/p&gt;

&lt;p&gt;Versioning lets you recover when the right people change the wrong thing.&lt;/p&gt;

&lt;p&gt;Neither feature makes a system more exciting.&lt;/p&gt;

&lt;p&gt;They just make it far more forgiving.&lt;/p&gt;

&lt;p&gt;So here's the question.&lt;/p&gt;

&lt;p&gt;If someone accidentally deleted an important file or modified a production script today, how quickly could you recover?&lt;/p&gt;

&lt;p&gt;The best time to prepare for mistakes is before anyone makes one.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 3: Disabling Root SSH Login and Carving Out a VPC Subnet</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Fri, 26 Jun 2026 18:10:36 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-day-3-disabling-root-ssh-login-and-carving-out-a-vpc-subnet-17a8</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-day-3-disabling-root-ssh-login-and-carving-out-a-vpc-subnet-17a8</guid>
      <description>&lt;p&gt;A default is a decision someone else made for you.&lt;/p&gt;

&lt;p&gt;It's set for convenience, to get you moving fast. That's the opposite of what you want once a system is real and people depend on it. Both of today's tasks change a default on purpose, one in Linux and one in AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task 1 (Linux): Disable Root Login Over SSH
&lt;/h2&gt;

&lt;p&gt;By default, a lot of Linux images let root log in directly over SSH. That's one weak password, or one leaked key away from someone owning the entire machine. So you turn it off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Edit the SSH daemon config&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;vi /etc/ssh/sshd_config

&lt;span class="c"&gt;# Find this line and change it&lt;/span&gt;
&lt;span class="c"&gt;# PermitRootLogin yes  -&amp;gt;  PermitRootLogin no&lt;/span&gt;

&lt;span class="c"&gt;# Restart SSH to apply the change&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart sshd

&lt;span class="c"&gt;# Confirm the setting is live, without restarting again&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sshd &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;permitrootlogin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sshd -T&lt;/code&gt; prints the effective running config. Use it to confirm the change took, instead of trusting that you edited the right line in the right file.&lt;/li&gt;
&lt;li&gt;Make sure a non-root user with sudo exists before you do this. Turn off root login with no other way in, and you've locked yourself out of your own server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've never sat in a hardening review where this wasn't on the list. Not because disabling root is clever. Because the attacks that actually work are rarely clever. They're someone trying to root with a common password against a box that should never have accepted the attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task 2 (AWS): Carve Out a VPC Subnet Without Overlap
&lt;/h2&gt;

&lt;p&gt;Goal: create a subnet inside an existing VPC. What the task is really checking is whether you look at what's already there before you add to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See the VPC you're working in&lt;/span&gt;
aws ec2 describe-vpcs

&lt;span class="c"&gt;# List existing subnets and their CIDR ranges first&lt;/span&gt;
aws ec2 describe-subnets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Subnets[*].{SubnetId:SubnetId, CidrBlock:CidrBlock}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Create the subnet with a range that doesn't overlap&lt;/span&gt;
aws ec2 create-subnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; &amp;lt;VpcId&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cidr-block&lt;/span&gt; 10.0.0.0/24 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=subnet,Tags=[{Key=Name,Value=my-subnet}]'&lt;/span&gt;

&lt;span class="c"&gt;# Verify it landed&lt;/span&gt;
aws ec2 describe-subnets &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=my-subnet"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List existing subnets before you create one. Two subnets with overlapping CIDR ranges are the kind of problem you don't notice now and can't easily explain later, when routing starts behaving in ways that make no sense.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;/24&lt;/code&gt; gives you 256 addresses, 251 usable. AWS reserves 5 in every subnet, so the count never quite matches what you'd expect.&lt;/li&gt;
&lt;li&gt;Tag it at creation. Finding a resource by name later beats hunting through a list of subnet IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subnets look trivial until you're a few environments deep and two of them claim the same range. Address planning is boring on Day 1 and the reason for a war room on Day 200. The check is one command. Run it before, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Day 3 Is Really About
&lt;/h2&gt;

&lt;p&gt;Both tasks took less than a minute of typing. Neither is hard. The skill isn't the command; it's the habit of not accepting what you were handed just because it works out of the box.&lt;/p&gt;

&lt;p&gt;So here's the question for the systems you run right now. How many defaults are still sitting there untouched because nobody stopped to ask whether they should be? Pick one this week and change it on purpose.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>linux</category>
      <category>security</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 2: Expiring Linux Users and Locking Down Security Groups</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Tue, 23 Jun 2026 09:48:23 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-day-2-expiring-linux-users-and-locking-down-security-groups-29lp</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-day-2-expiring-linux-users-and-locking-down-security-groups-29lp</guid>
      <description>&lt;p&gt;Day 2. Not Day 1, not Day 50. Day 2 is the day the motivation from starting has worn off, and the finish line isn't in sight yet. This is where most challenges quietly die.&lt;/p&gt;

&lt;p&gt;So here's mine.&lt;/p&gt;

&lt;p&gt;Same format as yesterday: the Linux task and the AWS task from the session, plus what actually matters about each one once you've done it a few hundred times in production. Today, both tasks come down to the same idea. Controlling access. Who gets in, and from where.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task 1 (Linux): Create a User That Expires on Its Own
&lt;/h2&gt;

&lt;p&gt;The task: create an account that switches itself off on a set date. This is what you set up for a contractor, an auditor, anyone with temporary access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the user with an expiry date (format: YYYY-MM-DD)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;-e&lt;/span&gt; 2026-12-31 username

&lt;span class="c"&gt;# Verify the expiry date is set&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;chage &lt;span class="nt"&gt;-l&lt;/span&gt; username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useradd -e&lt;/code&gt; sets the expiry at the moment you create the account, so there's no follow-up task to remember.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chage -l&lt;/code&gt; lists the account's ageing settings. Confirm &lt;code&gt;Account expires&lt;/code&gt; shows the date you set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The accounts that cause incidents aren't the ones you remember to remove. They're the ones you forget. Setting the expiry at creation means there's nothing to forget. In a regulated environment, a contractor account with no end date is exactly the kind of thing that turns up in an access review with your name next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task 2 (AWS): Create a Security Group and Open One Port
&lt;/h2&gt;

&lt;p&gt;Goal: create a security group and allow a single port in. Here, that's RDP on 3389.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the security group in the default VPC&lt;/span&gt;
aws ec2 create-security-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-name&lt;/span&gt; my-sg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"My security group"&lt;/span&gt;

&lt;span class="c"&gt;# Add an inbound rule. Allow TCP 3389 (RDP) from a specific CIDR&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; &amp;lt;GroupId&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3389 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cidr&lt;/span&gt; x.x.x.x/x

&lt;span class="c"&gt;# Verify the rule was added&lt;/span&gt;
aws ec2 describe-security-groups &lt;span class="nt"&gt;--group-ids&lt;/span&gt; &amp;lt;GroupId&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new security group blocks all inbound traffic and allows all outbound by default. You only add what you want to let in.&lt;/li&gt;
&lt;li&gt;Security groups are stateful. Allow the inbound request and the return traffic is automatic. You don't need a matching outbound rule.&lt;/li&gt;
&lt;li&gt;Scope the CIDR. &lt;code&gt;0.0.0.0/0&lt;/code&gt; on a port like 3389 or 22 means the entire internet can reach it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read enough breach write-ups, and a pattern shows up. There's almost always a security group in the story, opened wider than it needed to be, for a reason nobody can remember. &lt;code&gt;0.0.0.0/0&lt;/code&gt; is convenient on a Friday afternoon and a liability by Monday. Scope it to the address that actually needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Day 2 Is Really About
&lt;/h2&gt;

&lt;p&gt;Both tasks today limit something. The user expires. The port opens to one address, not the whole internet.&lt;/p&gt;

&lt;p&gt;None of it is advanced. All of it is the difference between a system that passes an audit and one that becomes a story other engineers read later.&lt;/p&gt;

&lt;p&gt;Day 3 is already done. If you're documenting your own work, here's my question: do you write down the boring controls, the expiry dates and the scoped CIDRs, or only the clever stuff? Because in my experience, the boring controls are the ones that save you.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>linux</category>
      <category>security</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 1: Linux User Management and AWS Key Pairs</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 20 Jun 2026 21:46:34 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-day-1-linux-user-management-and-aws-key-pairs-3bci</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-day-1-linux-user-management-and-aws-key-pairs-3bci</guid>
      <description>&lt;p&gt;Doing the work and being able to explain the work are two different skills. &lt;br&gt;
I've had the first one for 8 years. I'm building the second one now.&lt;/p&gt;

&lt;p&gt;I'm a Cloud Platform Engineer. AWS, Kubernetes, Terraform, Linux. Regulated &lt;br&gt;
environments, healthcare, production systems. Real experience. Almost zero &lt;br&gt;
public documentation of it. That's the gap I'm closing, starting from Day 1.&lt;/p&gt;

&lt;p&gt;The platform is KodeKloud. Each session gives you tasks across multiple tools. &lt;br&gt;
I'm posting the Linux and AWS tasks here. Here's what I built and what &lt;br&gt;
actually matters about each one.&lt;/p&gt;


&lt;h2&gt;
  
  
  Task 1 (Linux): Create a User with a Non-Interactive Shell
&lt;/h2&gt;

&lt;p&gt;The task was to create a system user that can own processes but cannot log in &lt;br&gt;
interactively. This is what you do for service accounts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# SSH into the private server via the jump server&lt;/span&gt;
ssh user@hostname

&lt;span class="c"&gt;# Switch to root&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;su -

&lt;span class="c"&gt;# Create the user with a non-interactive shell&lt;/span&gt;
useradd username &lt;span class="nt"&gt;-s&lt;/span&gt; /sbin/nologin

&lt;span class="c"&gt;# Verify the user was added&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/passwd | &lt;span class="nb"&gt;grep &lt;/span&gt;username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/sbin/nologin&lt;/code&gt; prevents the user from getting a shell session — they can own processes but cannot log in.&lt;/li&gt;
&lt;li&gt;Always verify by grepping &lt;code&gt;/etc/passwd&lt;/code&gt; — the last field confirms the shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've been doing this in production environments for years. I still verify&lt;br&gt;
every time. Not because I'm unsure. Because in a regulated environment, you&lt;br&gt;
don't assume, you confirm.&lt;/p&gt;


&lt;h2&gt;
  
  
  Task 2 (AWS): Create an EC2 Key Pair via CLI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Generate and register an RSA key pair in AWS EC2 for SSH access to instances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the key pair and save the private key locally&lt;/span&gt;
aws ec2 create-key-pair &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-name&lt;/span&gt; my-key-pair &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-type&lt;/span&gt; rsa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key-format&lt;/span&gt; pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"KeyMaterial"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; my-key-pair.pem

&lt;span class="c"&gt;# Verify the key pair exists in AWS&lt;/span&gt;
aws ec2 describe-key-pairs &lt;span class="nt"&gt;--key-names&lt;/span&gt; my-key-pair
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The private key is returned only once at creation — save it immediately, AWS does not store it.&lt;/li&gt;
&lt;li&gt;Set correct permissions before use: &lt;code&gt;chmod 400 my-key-pair.pem&lt;/code&gt;
I've seen this cause real problems in production environments where the key
wasn't backed up properly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always run chmod 400 my-key-pair.pem after saving it. SSH will refuse to use a key file with open permissions. It won't tell you that's the reason straight away.&lt;/p&gt;

&lt;p&gt;What Day 1 Taught Me That 8 Years Didn't&lt;br&gt;
Nothing here was technically new to me. That's not the point.&lt;/p&gt;

&lt;p&gt;The point is that explaining something clearly, step by step, with the&lt;br&gt;
reasoning, is a skill completely separate from being able to do it. Most&lt;br&gt;
engineers build the doing skill and ignore the explaining skill. I was one&lt;br&gt;
of them.&lt;/p&gt;

&lt;p&gt;Day 2 is already done. If you're running your own DevOps challenge or thinking about starting one, I'd ask you this: would you rather keep building experience silently and have nothing to show for it at the end, or build it loudly and have 100 posts that prove you did the work?&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>aws</category>
      <category>linux</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
