Why you should not trust the 11 nines of durability claims?


TLDR Summary

Ignore the eleven and sometimes 16 nines of durability claims and secure your data like your business depends on it—because it does.

What Is The Claim?

Amazon's S3 FAQ states that the service delivers 99.999999999% durability—known as "11 nines"—meaning a single object is expected to be lost only once every 100 billion years, or one object every 10,000 years if you store 10 million objects. This is a tall claim. But it is echoed by the Google and Microsoft as well. Google (documentation) and Microsoft Azure (documentation) echo these figures—even extending to 16 nines for geo-replicated storage.

Some experts repeat these numbers without fully grasping their theoretical basis, creating a dangerous disconnect between marketing claims and operational realities.

Why It Is A Problem?

When decision makers cut investments based on these marketing buzzwords, they risk designing architectures that could lead to catastrophic business failures. Often, even inexperienced architects trust the marketing without delving deeper—even though detailed documentation tells a more cautious story.

Note: Durability refers to the long-term preservation of data bits, while availability measures the short-term system uptime. Durability is a theoretical construct based on mathematical modeling, not an empirically verifiable outcome.

What Cloud Vendors Actually Recommend

Despite the impressive marketing claims, technical documentation from providers suggests a more cautious approach: - Google Cloud advises maintaining secondary or even air-gapped backups. (Google Cloud Blog) - AWS recommends cross-account and cross-region backups. (AWS Backup Documentation) - Microsoft Azure promotes Geo-Redundant Storage and custom recovery plans.

This raises an important question: Why are these extra precautions necessary if the system is claimed to be so reliable?

Why the Claim Cannot Be Proven

Amazon asserts that if you store 10 million objects with them, you could expect to lose one every 10,000 years—an 11-nines durability claim. However, this is not about availability, which can be measured over shorter time spans; it concerns how reliably data is preserved over time. Measuring this is challenging because you would need to observe the system for thousands of years to validate the claim. Until then, it remains a theoretical concept.

It’s Just A Theoretical Construct

11-nines durability is not something anyone can empirically verify. Observing such durability over even a decade would require storing hundreds of billions of objects, tracking each one, and proving that none were ever silently corrupted, lost, or replaced. Even if this were possible, it assumes that future conditions will remain as ideal as the past. In reality, durability figures come from mathematical models based on assumptions about data replication, integrity checks, and hardware replacement speeds—assumptions that often do not hold true in real-world scenarios.

Real-World Infrastructure Has Real Problems

Real-world storage systems face constant risks. Consider these issues: - Hard Drives Fail:
Industry data shows HDDs have an annualized failure rate (AFR) of 0.5% to over 2% depending on model and age. Seagate lists an AFR of 0.73% for some models (Seagate). A 2007 Google study reported AFRs up to 8.6% by year three (Wikipedia). Backblaze reported a 0.89% lifetime AFR for SSDs (ExtremeTech). - Bit Rot Happens:
Silent corruption from cosmic rays, electrical interference, or magnetic decay can occur over time. CERN estimates that a cosmic ray-induced RAM error occurs monthly in consumer-grade hardware. Filesystems like ZFS use checksumming and scrubbing, but even these measures cannot eliminate all risks. - Software and Firmware Bugs:
For example, Linux’s ext4 filesystem had a bug in 2012 that caused metadata loss, and a 2009 Seagate firmware bug rendered thousands of drives inoperable until patched. - Human Error:
Uptime Institute’s 2022 report indicates that human mistakes cause about 40% of data center outages. GitLab's 2017 incident, where production data was deleted due to backup mismanagement, is a well-known example.

These issues are not rare anomalies—they are persistent risks in real environments. Ignoring them in durability claims leads to a dangerous disconnect between marketing and operational reality.

The Cloud Isn’t Immune

Despite their scale, engineering excellence, and redundancy, cloud providers are not immune to failures: - In April 2011, AWS experienced a major outage in US-East-1 due to a failure in the EBS control plane, which cascaded into EC2 and S3 disruption for major sites like Reddit and Quora (AWS Incident Report). - In December 2021, a replication rule misconfiguration during an AWS internal update led to permanent deletion of S3 objects for several users (The Register). - In March 2020, AWS S3 experienced disruptions in Northern Virginia due to capacity scaling delays, affecting multiple services downstream (AWS Post-Mortem).

Other major cloud vendors have faced similar failures: - Google Cloud suffered a widespread multi-service outage in March 2022 due to a misconfigured quota enforcement system that incorrectly throttled backend traffic (Google Incident Summary). - Microsoft Azure experienced a global outage in March 2021 due to a key rotation error in its identity service, impacting access to Azure Storage, Microsoft 365, and more (Azure RCA).

These incidents prove that no matter how advanced, cloud infrastructure can still fail due to cascading bugs, misconfigurations, and other complexities.

Durability should be auditable, testable, and independently observable. If a claim cannot be measured or verified by external observation, it is not reliable enough to base critical systems on. Trusting such unprovable assurances is a gamble no responsible architect or decision-maker should take.

What Should Be the Course of Action?

No amount of marketing, modeling, or replication eliminates the need for sound, verifiable engineering practices. Durability claims may sound convincing, but real-world protection comes from disciplined data management. Here’s how to approach it: - Start with Business Impact Analysis:
Not all data is equal—some data is irreplaceable, while other datasets can be reconstructed. Classify your data and tailor your protection efforts accordingly. - Follow the 3-2-1 Backup Strategy:
This proven method—three copies, two media types, and one offsite backup—is still the most effective way to ensure data availability even in the face of disaster. - Introduce Physical and Logical Air Gaps:
Segment critical backups across isolated accounts, regions, or infrastructure. Offline or air-gapped backups protect against ransomware, human error, and cascading software bugs. - Test Like It’s Production:
Backups are useless if you can’t restore them. Conduct regular recovery drills, simulate failures, and validate backup integrity through checksum comparisons or automated verification systems.

Redundancy is not Resilience

High availability features and replication won’t save your data from systemic bugs or operator error. Only operational discipline and measurable protections can.

Final Thoughts

I’m not saying S3 is unreliable. Far from it—I use it and trust it for many workloads. However, I do not believe in the 11-nines claim as a sole measure of durability. It remains a useful aspirational target, but without empirical verification, it is simply a mathematical model rather than a proven guarantee.

If something sounds too good to be true, it probably is.

Understanding REST APIs through a Metaphor


What is a REST API?

REST - Representational State Transfer is a software architectural style that was created to describe the design and guide the development of the architecture for the World Wide Web.
- It is an architectural style for designing networked applications.
- It uses HTTP methods to perform CRUD operations (Create, Retrieve, Update, Delete).
- Data is commonly exchanged in JSON or XML formats.


The Metaphor

Let's understand it through a metaphor. Imagine you own an automated clothes store with shelves of clothes, especially shirts. At the entrance, there’s a Robot that listens to your commands in a special language called REST. Let's conveniently call it Robo. To make the scenario more realistic, imagine that this robot doesn’t act on its own but delegates tasks to smaller, specialized robots called API Endpoint Functions.

The Robo will follow your instructions as long as they are valid. In the world of REST, these instructions are given using HTTP methods, which act like action words or verbs in a sentence.

Now, as dealing with HTTP Verbs is not very convenient, they are exposed to you through a website in a Web Browser.

[!NOTE]
When a link is clicked, a form is submitted, or a button is clicked, the browser translates it into an HTTP Verb and sends it to Robo.


Interacting with our REST Robo

In this section, we will explore how REST APIs work by simulating interactions with our REST Robo. Each interaction demonstrates a specific HTTP method and its corresponding operation on the store's inventory. These examples will help you understand how REST APIs handle requests and responses in a structured and predictable manner.

Interaction 1: Listing All Shirts with GET

To decide what to update, I should first know what I have in the store. I want to know how many shirts are in the store. To do this, I need to tell it to get all the shirts. For this, I click on "list all shirts." The browser translates the click into a GET Verb and sends it to Robo.

Browser: Hey Robo, give me all the shirts.

GET /shirts

Robo: Wait here for a moment, please, while I see what I can do.

Robo searches through its list of API Endpoint Functions and finds a match in the get_shirts API Endpoint. It assigns the task and waits for a response.
When the response is ready:

Robo: Here are all the shirts you asked for.

STATUS: 200 OK

{"shirts": [
  "shirt 1",
  "shirt 2",
  ...]}

The browser converts the list into the UI and displays it.

In case no match was found, it would have returned an error code:

STATUS: 404 NOT FOUND

This completes our first interaction.


Interaction 2: Getting Details of a Single Shirt with GET/shirt_id

Now that I have all the shirts, I can decide which one to replace. I notice the blue shirt with the old design. I want to know more details about it. I click on it.

Browser: Hey Robo, get me the third shirt from the list.

GET /shirt/3

Robo: Happy to help; please wait a moment.

This time, Robo matches the request to the get_shirt API Endpoint, which needs to know the shirt number to fetch the details. It goes out, gets all the details of shirt number 3, and hands them back to Robo.

Robo: Here are all the details for shirt 3 that you asked for.

STATUS: 200 OK

{"id": 3,
 "color": "blue",
 "size": "XXL"}

The browser takes the details and displays them.

But suppose get_shirt encountered an error and didn't get the details, then Robo would have returned:

STATUS: 404 NOT FOUND

This completes our second interaction.


Interaction 3: Creating a New Shirt with POST

Browser: Hey Robo, I got a new shirt; add it to your inventory.

POST /shirts
{
  "color": "red",
  "size": "M"
}

Robo: Let me add that new shirt for you. Please wait a moment...
Robo: New shirt added successfully.

STATUS: 201 Created

{"id": 5, "color": "red", "size": "M"}

The browser displays the new shirt in the inventory.


Interaction 4: Replace an Existing Shirt with PUT

Browser: Hey Robo, replace the fourth shirt with this new one.

PUT /shirts/4
{
  "color": "yellow",
  "size": "L"
}

Robo: Replacing the fourth shirt. Please wait a moment...
Robo: Shirt replaced successfully.

STATUS: 200 OK

{"id": 4, "color": "yellow", "size": "L"}

The browser shows the updated shirt.


Interaction 5: Updating a Shirt's Attribute with PATCH

Browser: Hey Robo, update the size of shirt 2 to XL.

PATCH /shirts/2
{
  "size": "XL"
}

Robo: Updating shirt 2 with the new size. Please hold on...
Robo: Shirt updated successfully.

STATUS: 200 OK

{"id": 2, "color": "blue", "size": "XL"}

The browser displays the updated details for shirt 2.


Interaction 6: Removing a Shirt with DELETE

Browser: Hey Robo, this shirt is out of fashion. Remove shirt 3.

DELETE /shirts/3

Robo: Removing shirt 3. Please wait...
Robo: Shirt removed successfully.

STATUS: 200 OK

The browser shows that shirt 3 has been removed from the inventory.


Core Concepts

REST APIs are built on a few fundamental concepts. Let's summarize them here to reinforce what we've learned:

Resources
  • Everything in REST is a resource (e.g., shirts, Posts, Products)
  • Each resource is identified by a unique URL
HTTP Methods

As we saw already, HTTP methods allow us to interact with REST API endpoints. Let's summarize them in one place now.

Method Endpoint Description
GET /shirts Retrieve all shirts
GET /shirts/{id} Retrieve shirt by ID
POST /shirts Create a new shirt
PUT /shirts/{id} Update entire shirt info
PATCH /shirts/{id} Modify part of shirt info
DELETE /shirts/{id} Delete a shirt

What Happens When You Ask?

If the robot understands, it will tell you:
- 200 OK - Request was successful
- 201 Created - Resource was successfully created
- 204 No Content - Update was successful, but no data is returned
- 400 Bad Request - Client sent an invalid request
- 401 Unauthorized - Authentication required
- 403 Forbidden - Insufficient permissions
- 404 Not Found - Resource not found
- 500 Internal Server Error - Server encountered an error


REST API Best Practices

  • Use meaningful resource names (e.g., /shirts instead of /getshirts)
  • Follow a consistent URL structure
  • Use appropriate HTTP methods
  • Implement authentication & authorization (e.g., JWT, OAuth)
  • Support pagination & filtering for large datasets
  • Return useful error messages with proper status codes

Conclusion

Through the metaphor of the automated clothes store and the REST Robo, we have explored how REST APIs function in a real-world scenario. Each interaction demonstrated the use of HTTP methods to perform specific operations, such as retrieving, creating, updating, or deleting resources. By understanding these interactions, you can better appreciate the simplicity and power of REST APIs in building scalable and efficient web applications. Remember, the key to mastering REST lies in understanding its core principles and adhering to best practices.

Linux tip: Sort directories by size.


Sorting Directories by Size in Linux

I often need to clean-up so natually I need to list directories in the current directory, sorted by size from largest to smallest. But since I keep forgetting the command, I am putting it here so that I can remember it later.:

du -h --max-depth=1 | sort -hr
  • du -h --max-depth=1
    The du (disk usage) command is used to estimate file space usage. The -h flag makes the output human-readable (e.g., in KB, MB, GB). The --max-depth=1 option limits the depth of the directory tree that du will traverse to current directory. It passes the output to the sort command.

  • sort -hr The sort command sorts lines of text. The -h flag sorts the numbers in human-readable format, and the -r flag sorts the results in reverse order (largest to smallest).

Seamless History Backup with Git, Cron, and Stow


If you’re tired of manually backing up your command history or reconfiguring your environment on every new machine, this setup might be just what you need. In this post, I describe how I automated my bash history management using Git, cron jobs, and GNU stow—resulting in a solution that is both robust and easily portable.


1. The Problem

Maintaining an accurate, version-controlled record of every command you run is invaluable for troubleshooting, auditing, or retracing your steps. However, several challenges make this difficult:

  • Custom History Storage: The default bash history file (~/.bash_history) is shared across sessions and lacks context, making it hard to track history on a per-machine basis.
  • Version Control: Without version control, it’s nearly impossible to track changes or recover previous states of your command history.
  • Automated Backups: Manual backups are cumbersome and error-prone—you need a system that automatically commits and pushes changes.
  • Configuration Portability: Rebuilding your environment on a new machine often means painstakingly restoring multiple configuration files.

In short, critical work is often lost when history is overwritten or machines are reformatted. I wanted a one-click solution that saves and pushes history file named based on workstation and OS to a remote Git repository. If the repository doesn’t exist locally, it should clone it automatically, then commit history every minute and push every five minutes, all while ensuring the configuration is automatically restored on new machines.

2. The Solution and Its Benefits

A. The Core: .history_manager.sh

This is the heart of the solution. Stored in the bash folder of my dotfiles repository and sourced by my .bashrc, it consolidates all history-related configurations, repository setup, and cron job installation. It also logs errors to help with troubleshooting.

File: .history_manager.sh

#!/bin/bash
# .history_manager.sh
#
# This script configures a custom bash history file, initializes a Git repository in ~/history_logs,
# and installs cron jobs to auto-commit and auto-push the history.
# Errors are logged to $HOME/history_logs/history_manager.log for troubleshooting.

# Log file for error reporting
LOGFILE="$HOME/history_logs/history_manager.log"
mkdir -p "$HOME/history_logs"

log_error() {
    echo "$(date): $1" >> "$LOGFILE"
}

# Function to configure custom history settings
configure_history() {
    # Avoid duplicate lines and lines starting with a space
    HISTCONTROL=ignoreboth:erasedups
    shopt -s histappend cmdhist checkwinsize

    # Set large history sizes
    HISTSIZE=10000
    HISTFILESIZE=999999999
    HISTTIMEFORMAT="%h %d %H:%M:%S $(tty) "
    HISTIGNORE="ls:ps:history"

    # Set hostname and OS details
    HOST=$(hostname)
    OS_ID=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
    case $OS_ID in
        ubuntu) OS="Ubuntu" ;;
        rhel) OS="RHEL" ;;
        kali) OS="Kali" ;;
        *) OS=$OS_ID ;;
    esac

    # Configure custom history file location
    HISTORY_DIR="$HOME/history_logs"
    mkdir -p "$HISTORY_DIR"
    HISTORY_FILE="${HISTORY_DIR}/${HOST}-${OS}.history"

    export HISTFILE="$HISTORY_FILE"
    [ -f "$HISTFILE" ] && history -r "$HISTFILE"
    export PROMPT_COMMAND="history -a; ${PROMPT_COMMAND}"
}

# Function to set up the Git repository
setup_repository() {
    HISTORY_DIR="$HOME/history_logs"
    if [ ! -d "$HISTORY_DIR" ]; then
        echo "Cloning history_logs repository..."
        git clone "$REMOTE_REPO" "$HISTORY_DIR" || { log_error "Failed to clone repository"; return 1; }
    fi

    cd "$HISTORY_DIR" || { log_error "Cannot change directory to $HISTORY_DIR"; return 1; }
    if [ ! -d ".git" ]; then
        git init >/dev/null 2>&1 || log_error "Git init failed"
        [ -n "$REMOTE_REPO" ] && git remote add origin "$REMOTE_REPO" >/dev/null 2>&1 || log_error "Git remote add failed"
    else
        if [ -n "$REMOTE_REPO" ] && [ -z "$(git remote)" ]; then
            git remote add origin "$REMOTE_REPO" >/dev/null 2>&1 || log_error "Git remote add failed"
        fi
    fi
}

# Define remote repository URL
REMOTE_REPO="git@gitlab-personal:sudhirchauhan/history_logs.git"

# Run configuration and repository setup
configure_history
setup_repository

# Install cron jobs for auto-commit and auto-push
source "$HOME/history_logs/scripts/install_history_commit_cronjobs.sh"

B. Version Management Scripts

These scripts, located in ~/history_logs/scripts, manage Git operations:

  • auto_commit.sh: Stages and commits all .history files every minute.
  • auto_push.sh: Pushes commits to the remote repository every five minutes.

Both scripts use flock to prevent overlapping executions and log errors if issues arise.

File: auto_commit.sh

#!/bin/bash
# auto_commit.sh
#
# This script stages and commits all .history files in ~/history_logs.
# It runs via cron every minute and logs errors to the history_manager.log.

(
  flock -n 200 || exit 1
  cd "$HOME/history_logs" || exit 1
  git add *.history
  if ! git diff-index --quiet HEAD --; then
      git commit -m "Auto commit: $(date)" >/dev/null 2>&1 || echo "Git commit error" >> "$HOME/history_logs/history_manager.log"
  fi
) 200<>/tmp/history_manager_commit.lock

File: auto_push.sh

#!/bin/bash
# auto_push.sh
#
# This script pushes commits from ~/history_logs to the remote repository.
# It runs via cron every five minutes and logs errors to the history_manager.log.

(
  flock -n 201 || exit 1
  cd "$HOME/history_logs" || exit 1
  git push >/dev/null 2>&1 || echo "Git push error" >> "$HOME/history_logs/history_manager.log"
) 201<>/tmp/history_manager_push.lock

C. Cron Manager Script

This script automatically installs cron jobs for the above scripts. It adds entries to run the commit script every minute and the push script every five minutes.

File: install_history_commit_cronjobs.sh

#!/bin/bash
# install_history_commit_cronjobs.sh
#
# This script installs cron jobs to run auto_commit.sh every minute
# and auto_push.sh every five minutes.

COMMIT_SCRIPT="$HOME/history_logs/scripts/auto_commit.sh"
PUSH_SCRIPT="$HOME/history_logs/scripts/auto_push.sh"

CRON_ENTRY_COMMIT="* * * * * /bin/bash \"$COMMIT_SCRIPT\""
CRON_ENTRY_PUSH="*/5 * * * * /bin/bash \"$PUSH_SCRIPT\""

TMP_CRON="$(mktemp)"
crontab -l 2>/dev/null > "$TMP_CRON"

grep -F "$COMMIT_SCRIPT" "$TMP_CRON" >/dev/null 2>&1 || echo "$CRON_ENTRY_COMMIT" >> "$TMP_CRON"
grep -F "$PUSH_SCRIPT" "$TMP_CRON" >/dev/null 2>&1 || echo "$CRON_ENTRY_PUSH" >> "$TMP_CRON"

crontab "$TMP_CRON"
rm "$TMP_CRON"

echo "Cron jobs installed:"
echo "  $CRON_ENTRY_COMMIT"
echo "  $CRON_ENTRY_PUSH"

D. Directory Structure Overview

Below is an example of how my dotfiles repository is organized with GNU stow:

dotfiles/
├── bash/
│   ├── .bashrc
│   └── .history_manager.sh
└── README.md

To restore your configuration on a new machine, simply run:

stow bash

This command creates symbolic links from the files in the bash folder to the appropriate locations in your home directory.

Benefits of This Setup

  • Automatic and Versioned: Every command is automatically archived and versioned, providing a complete timeline of your terminal activity.
  • Easy Restoration: GNU stow makes transferring your configuration to a new system effortless—just run stow bash and your settings are restored.
  • Minimal Manual Intervention: Cron jobs handle the automated commits and pushes, so you can focus on your work.
  • Scalable Across Machines: Each machine maintains its own history file while all history files are centrally managed in a Git repository.
  • Enhanced Error Reporting: Errors are logged to a dedicated file, simplifying troubleshooting.

3. Closing Thoughts

This setup elegantly addresses the challenges of maintaining a robust, version-controlled command history while ensuring your configuration is easily portable. By leveraging Git for version control, cron for automation, and GNU stow for managing dotfiles, you can create an environment where every command is safely archived and your personal settings are restored with minimal effort.

If you're looking for a reliable, automated way to back up your bash history and simplify configuration restoration, give this solution a try. It’s a scalable, well-documented system that saves you time and reduces the risk of losing critical work.

Automating My Dotfile Restoration with a Python Script


I love using GNU Stow to manage my dotfiles. It keeps everything neat and organized in my Git repository, making it easy to sync configurations across different machines. But as my list of dotfile packages grew, restoring them manually became a pain. Having to stow each package one by one felt tedious, especially after setting up a new system.

So, I decided to automate the process with a Python script that restores my dotfiles quickly and interactively. I also wanted the flexibility to choose which packages to restore instead of blindly applying everything.

Initially, I thought about just writing a simple loop to stow everything, but I realized I needed more control. Some dotfiles belong in ~/.config/, while others go directly into ~. A single command to restore everything wouldn't work unless I had a way to handle different target directories. That's where my script comes in. It allows me to:
- List available Stow packages from my repository.
- Select which packages to restore interactively.
- Choose between global and local restore modes to control where files are linked.

How the script works

The script takes three main parameters:
1. stow_dir – The directory where all my Stow packages are stored.
2. target_dir – The directory where the selected packages should be restored.
3. global_target – A mode that determines whether all packages use the same --target-dir or if I should be prompted for each package.

Two restoration modes

Script also support two restoration modes. In the global mode all selected packages are restored in a single --target-dir. In the local mode script asks for the target directory for each restore.

Global Mode - One Target Directory for All Packages

When global mode is enabled using --global-target, all selected packages are restored to --target-dir. This is great when all configurations belong in the same place, like ~/.config/.

Local Mode - Different Target Directories per Package

When global mode is disabled (i.e., not using --global-target), the script asks me where to place each package. If I don’t specify a directory, it defaults to ~.

The final script

Fully functional script is as follows:

import os
import subprocess
import argparse

def list_stow_packages(stow_dir):
    """List available Stow packages in the given directory."""
    return [pkg for pkg in os.listdir(stow_dir) if os.path.isdir(os.path.join(stow_dir, pkg))]

def restore_packages(stow_dir, target_dir, use_global_target):
    """Interactively restore selected Stow packages."""
    packages = list_stow_packages(stow_dir)

    if not packages:
        print("No Stow packages found.")
        return

    print("Available Stow packages:")
    for idx, pkg in enumerate(packages, 1):
        print(f"{idx}. {pkg}")

    selected_indices = input("Enter the numbers of packages to restore (comma-separated): ").strip()

    try:
        selected_indices = [int(i) for i in selected_indices.split(',') if i.strip().isdigit()]
    except ValueError:
        print("Invalid input. Exiting.")
        return

    selected_packages = [packages[i - 1] for i in selected_indices if 0 < i <= len(packages)]

    if not selected_packages:
        print("No valid packages selected.")
        return

    for pkg in selected_packages:
        pkg_target_dir = target_dir if use_global_target else input(f"Enter the target directory for {pkg} (default: {target_dir}): ").strip() or target_dir
        print(f"Restoring {pkg} to {pkg_target_dir}...")
        cmd = ["stow", "-t", pkg_target_dir, pkg]
        subprocess.run(cmd, cwd=stow_dir)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Interactive GNU Stow package restoration.")
    parser.add_argument("stow_dir", nargs="?", default=os.getcwd(), help="Path to the Stow directory (default: current directory)")
    parser.add_argument("--global-target", action="store_true", help="Use a single target directory for all packages")
    parser.add_argument("--target-dir", default=os.path.expanduser("~"), help="Global target directory (default: ~)")

    args = parser.parse_args()

    if not os.path.isdir(args.stow_dir):
        print("Invalid Stow directory.")
    elif args.global_target and not os.path.isdir(args.target_dir):
        print("Invalid global target directory.")
    else:
        restore_packages(args.stow_dir, args.target_dir, args.global_target)

Running the Script

With only 3 arguments ,script is simple to use:

python stow_restore.py <stow_directory> [--global-target] [--target-dir <directory>]

Sample Runs

Example 1: Restore All Packages to One Directory
python stow_restore.py ~/.dotfiles --global-target --target-dir ~/.config

This restores all selected dotfiles to ~/.config/ without asking where to put them.

Example 2: Select a Directory for Each Package
python stow_restore.py ~/.dotfiles

With this, the script asks where to place each selected package. If I just hit Enter, it defaults to ~.

Example 3: Restoring Only Specific Packages
python stow_restore.py ~/.dotfiles --global-target --target-dir ~/dotfiles

I pick only the packages I want, and they all go into ~/dotfiles/.

Conclusion

This script has completely changed how I manage my dotfiles. Now, I don’t have to manually restore each package, and I can easily choose which ones to apply. It’s simple, interactive, and flexible—exactly what I needed!

How to Maintaining Python environments with Ansible?


A while ago, I settled on pyenv to centralize my Python virtual environment management. I documented the setup in my previous post. However, when I had to configure a new machine, I realized I was repeating the same steps manually. To save time and maintain consistency, I decided to automate this setup using Ansible.

Automation ensures that the configuration remains consistent across systems and idempotent—meaning no matter how many times I run it, the system always ends up in the same state.

Before diving into the Ansible playbook, let’s first discuss why scripts alone are not the best approach.


Why Not Just Use a Script?

Shell scripts don’t inherently track system state, which leads to several issues:

  • Running the script multiple times can cause conflicts such as duplicate environment variables or broken installations.
  • Failures leave the system in an inconsistent state which often can requiring manual intervention.
  • Handling edge cases means adding more logic making the script harder to maintain and difficult to write.

A configuration management tool like Ansible eliminates these problems by ensuring the system reaches the desired state automatically and idempotently.


Why Ansible?

Idempotency means applying a change only if it hasn’t been applied before. Running the same Ansible playbook multiple times won't introduce unintended modifications. This makes Ansible superior to traditional shell scripts for system configuration.

Now, let’s look at my first version of the playbook.


Initial Ansible Playbook for pyenv Setup

Here’s my first attempt at automating the installation of pyenv and pyenv-virtualenv using Ansible:

- name: Setup Pyenv and Pyenv-Virtualenv
  hosts: localhost
  become: yes
  tasks:
    - name: Install system dependencies
      apt:
        name:
          - git
          - curl
          - build-essential
          - libssl-dev
          - zlib1g-dev
          - libbz2-dev
          - libreadline-dev
          - libsqlite3-dev
          - wget
          - llvm
          - libncurses5-dev
          - xz-utils
          - tk-dev
          - libffi-dev
          - liblzma-dev
          - python3-venv
        state: present
        update_cache: yes

    - name: Clone pyenv repository
      git:
        repo: "https://github.com/pyenv/pyenv.git"
        dest: "{{ ansible_env.HOME }}/.pyenv"
        update: no

    - name: Clone pyenv-virtualenv repository
      git:
        repo: "https://github.com/pyenv/pyenv-virtualenv.git"
        dest: "{{ ansible_env.HOME }}/.pyenv/plugins/pyenv-virtualenv"
        update: no

    - name: Source pyenv configuration in .bashrc
      lineinfile:
        path: "{{ ansible_env.HOME }}/.bashrc"
        line: '[[ -f ~/.pyenv_config ]] && source ~/.pyenv_config'
        insertafter: EOF
        state: present

Issues in This Playbook

This playbook had two major issues:

1. Installing pyenv as Root Instead of the Current User

Since become: yes was applied globally, Ansible executed all tasks as root. This meant pyenv was installed in /root/.pyenv instead of the actual user’s home directory.

2. Duplicate .bashrc Modifications

Each run of this playbook appended the sourcing command to .bashrc, leading to redundant lines.


2. Sourcing Pyenv in .bashrc

Another tricky idempotency problem arose when trying to ensure pyenv was sourced in .bashrc. Since this code block simply inserts the following line at the end of .bashrc without checking for previous entries, idempotency was lost:

    - name: Source pyenv configuration in .bashrc (if not already sourced)
      lineinfile:
        path: "{{ ansible_env.HOME }}/.bashrc"
        line: '[[ -f ~/.pyenv_config ]] && source ~/.pyenv_config'
        insertafter: EOF
        state: present

This was fixed using blockinfile with a marker, which tracks the previous entry and only updates .bashrc conditionally:

    - name: Ensure Pyenv configuration is added to .bashrc with markers
      blockinfile:
        path: ~/.bashrc
        marker: "# START PYENV CONFIGURATION"
        insertafter: EOF
        block: |
          # Pyenv configuration
          [[ -f ~/.pyenv_config ]] && source ~/.pyenv_config
Benefits of blockinfile with marker over lineinfile
  1. Prevents duplication: Unlike lineinfile, which only inserts a single line, blockinfile ensures that an entire block is managed as a unit, preventing redundant insertions.
  2. Easier maintenance: Using markers allows easy identification and modification of configurations later.
  3. Ensures idempotency: blockinfile keeps track of whether the block has already been added, avoiding unnecessary modifications.

Final Ansible Playbook

- name: Install and Configure Pyenv
  hosts: localhost
  tasks:
    - name: Install system dependencies
      become: yes
      apt:
        name:
          - git
          - curl
          - build-essential
          - libssl-dev
          - zlib1g-dev
          - libbz2-dev
          - libreadline-dev
          - libsqlite3-dev
          - wget
          - llvm
          - libncurses5-dev
          - xz-utils
          - tk-dev
          - libffi-dev
          - liblzma-dev
          - python3-venv
        state: present
        update_cache: yes

    - name: Clone pyenv repository
      git:
        repo: "https://github.com/pyenv/pyenv.git"
        dest: "{{ ansible_env.HOME }}/.pyenv"
        update: no

    - name: Clone pyenv-virtualenv repository
      git:
        repo: "https://github.com/pyenv/pyenv-virtualenv.git"
        dest: "{{ ansible_env.HOME }}/.pyenv/plugins/pyenv-virtualenv"
        update: no

    - name: Ensure Pyenv configuration is added to .bashrc
      blockinfile:
        path: ~/.bashrc
        marker: "# START PYENV CONFIGURATION"
        insertafter: EOF
        block: |
          # Pyenv configuration
          export PYENV_ROOT="$HOME/.pyenv"
          command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
          eval "$(pyenv init --path)"
          eval "$(pyenv init -)"
          eval "$(pyenv virtualenv-init -)"

This final playbook ensures a clean, idempotent, and repeatable setup for managing Python environments with pyenv using Ansible. Thanks for reading this far.

Centralising the python Virtual Environment Management with pyenv and plugin


Best practice for Python development is to create virtual environments, ideally one per project, but this often leads to scattered environments across your system. With pyenv and its plugin pyenv-virtualenv, you can centralize all environments in one location. Pyenv is a powerful tool for managing multiple Python versions, while pyenv-virtualenv extends it by enabling seamless virtual environment creation and management. This combination keeps your Python environments well-organized, ensures they are tied to specific Python versions, and even allows automatic activation based on the project directory, making development simpler and more efficient. Here's how to set it up:


Install and Configure pyenv and pyenv-virtualenv

1. Install System Package for Virtual Environments

First, install the required venv package for your system's default Python version:

# Get the default Python version from the system
PYTHON_VERSION=$(python3 --version | awk '{print $2}' | cut -d. -f1,2)

# Install the appropriate package
sudo apt install -y python${PYTHON_VERSION}-venv

2. Clone pyenv and pyenv-virtualenv

Clone both pyenv and the pyenv-virtualenv plugin:

# Clone pyenv
git clone https://github.com/pyenv/pyenv.git ~/.pyenv

# Clone pyenv-virtualenv plugin
git clone https://github.com/pyenv/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv

3. Update Shell Configuration

Modify your shell configuration files (~/.bashrc, ~/.profile, or ~/.bash_profile) to include the necessary environment variables and initialization commands. Use the following commands based on your shell configuration:

Add to ~/.bashrc:
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc 
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc 
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
Add to ~/.profile (if ~/.bashrc is not sourced):
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init -)"' >> ~/.profile
Add to ~/.bash_profile (alternative to ~/.bashrc):
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

4. Reload Shell

To apply the changes, reload your shell:

source ~/.bashrc  # Or source the relevant file, e.g., ~/.profile or ~/.bash_profile

Using the pyenv to manage the environment

Create a Virtual Environment

Use pyenv virtualenv to create a new virtual environment tied to a specific Python version:

pyenv install 3.11.6  # Install the desired Python version
pyenv virtualenv 3.11.6 myenv  # Create a virtual environment named 'myenv'
List Virtual Environments

View all available virtual environments:

pyenv virtualenvs
Activate a Virtual Environment

Manually activate a virtual environment:

pyenv activate myenv
Deactivate a Virtual Environment

Deactivate the currently active virtual environment:

pyenv deactivate
Delete a Virtual Environment

Remove a virtual environment:

pyenv uninstall myenv
Set a Local Environment (Auto-Activate)

To associate a virtual environment with a specific project directory, navigate to the project and set the local environment:

cd /path/to/project
pyenv local myenv

This creates a .python-version file in the project directory. The virtual environment will auto-activate whenever you enter the directory.


Verification

  • Verify the installed pyenv version: bash pyenv --version
  • Verify pyenv-virtualenv integration: bash pyenv virtualenvs

how to lock down WinRM service?


Additional Group Policy Settings for Enabling WinRM over HTTPS

To ensure a secure and robust configuration for enabling Windows Remote Management (WinRM) over HTTPS, additional Group Policy settings can be configured. These settings will further enhance security, improve compatibility, and provide more control over the remote management environment.

Below are the additional GPO settings that can be applied:

Step 1: Configure GPO for WinRM Client
  1. Open Group Policy Management Console (GPMC):
  2. Press Win + R, type gpmc.msc, and press Enter.

  3. Edit the Existing GPO:

  4. Right-click the GPO created earlier ("Enable WinRM with HTTPS and Auto-Enrollment") and select Edit.

  5. Navigate to WinRM Client Settings:

  6. Go to Computer Configuration > Policies > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Client.

  7. Set Trusted Hosts:

  8. Double-click on "Trusted Hosts".
  9. Set it to Enabled.
  10. Add the names or IP addresses of the computers that are allowed to connect using WinRM. Use wildcards (e.g., *.domain.com) to specify multiple hosts.

  11. Specify Maximum Number of Concurrent Operations:

  12. Double-click "Allow maximum number of concurrent operations".
  13. Set it to Enabled and define the maximum number of concurrent operations that the WinRM client can establish. The default is 5.

  14. Configure CredSSP for Authentication:

  15. Go to Computer Configuration > Policies > Administrative Templates > System > Credentials Delegation.
  16. Double-click "Allow delegating fresh credentials with NTLM-only server authentication".
  17. Set it to Enabled and specify the list of servers for which the credentials can be delegated.
Step 2: Configure GPO for WinRM Service
  1. Navigate to WinRM Service Settings:
  2. Go to Computer Configuration > Policies > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Service.

  3. Allow Remote Shell Access:

  4. Double-click on "Allow remote server management through WinRM".
  5. Set it to Enabled.
  6. Ensure that the IPv4 and IPv6 Filter settings allow connections from the required IP ranges or addresses.

  7. Configure the Maximum Number of Concurrent Users:

  8. Double-click "Allow maximum number of connections".
  9. Set it to Enabled and specify the maximum number of users that can connect concurrently to the WinRM service. The default value is 25.

  10. Set Timeouts for Idle and Operation Timeout:

  11. Double-click "Allow WinRM service to receive requests from remote computers".
  12. Set it to Enabled and configure the Idle Timeout (e.g., 240000 milliseconds) and Operation Timeout as needed.

  13. Enable Compatibility HTTP Listener:

  14. Double-click "Allow compatibility HTTP listener".
  15. Set it to Enabled. This setting allows older systems to connect using HTTP for management purposes while you transition to HTTPS.
Step 3: Configure Additional Security Settings
  1. Enable Secure Connections with HTTPS:
  2. Ensure that WinRM is configured to use only HTTPS by restricting the allowed listeners:

    • Go to Computer Configuration > Policies > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Service > Allow unencrypted traffic.
    • Set it to Disabled to enforce encrypted communication only.
  3. Disable Basic Authentication:

  4. Go to Computer Configuration > Policies > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Service > Allow Basic authentication.
  5. Set it to Disabled to prevent the use of basic (plaintext) authentication.

  6. Enable Kerberos Authentication:

  7. Go to Computer Configuration > Policies > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Service > Allow Kerberos authentication.
  8. Set it to Enabled to ensure Kerberos authentication is always used.
Step 4: Apply the Policy and Test Configuration
  1. Close the GPO Editor:
  2. Save and close the Group Policy Management Editor.

  3. Force Group Policy Update:

  4. On the target servers, open a command prompt with administrative privileges and run:

powershell gpupdate /force

  1. Verify WinRM Configuration:
  2. Test the configuration using the following command:

powershell Test-WsMan -ComputerName <ServerName>

Summary of Additional GPO Settings

By applying these additional GPO settings, you enhance the security and control of your WinRM environment:

  • WinRM Client Configuration:
  • Set trusted hosts and maximum concurrent operations.
  • Configure CredSSP for secure authentication.
  • WinRM Service Configuration:
  • Allow remote shell access and set maximum connections.
  • Enable timeouts and compatibility listeners.
  • Security Settings:
  • Enforce HTTPS, disable basic authentication, and enable Kerberos.

Additional Considerations

  • Audit and Monitoring: Regularly audit WinRM logs and monitor connections for unauthorized access attempts.
  • Testing: Test all configurations in a controlled environment before applying them widely to ensure they meet your organization's security policies.

Pure PowerShell Implementation

Here's a script to apply these additional settings using PowerShell:

# Define variables
$gpoName = "Enable WinRM with HTTPS and Auto-Enrollment"
$trustedHosts = "*.domain.com" # Replace with your domain or specific hosts
$maxConcurrentOperations = 5
$maxConnections = 25
$idleTimeoutMs = 240000
$operationTimeoutMs = 60000

# Function to configure additional GPO settings for WinRM
function Configure-AdditionalGPOSettings {
    # Import GroupPolicy module
    Import-Module GroupPolicy

    # Set trusted hosts
    Write-Output "Configuring Trusted Hosts..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client" -ValueName "TrustedHosts" -Type String -Value $trustedHosts

    # Set maximum concurrent operations
    Write-Output "Configuring maximum concurrent operations..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client" -ValueName "MaxConcurrentOperations" -Type DWORD -Value $maxConcurrentOperations

    # Allow remote server management through WinRM
    Write-Output "Allowing remote server management through WinRM..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "AllowAutoConfig" -Type DWORD -Value 1

    # Set maximum connections
    Write-Output "Configuring maximum connections..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "MaxConnections" -Type DWORD -Value $maxConnections

    # Set timeouts
    Write-Output "Configuring timeouts..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "IdleTimeout" -Type DWORD -Value $idleTimeoutMs
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "OperationTimeout" -Type DWORD -Value $operationTimeoutMs

    # Disable basic authentication
    Write-Output "Disabling Basic Authentication..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "AllowBasic" -Type DWORD -Value 0

    # Enable Kerberos authentication
    Write-Output "Enabling Kerberos Authentication..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "AllowKerberos" -Type DWORD -Value 1

    # Enforce HTTPS-only connections
    Write-Output "Disabling unencrypted traffic..."
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "AllowUnencryptedTraffic" -Type DWORD -Value 0
}

# Execute additional GPO configuration
Write-Output "Applying additional GPO settings for WinRM..."
Configure-AdditionalGPOSettings
Write-Output "Additional GPO configuration completed successfully!"

How to Use the PowerShell Script

  1. Run PowerShell as Administrator: Open PowerShell with elevated privileges.
  2. Save the Script: Save the script as Configure-Additional-WinRM-GPO.ps1.
  3. Execute the Script:

powershell .\Configure-Additional-WinRM-GPO.ps1

how to enable WinRM securely in PowerShell?


Article 2: Enabling WinRM over HTTPS Using PowerShell

This article will provide you with a PowerShell script to automate the process of enabling WinRM over HTTPS on multiple servers, configuring certificate auto-enrollment, creating a specific certificate template, and setting up the HTTPS listener.

PowerShell Script for Enabling WinRM over HTTPS

Save the following script as Configure-WinRM-HTTPS.ps1 and run it with administrative privileges on the server.

# Define variables
$domain = (Get-WmiObject Win32_ComputerSystem).Domain
$gpoName = "Enable WinRM with HTTPS and Auto-Enrollment"
$certificateTemplateName = "WinRM HTTPS"
$certRenewalPeriodHours = 1 # Certificate renewal period in hours
$additionalOU = "Servers"  # Additional OU or attribute to be included in the certificate subject

# Function to create a new GPO and edit settings
function Configure-GPO {
    # Import the GroupPolicy module
    Import-Module GroupPolicy

    # Create a new GPO
    Write-Output "Creating GPO: $gpoName"
    New-GPO -Name $gpoName

    # Link GPO to the domain root
    Write-Output "Linking GPO to domain root"
    New-GPLink -Name $gpoName -Target "DC=$($domain -replace '\.', ',DC=')"

    # Configure Auto-Enrollment for Certificates
    Write-Output "Configuring Auto-Enrollment for Certificates"
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\Software\Policies\Microsoft\Cryptography\AutoEnrollment" -ValueName "AEPolicy" -Type DWORD -Value 1

    # Configure Allow Remote Management through WinRM
    Write-Output "Configuring WinRM Settings"
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "AllowAutoConfig" -Type DWORD -Value 1
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" -ValueName "IPv4Filter" -Type String -Value "*"

    # Enable WinRM Service
    Write-Output "Configuring WinRM Service to start automatically"
    Set-GPRegistryValue -Name $gpoName -Key "HKLM\SYSTEM\CurrentControlSet\Services\WinRM" -ValueName "Start" -Type DWORD -Value 2

    # Configure firewall rules for WinRM
    Write-Output "Configuring Firewall Rules for WinRM"
    Invoke-Expression "netsh advfirewall firewall add rule name='WinRM HTTPS' dir=in action=allow protocol=TCP localport=5986"
    Invoke-Expression "netsh advfirewall firewall add rule name='WinRM HTTP' dir=in action=allow protocol=TCP localport=5985"
}

# Function to create and publish a certificate template
function Configure-CertificateTemplate {
    # Import Certificate Authority module
    Import-Module PKI

    # Duplicate the Web Server template
    Write-Output "Creating new Certificate Template for WinRM"
    $webServerTemplate = Get-CATemplate | Where-Object {$_.DisplayName -eq 'Web Server'}
    $newTemplate = $webServerTemplate.Duplicate()
    $newTemplate.DisplayName = $certificateTemplateName

    # Configure the template for dynamic subject with hostname and additional criteria
    $newTemplate.SubjectNameFlags = 'Machine Name'
    $newTemplate.ValidityPeriod =

 'Hours'
    $newTemplate.ValidityPeriodUnits = $certRenewalPeriodHours
    $newTemplate.Purpose = @('Server Authentication')

    # Set template for automatic enrollment
    Write-Output "Publishing Certificate Template for Auto-Enrollment"
    $newTemplate.EnrollmentFlags = 'IncludeSymmetricAlgorithms'
    Add-CATemplate -Template $newTemplate
}

# Function to create WinRM HTTPS listener using a specific certificate
function Configure-WinRMHTTPSListener {
    Write-Output "Creating Scheduled Task to configure WinRM HTTPS Listener"

    # Retrieve the certificate thumbprint for the specific template name
    $certThumbprint = (Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.NotAfter -gt (Get-Date) } | ForEach-Object { 
        $templateName = ($_.Extensions | Where-Object { $_.Oid.Value -eq "1.3.6.1.4.1.311.21.7" }).Format($false); 
        if ($templateName -like "*WinRM*") { 
            [PSCustomObject]@{ Subject = $_.Subject; TemplateName = $templateName } 
        } 
    }).Thumbprint

    if (-not $certThumbprint) {
        Write-Output "No valid certificate found for template: $certificateTemplateName"
        return
    }

    # Create a scheduled task to configure the WinRM HTTPS listener
    $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-Command `"New-Item -Path WSMan:\Localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint $certThumbprint`""
    $trigger = New-ScheduledTaskTrigger -AtStartup
    Register-ScheduledTask -Action $action -Trigger $trigger -TaskName 'Configure WinRM HTTPS Listener' -Description 'Configure WinRM HTTPS Listener for secure remote management' -RunLevel Highest -User 'SYSTEM'
}

# Function to set up scheduled task for certificate renewal
function Configure-CertRenewalTask {
    Write-Output "Creating Scheduled Task for frequent certificate renewal check"

    # Create a scheduled task action to trigger certificate renewal
    $renewalAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-Command "gpupdate /force"'
    # Set a trigger to run the task every hour
    $renewalTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Hours $certRenewalPeriodHours) -RepetitionDuration ([TimeSpan]::MaxValue)

    Register-ScheduledTask -Action $renewalAction -Trigger $renewalTrigger -TaskName 'CertRenewal' -Description 'Certificate auto-renewal every hour' -RunLevel Highest -User 'SYSTEM'
}

# Main script execution
Write-Output "Starting the configuration process..."
Configure-GPO
Configure-CertificateTemplate
Configure-WinRMHTTPSListener
Configure-CertRenewalTask
Write-Output "Configuration completed successfully!"

How to Use the PowerShell Script

  1. Run PowerShell as Administrator: Open PowerShell with elevated privileges.
  2. Save the Script: Save the script as Configure-WinRM-HTTPS.ps1.
  3. Execute the Script:

powershell .\Configure-WinRM-HTTPS.ps1

How To Enable WinRM Securely?


Article 1: Enabling WinRM over HTTPS Using the GUI

This article will guide you through the steps to enable Windows Remote Management (WinRM) over HTTPS on multiple servers using the GUI. You will also configure certificate auto-enrollment and create a Group Policy Object (GPO) to automate this process.

Step 1: Prepare the Certificate Authority (CA)
  1. Open the Certificate Authority (CA) Management Console:
  2. Press Win + R, type certsrv.msc, and press Enter.
  3. Expand the CA tree and select Certificate Templates.

  4. Duplicate the Web Server Template:

  5. Right-click Certificate Templates and select Manage.
  6. Find and right-click the Web Server template and choose Duplicate Template.
  7. Select Windows Server 2008 R2 or later as the Minimum Supported CA.

  8. Configure the New Template:

  9. Name the new template, e.g., "WinRM HTTPS".
  10. In the Subject Name tab, choose Supply in the request.
  11. Under the Request Handling tab, ensure Allow private key to be exported is checked if needed.
  12. In the Extensions tab, ensure Server Authentication is set as the intended purpose.

  13. Publish the New Template:

  14. Go back to the Certificate Templates in the CA Management Console.
  15. Right-click and choose New > Certificate Template to Issue.
  16. Select the newly created WinRM HTTPS template and click OK.
Step 2: Configure Auto-Enrollment in Group Policy
  1. Open Group Policy Management Console (GPMC):
  2. Press Win + R, type gpmc.msc, and press Enter.

  3. Create a New GPO:

  4. Right-click the domain or the Organizational Unit (OU) where you want to apply the policy and select Create a GPO in this domain, and Link it here....
  5. Name the GPO "Enable WinRM with HTTPS and Auto-Enrollment".

  6. Edit the GPO:

  7. Right-click the new GPO and select Edit.
  8. Navigate to Computer Configuration > Policies > Windows Settings > Security Settings > Public Key Policies.

  9. Configure Certificate Auto-Enrollment:

  10. Double-click Certificate Services Client - Auto-Enrollment.
  11. Set it to Enabled.
  12. Check both:
    • Renew expired certificates, update pending certificates, and remove revoked certificates.
    • Update certificates that use certificate templates.
Step 3: Configure WinRM Settings in Group Policy
  1. Enable Remote Management Through WinRM:
  2. In the GPO editor, navigate to Computer Configuration > Policies > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Service.
  3. Double-click Allow remote server management through WinRM.
  4. Set it to Enabled.
  5. Under IPv4 and IPv6 Filter, enter * to allow connections from any IP address.

  6. Set WinRM Service to Start Automatically:

  7. Go to Computer Configuration > Policies > Windows Settings > Security Settings > System Services.
  8. Locate Windows Remote Management (WS-Management).
  9. Set the Startup type to Automatic.
Step 4: Configure Firewall Rules
  1. Open Windows Defender Firewall with Advanced Security:
  2. In the GPO editor, navigate to Computer Configuration > Policies > Windows Settings > Security Settings > Windows Defender Firewall with Advanced Security > Inbound Rules.

  3. Allow Inbound WinRM Traffic:

  4. Right-click Inbound Rules and select New Rule....
  5. Select Predefined and choose Windows Remote Management.
  6. Choose both Windows Remote Management (HTTP-In) and Windows Remote Management (HTTPS-In).
  7. Select Allow the connection and click Finish.
Step 5: Configure WinRM HTTPS Listener on Each Server
  1. Open PowerShell as Administrator:
  2. Right-click on the Start menu and select Windows PowerShell (Admin).

  3. Get the Correct Certificate for WinRM:

  4. Run the following PowerShell command to find the certificate issued with the "WinRM" template:

powershell Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.NotAfter -gt (Get-Date) } | ForEach-Object { $templateName = ($_.Extensions | Where-Object { $_.Oid.Value -eq "1.3.6.1.4.1.311.21.7" }).Format($false); if ($templateName -like "*WinRM*") { [PSCustomObject]@{ Subject = $_.Subject; TemplateName = $templateName } } }

  1. Configure WinRM Listener:
  2. Use the certificate thumbprint found in the previous step to create an HTTPS listener:

powershell winrm create winrm/config/Listener?Address=*+Transport=HTTPS @{Hostname="<hostname>";CertificateThumbprint="<thumbprint>"}

Step 6: Apply and Verify Configuration
  1. Force Group Policy Update:
  2. On each server, open a command prompt as an administrator and run:

powershell gpupdate /force

  1. Test WinRM Configuration:
  2. Run the following command to test the WinRM connection:

powershell Test-WsMan -ComputerName <ServerName>

Summary

By following these steps, you have enabled WinRM over HTTPS using a certificate issued with a specific template and ensured that all necessary settings are configured using Group Policy.