<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Linux &#8211; Everything is Broken</title>
	<atom:link href="https://play.datalude.com/blog/category/linux/feed/" rel="self" type="application/rss+xml" />
	<link>https://play.datalude.com/blog</link>
	<description>Efficiency vs. Inefficiency, in a no-holds barred fight.</description>
	<lastBuildDate>Fri, 06 Mar 2026 09:17:10 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.1</generator>
	<item>
		<title>Removing all 5.xx kernels and associated modules, config from Ubuntu</title>
		<link>https://play.datalude.com/blog/2026/03/removing-all-5-xx-kernels-and-associated-modules-config-from-ubuntu/</link>
					<comments>https://play.datalude.com/blog/2026/03/removing-all-5-xx-kernels-and-associated-modules-config-from-ubuntu/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Fri, 06 Mar 2026 09:05:36 +0000</pubDate>
				<category><![CDATA[Bash Script]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=797</guid>

					<description><![CDATA[I was getting a bit cramped for space on the root partition of an older desktop, which has been running for 4 years or so, and discovered that although I'd been running the 6.x kernel for several years, there was a lot of baggage from 5.x kernels lying around. Here's a reminder to myself how ... <a title="Removing all 5.xx kernels and associated modules, config from Ubuntu" class="read-more" href="https://play.datalude.com/blog/2026/03/removing-all-5-xx-kernels-and-associated-modules-config-from-ubuntu/" aria-label="Read more about Removing all 5.xx kernels and associated modules, config from Ubuntu">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I was getting a bit cramped for space on the root partition of an older desktop, which has been running for 4 years or so, and discovered that although I'd been running the 6.x kernel for several years, there was a lot of baggage from 5.x kernels lying around. Here's a reminder to myself how to clear it up. If you've done an inplace upgrade from Ubuntu 22 to 24, you're probably in the same position. <br><br>So first of all, check your running kernel. You definitely want to keep that one! Then check what kernels you've got installed</p>



<pre class="wp-block-code"><code># Current kernel
uname -r
6.8.0-101-generic
# What kernels do we have installed. 
# ii at the beginning of the line means installed, but rc entries might have some residual config files.
dpkg --list | grep 'linux-image-'</code></pre>



<p class="wp-block-paragraph">Now, if you're certain that you want to get rid of all the kernels beginning with 5.x, then go ahead and run the following. Or otherwise edit the commands so that you're disposing of the ones you want to get. </p>



<pre class="wp-block-code"><code>sudo apt remove $(dpkg --list | grep 'linux-image-5\.' | awk '{print $2}' | xargs)  
# And now we get rid of all the associated modules and headers
sudo apt remove $(dpkg --list | grep 'linux-headers-5\.' | awk '{print $2}' | xargs)
sudo apt remove $(dpkg --list | grep 'linux-modules-5\.' | awk '{print $2}' | xargs)   
sudo apt remove $(dpkg --list | grep 'linux-modules-extra-5\.' | awk '{print $2}' | xargs)
# And clean up
sudo apt autoremove --purge
sudo update-grub   </code></pre>



<p class="wp-block-paragraph">So that cleared over a Gigabyte. Ubuntu will generally (I think) keep the last 3 kernels, but sometimes it doesn't seem to clean up everything it can. </p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2026/03/removing-all-5-xx-kernels-and-associated-modules-config-from-ubuntu/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Mail troubleshooting with curl.</title>
		<link>https://play.datalude.com/blog/2026/02/mail-troubleshooting-with-curl/</link>
					<comments>https://play.datalude.com/blog/2026/02/mail-troubleshooting-with-curl/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Thu, 26 Feb 2026 08:30:56 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=793</guid>

					<description><![CDATA[A docker container wasn't sending mail. I'd debugged the connection from the host machine and that was working, but once inside the container, there were no tools available. No mail, no mutt, no traceroute, no ping, no mtr, no netcat &#8230; so how to find out if the container could send to the mail server ... <a title="Mail troubleshooting with curl." class="read-more" href="https://play.datalude.com/blog/2026/02/mail-troubleshooting-with-curl/" aria-label="Read more about Mail troubleshooting with curl.">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A docker container wasn't sending mail. I'd debugged the connection from the host machine and that was working, but once inside the container, there were no tools available. No mail, no mutt, no traceroute, no ping, no mtr, no netcat &#8230; so how to find out if the container could send to the mail server &#8230; </p>



<p class="wp-block-paragraph">Well it turns out curl was available and curl can help! This command was able to send a test email without any of the tools I'd normally use on a full OS. Sweet. </p>



<pre class="wp-block-code"><code>curl --url 'smtp://103.125.202.12:25' \
 --mail-from no_reply@pretend.org \
 --mail-rcpt mailtest@datalude.com \
 --upload-file - &lt;&lt;EOF
From: Mr Roboto &lt;no_reply@pretend.org>
To: Mailtest &lt;mailtest@datalude.com>
Subject: Robot Mail Test
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

So is it working? Maybe.
EOF</code></pre>



<p class="wp-block-paragraph">Other useful looking options, not needed this time were:<br>&#8211;ssl-reqd \<br>&#8211;user 'username:password' \<br></p>



<p class="wp-block-paragraph">I did find another even more rudimentary connectivity check, which I'll put here for kicks, which doesn't even need curl. </p>



<pre class="wp-block-code"><code>(echo >/dev/tcp/103.125.202.12/25) &amp;>/dev/null &amp;&amp; echo "open" || echo "closed"   </code></pre>



<p class="wp-block-paragraph">Its all about using what you've got. But then again if it wasn't for docker in the first place it would all have been a lot simpler &#8230;</p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2026/02/mail-troubleshooting-with-curl/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bash script to replicate a directory structure</title>
		<link>https://play.datalude.com/blog/2025/12/bash-script-to-replicate-a-directory-structure/</link>
					<comments>https://play.datalude.com/blog/2025/12/bash-script-to-replicate-a-directory-structure/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 06:17:08 +0000</pubDate>
				<category><![CDATA[Bash Script]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=789</guid>

					<description><![CDATA[I found a need for this once upon a time. It will descend through a Linux directory, get the permissions, ownership details for all the subdirs and then write a script for you to recreate it somewhere else. You use it like this. Script listing]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I found a need for this once upon a time. It will descend through a Linux directory, get the permissions, ownership details for all the subdirs and then write a script for you to recreate it somewhere else. </p>



<p class="wp-block-paragraph">You use it like this. </p>



<pre class="wp-block-code"><code># Put the script in the parent directory of the one you want to clone and run it.
clone_directory_structure.sh targetdirectory &gt; regenerate_directory_structure.sh

# Then move the script to where you want to clone the directory and run it. 
regenerate_directory_structure.sh 
</code></pre>



<p class="wp-block-paragraph">Script listing</p>



<pre class="wp-block-code"><code>#!/bin/bash

# Check if directory is provided
if &#91; $# -ne 1 ]; then
    echo "Usage: $0 &lt;directory>"
    echo "Example: $0 /path/to/directory"
    exit 1
fi

# Remove trailing slash if present
TARGET_DIR="${1%/}"
TARGET_BASENAME="$(basename "$TARGET_DIR")"
TARGET_PARENT="$(dirname "$TARGET_DIR")"

# Check if directory exists
if &#91; ! -d "$TARGET_DIR" ]; then
    echo "Error: Directory '$TARGET_DIR' does not exist"
    exit 1
fi

echo "#!/bin/bash"
echo "# Script generated on $(date)"
echo "# To recreate the directory structure, run this script as root or with sudo"
echo "set -e  # Exit on error"
echo

# Process the target directory itself
echo "# Creating target directory: $TARGET_BASENAME"
echo "mkdir -p \"$TARGET_BASENAME\""

# Get permissions, owner, and group of the target directory
TARGET_PERMS=$(stat -c "%a" "$TARGET_DIR")
TARGET_OWNER=$(stat -c "%U" "$TARGET_DIR")
TARGET_GROUP=$(stat -c "%G" "$TARGET_DIR")

echo "chmod $TARGET_PERMS \"$TARGET_BASENAME\""
echo "chown $TARGET_OWNER:$TARGET_GROUP \"$TARGET_BASENAME\""
echo

# Find all subdirectories and process them
find "$TARGET_DIR" -type d -not -path "$TARGET_DIR" -print0 | while IFS= read -r -d $'\0' dir; do
    # Quote the directory path to handle spaces
    dir="$dir"
    # Get relative path from target directory
    rel_path="${dir#$TARGET_DIR/}"
    
    # Quote the path to handle spaces
    rel_path_quoted="$(printf '%q' "$rel_path")"
    
    # Get permissions in octal
    perms=$(stat -c "%a" "$dir")
    
    # Get owner and group
    owner=$(stat -c "%U" "$dir")
    group=$(stat -c "%G" "$dir")
    
    # Output commands with proper quoting for paths with spaces
    echo "# Creating directory: $TARGET_BASENAME/$rel_path"
    echo "mkdir -p $(printf '%q' "$TARGET_BASENAME/$rel_path")"
    echo "chmod $perms $(printf '%q' "$TARGET_BASENAME/$rel_path")"
    echo "chown $owner:$group $(printf '%q' "$TARGET_BASENAME/$rel_path")"
    echo
done

echo "echo \"Directory structure recreation complete!\""
echo "echo \"Created structure under: $TARGET_BASENAME/\""
</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/12/bash-script-to-replicate-a-directory-structure/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>bash htaccess file management script</title>
		<link>https://play.datalude.com/blog/2025/11/bash-htaccess-file-management-script/</link>
					<comments>https://play.datalude.com/blog/2025/11/bash-htaccess-file-management-script/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Thu, 06 Nov 2025 02:23:24 +0000</pubDate>
				<category><![CDATA[Bash Script]]></category>
		<category><![CDATA[General IT]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=780</guid>

					<description><![CDATA[OK, so its not too hard to run htpasswd manually, but if you spend a lot of time in your job doing it, it's nice to have a tool to do it more efficiently. This is a menu driven script, which will make a backup of your file, and suggest a random password, which you ... <a title="bash htaccess file management script" class="read-more" href="https://play.datalude.com/blog/2025/11/bash-htaccess-file-management-script/" aria-label="Read more about bash htaccess file management script">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">OK, so its not too hard to  run htpasswd manually, but if you spend a lot of time in your job doing it, it's nice to have a tool to do it more efficiently. This is a menu driven script, which will make a backup of your file, and suggest a random password, which you can choose to use or not. </p>



<p class="wp-block-paragraph">As always, don't trust everything you read on the internet, and test before you use in anger. </p>



<pre class="wp-block-code"><code>#!/bin/bash

# Configuration
HTACCESS_FILE_DEFAULT="/etc/nginx/htpasswd-developers

# Function to display a list of users
display_users() {
  echo "--- Existing Users ---"
  # Grep for lines that don't start with # and aren't empty
  # Then use cut to show only the username before the colon, and number the lines
  grep -vE '^(#|$)' "$HTACCESS_FILE" | cut -d':' -f1 | cat -n
  echo "----------------------"
}

# Function to perform a backup
backup_file() {
  if &#91; -f "$HTACCESS_FILE" ]; then
    cp "$HTACCESS_FILE" "$HTACCESS_FILE.bak"
    echo "Backup created at $HTACCESS_FILE.bak"
  else
    echo "No .htpasswd file to back up."
  fi
}

# Function to generate a random password
generate_password() {
    tr -cd '&#91;:alnum:]' &lt; /dev/urandom | head -c 12
}

# --- Main Script ---

# Select the .htpasswd file. Suggest the default value, but allow user to type another name
read -p "Enter .htpasswd file (default: $HTACCESS_FILE_DEFAULT): " HTACCESS_FILE
HTACCESS_FILE=${HTACCESS_FILE:-$HTACCESS_FILE_DEFAULT}

# Ensure the file exists, exit if not
if &#91; ! -f "$HTACCESS_FILE" ]; then
  echo "Password file doesn't exist"
  exit 1
fi

# Main menu loop
while true; do
  echo "Do you want to:"
  echo "a) Add a user"
  echo "r) Remove a user"
  echo "u) Update a user's password"
  echo "l) List users"
  echo "q) Quit"
  read -p "Enter your choice: " choice

  case "$choice" in
    a)
      read -p "Enter username to add: " username
      # Check if the username already exists
      if grep -q "^$username:" "$HTACCESS_FILE"; then
        echo "Error: User '$username' already exists. Use the 'u' option to update their password."
      else
        suggested_password=$(generate_password)
        read -p "Enter password for '$username' (or press Enter to use suggested: $suggested_password): " password
        password=${password:-$suggested_password}
        
        backup_file
        htpasswd -b "$HTACCESS_FILE" "$username" "$password"
        echo "User '$username' added."
      fi
      ;;
    r)
      display_users
      read -p "Enter reference number of user to remove: " ref
      # Use grep and sed to find the line number and get the username
      username_to_remove=$(grep -vE '^(#|$)' "$HTACCESS_FILE" | sed -n "${ref}p" | cut -d':' -f1)

      if &#91; -z "$username_to_remove" ]; then
        echo "Invalid reference number."
      else
        backup_file
        # Create a temp file without the user and then replace the original
        grep -v "^$username_to_remove:" "$HTACCESS_FILE" > "$HTACCESS_FILE.tmp" &amp;&amp; mv "$HTACCESS_FILE.tmp" "$HTACCESS_FILE"
        echo "User '$username_to_remove' removed."
      fi
      ;;
    u)
      display_users
      read -p "Enter reference number of user to update: " ref
      username_to_update=$(grep -vE '^(#|$)' "$HTACCESS_FILE" | sed -n "${ref}p" | cut -d':' -f1)

      if &#91; -z "$username_to_update" ]; then
        echo "Invalid reference number."
      else
        suggested_password=$(generate_password)
        read -p "Enter new password for '$username_to_update' (or press Enter to use suggested: $suggested_password): " password
        password=${password:-$suggested_password}

        backup_file
        htpasswd -b "$HTACCESS_FILE" "$username_to_update" "$password"
        echo "Password for user '$username_to_update' updated."
      fi
      ;;
    l)
      display_users
      ;;
    q)
      echo "Exiting."
      exit 0
      ;;
    *)
      echo "Invalid option. Please try again."
      ;;
  esac

  echo ""
done
</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/11/bash-htaccess-file-management-script/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Fixing up nginx file opening permissions</title>
		<link>https://play.datalude.com/blog/2025/10/fixing-up-nginx-file-opening-permissions/</link>
					<comments>https://play.datalude.com/blog/2025/10/fixing-up-nginx-file-opening-permissions/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 15 Oct 2025 08:16:16 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[nginx]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=776</guid>

					<description><![CDATA[Got this error message on startup of nginx. There are a lot of places you can change this value so it gets a bit confusing. It could be the user that nginx is running under, (usually www-data) or the process, or it could be set in systemd init file or in security/limits.conf &#8230; OK, so ... <a title="Fixing up nginx file opening permissions" class="read-more" href="https://play.datalude.com/blog/2025/10/fixing-up-nginx-file-opening-permissions/" aria-label="Read more about Fixing up nginx file opening permissions">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Got this error message on startup of nginx. </p>



<pre class="wp-block-code"><code>nginx: &#91;warn] 4096 worker_connections exceed open file resource limit: 1024</code></pre>



<p class="wp-block-paragraph">There are a lot of places you can change this value so it gets a bit confusing. It could be the user that nginx is running under, (usually www-data) or the process, or it could be set in systemd init file or in security/limits.conf &#8230; OK, so we'll run a script to gather the info</p>



<pre class="wp-block-code"><code>#!/bin/bash

# --- Configuration ---
NGINX_CONF="/etc/nginx/nginx.conf"
NGINX_SERVICE="nginx.service"
LIMITS_CONF="/etc/security/limits.conf"
SYSCTL_CONF="/etc/sysctl.conf"
# --- End Configuration ---

echo "#################################################################"
echo "# NGINX ULIMIT (MAX OPEN FILES) CHECKER"
echo "#################################################################"

# 1. Find the NGINX User
NGINX_USER=$(grep -E '^\s*user\s+' $NGINX_CONF | awk '{print $2}' | sed 's/;//' | head -n 1)
if &#91;&#91; -z "$NGINX_USER" ]]; then
    NGINX_USER="www-data" # Common default user if not explicitly set
fi
echo -e "\n&#91;1] NGINX RUNNING USER: ${NGINX_USER}"

# 2. Get NGINX Process Limits
echo -e "\n&#91;2] CURRENT NGINX WORKER PROCESS LIMITS (/proc/&lt;pid&gt;/limits)"
NGINX_PID=$(pgrep -u "$NGINX_USER" nginx | head -n 1)
if &#91;&#91; -n "$NGINX_PID" ]]; then
    cat "/proc/$NGINX_PID/limits" | grep "Max open files"
else
    echo "NGINX worker process is not currently running under user '$NGINX_USER'."
fi

# 3. Check NGINX Configuration Directives
echo -e "\n&#91;3] NGINX CONFIGURATION CHECK (${NGINX_CONF})"
NGINX_RBLIMIT=$(grep -E '^\s*worker_rlimit_nofile\s+' $NGINX_CONF | awk '{print $2}' | sed 's/;//' | head -n 1)
NGINX_CONNECTIONS=$(grep -E '^\s*worker_connections\s+' $NGINX_CONF | awk '{print $2}' | sed 's/;//' | head -n 1)

if &#91;&#91; -n "$NGINX_RBLIMIT" ]]; then
    echo -e "   - worker_rlimit_nofile: \t**$NGINX_RBLIMIT**"
else
    echo -e "   - worker_rlimit_nofile: \tNot set (NGINX inherits OS limit)"
fi

if &#91;&#91; -n "$NGINX_CONNECTIONS" ]]; then
    echo -e "   - worker_connections: \t**$NGINX_CONNECTIONS**"
else
    echo -e "   - worker_connections: \tNot set (Defaults to 512 or 1024)"
fi

# 4. Check systemd Service Unit Limit (Most modern Linux systems)
echo -e "\n&#91;4] SYSTEMD SERVICE UNIT LIMIT (${NGINX_SERVICE})"
SYSTEMD_LIMIT=$(systemctl show $NGINX_SERVICE --property LimitNOFILE | awk -F'=' '{print $2}')
if &#91;&#91; -n "$SYSTEMD_LIMIT" ]]; then
    echo -e "   - LimitNOFILE (systemd): \t**$SYSTEMD_LIMIT**"
else
    echo "   - LimitNOFILE (systemd): \tNot explicitly set (Inherits from system defaults/limits.conf)"
fi

# 5. Check User Limits (limits.conf)
echo -e "\n&#91;5] USER LIMITS CONFIGURATION (${LIMITS_CONF})"
USER_LIMITS=$(grep -E "^$NGINX_USER\s+(soft|hard)\s+nofile" $LIMITS_CONF)
ALL_USER_LIMITS=$(grep -E "^\*\s+(soft|hard)\s+nofile" $LIMITS_CONF)

if &#91;&#91; -n "$USER_LIMITS" ]]; then
    echo -e "   - Limits for $NGINX_USER:\n$USER_LIMITS"
elif &#91;&#91; -n "$ALL_USER_LIMITS" ]]; then
    echo -e "   - General limits (*):\n$ALL_USER_LIMITS"
else
    echo "   - No explicit nofile limits found for '$NGINX_USER' or '*'."
fi

# 6. Check System-Wide Kernel Limit
echo -e "\n&#91;6] SYSTEM-WIDE KERNEL LIMIT (fs.file-max)"
KERNEL_MAX=$(cat /proc/sys/fs/file-max)
echo -e "   - fs.file-max (Kernel): \t**$KERNEL_MAX**"

echo "#################################################################"</code></pre>



<p class="wp-block-paragraph">OK, so that gives us the output </p>



<pre class="wp-block-code"><code>#################################################################
# NGINX ULIMIT (MAX OPEN FILES) CHECKER
#################################################################

&#91;1] NGINX RUNNING USER: www-data

&#91;2] CURRENT NGINX WORKER PROCESS LIMITS (/proc/&lt;pid&gt;/limits)
Max open files            1024                 524288               files     

&#91;3] NGINX CONFIGURATION CHECK (/etc/nginx/nginx.conf)
   - worker_rlimit_nofile: 	Not set (NGINX inherits OS limit)
   - worker_connections: 	**4096**

&#91;4] SYSTEMD SERVICE UNIT LIMIT (nginx.service)
   - LimitNOFILE (systemd): 	**524288**

&#91;5] USER LIMITS CONFIGURATION (/etc/security/limits.conf)
   - Limits for www-data:
www-data         hard    nofile          65536
www-data         soft    nofile          65536

&#91;6] SYSTEM-WIDE KERNEL LIMIT (fs.file-max)
   - fs.file-max (Kernel): 	**9223372036854775807**
#################################################################</code></pre>



<p class="wp-block-paragraph">So we need to alter the low value for the nginx worker process, currently at 1024</p>



<p class="wp-block-paragraph">Insert this line near the top of /etc/nginx/nginx.conf file, before the events {} section. </p>



<pre class="wp-block-code"><code>worker_rlimit_nofile 65536; </code></pre>



<p class="wp-block-paragraph">And restart nginx. Now we see </p>



<pre class="wp-block-code"><code>&#91;2] CURRENT NGINX WORKER PROCESS LIMITS (/proc/&lt;pid>/limits)
Max open files            65536                65536                files     

&#91;3] NGINX CONFIGURATION CHECK (/etc/nginx/nginx.conf)
   - worker_rlimit_nofile: 	**65536**
   - worker_connections: 	**4096**
</code></pre>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/10/fixing-up-nginx-file-opening-permissions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Copy tmux Scroll Buffer to a File</title>
		<link>https://play.datalude.com/blog/2025/10/copy-tmux-scroll-buffer-to-a-file/</link>
					<comments>https://play.datalude.com/blog/2025/10/copy-tmux-scroll-buffer-to-a-file/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Fri, 10 Oct 2025 03:31:12 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=774</guid>

					<description><![CDATA[tmux is super handy for long running commands. Especially if you have a dodgy connection which is likely to break: the command will keep running and you can re-attach to the session to save the day. But as a dyed-in-the-wool bash user, I've kinda got used to being able to scroll up and down my ... <a title="Copy tmux Scroll Buffer to a File" class="read-more" href="https://play.datalude.com/blog/2025/10/copy-tmux-scroll-buffer-to-a-file/" aria-label="Read more about Copy tmux Scroll Buffer to a File">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">tmux is super handy for long running commands. Especially if you have a dodgy connection which is likely to break: the command will keep running and you can re-attach to the session to save the day.  </p>



<p class="wp-block-paragraph">But as a dyed-in-the-wool bash user, I've kinda got used to being able to scroll up and down my command history and copy items from there. But I always have to search the internet the exact commands I need in tmux. Here's an easy way to dump the whole tmux history buffer to a text file without complicated edit/scroll/copy start/copy end combinations. We'll assume your tmux prefix key is the default CTRL-B</p>



<pre class="wp-block-code"><code>CTRL-B  :                       # default prefix key and colon. opens the command pane, at the bottom
capture-pane -S -       # send whole history to tmux buffer

CTRL-B   :                                              # get ready for another command, tmux
<code>save-buffer ~/tmux_output.txt         # self explanatory</code> </code></pre>



<p class="wp-block-paragraph">Now you can use your text editor to look at the command history, alerts, and outputs. I had to use this recently when I piped a long running script through tee -a but it failed to send any of the screen output to the specified text file. This was a lifesaver.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/10/copy-tmux-scroll-buffer-to-a-file/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>HEREDOC operators</title>
		<link>https://play.datalude.com/blog/2025/09/heredoc-operators/</link>
					<comments>https://play.datalude.com/blog/2025/09/heredoc-operators/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 24 Sep 2025 06:41:25 +0000</pubDate>
				<category><![CDATA[Bash Script]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=767</guid>

					<description><![CDATA[Basic usage Output alternatives: Can also use >> to append to output file, or &#124; tee to output to screen as it runs. Quoting preserves content If you're outputting a script you might need to preserve the variable notation instead of evaluating it. You can use either single or double quotes to do this. Zapping ... <a title="HEREDOC operators" class="read-more" href="https://play.datalude.com/blog/2025/09/heredoc-operators/" aria-label="Read more about HEREDOC operators">Read more</a>]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Basic usage</h2>



<pre class="wp-block-code"><code>VARIABLE="variable"

# command format
cat &lt;&lt; CONTENTS > /path/to/output.conf
Normal text here 
The second line contains a $VARIABLE
        These are Tabbed
        Indented
CONTENTS

# Result, cat /path/to/output.conf
Normal text here
The second line contains a variable
        These are Tabbed
        Indented</code></pre>



<p class="wp-block-paragraph"><strong>Output alternatives:</strong> Can also use >> to append to output file, or | tee to output to screen as it runs. </p>



<h2 class="wp-block-heading">Quoting preserves content</h2>



<p class="wp-block-paragraph">If you're outputting a script you might need to preserve the variable notation instead of evaluating it. You can use either single or double quotes to do this. </p>



<pre class="wp-block-code"><code>VARIABLE="variable"

# command format
cat &lt;&lt; 'CONTENTS' > /path/to/output.conf
Normal text here 
The second line contains a $VARIABLE
        These are Tabbed
        Indented
CONTENTS

# Result, cat /path/to/output.conf
Normal text here 
The second line contains a $VARIABLE
        These are Tabbed
        Indented
</code></pre>



<h2 class="wp-block-heading">Zapping Tabs</h2>



<p class="wp-block-paragraph">The &lt;&lt;- construct will remove tabs from your input, which means you can format your code nicely. Doesn't work at the same time as "CONTENTS"<br><br></p>



<pre class="wp-block-code"><code>VARIABLE="variable"

# command format
cat &lt;&lt;- CONTENTS > /path/to/output.conf
Normal text here 
The second line contains a $VARIABLE
	These are Tabbed
		Indented
CONTENTS

# Result, cat /path/to/output.conf
Normal text here 
The second line contains a variable
These are Tabbed
Indented
</code></pre>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/09/heredoc-operators/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>rrsync: a hidden gem</title>
		<link>https://play.datalude.com/blog/2025/09/rrsync-a-hidden-gem/</link>
					<comments>https://play.datalude.com/blog/2025/09/rrsync-a-hidden-gem/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 10 Sep 2025 05:14:57 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=764</guid>

					<description><![CDATA[I've been using rsync for decades, and had never come across its cousin rrsync, until a google search put it on my map. I was revisiting the inherent security problem in rsync backups: if you've given ssh access to a server, it can typically do a lot more than just rsync. To limit the damage, ... <a title="rrsync: a hidden gem" class="read-more" href="https://play.datalude.com/blog/2025/09/rrsync-a-hidden-gem/" aria-label="Read more about rrsync: a hidden gem">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I've been using rsync for decades, and had never come across its cousin rrsync, until a google search put it on my map. I was revisiting the inherent security problem in rsync backups: if you've given ssh access to a server, it can typically do a lot more than just rsync. <br><br>To limit the damage, I'd already set up a 'pull' backup so the backup server grabs the files from the production server. If you do it the other way around, an attacker gaining access to your production server can delete that, plus all the remote backups! So I'd already taken one step in the right direction, but it wasn't enough. Which is where rrsync comes in. </p>



<p class="wp-block-paragraph">Its installed along with rsync, and resides in /usr/bin/rrsync (on Ubuntu at least). Its basically a wrapper that limits access for a remote rsync server to named directories, and can additionally specify that they're read only. </p>



<pre class="wp-block-code"><code>/usr/bin/rrsync -ro /backups/</code></pre>



<p class="wp-block-paragraph">You've probably already added the ssh key to the user's authorized_keys file on  your production server, so &#8230;</p>



<pre class="wp-block-code"><code># Change this
ssh-rsa AAAAAAgasaofasdfndsfasdfablahblah
# To this
command="/usr/bin/rrsync -ro /backups",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAAAgasaofasdfndsfasdfablahblah</code></pre>



<p class="wp-block-paragraph">On your backup server you might already be running rsync to pull a directory over from your production server. So, </p>



<pre class="wp-block-code"><code># Change this
rsync -avz -e ssh user@production.com:/backups/ /local/backups/production/
# To this
rsync -avz -e ssh user@production.com: /local/backups/production/
</code></pre>



<p class="wp-block-paragraph">It looks wrong, like it will backup the whole server, but basically on the production server end it will only let the backup server 'see' the single directory you specified in authorized_keys. Any other command you try to run from the backup server on the production server will fail. </p>



<p class="wp-block-paragraph">Limitation: As far as I can see, you can only specify a single directory. To do two directories, you'd need to connect with two separate ssh keys and do one directory each time. </p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/09/rrsync-a-hidden-gem/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Advanced Bash logging to handle errors.</title>
		<link>https://play.datalude.com/blog/2025/09/advanced-bash-logging-to-handle-errors/</link>
					<comments>https://play.datalude.com/blog/2025/09/advanced-bash-logging-to-handle-errors/#comments</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Mon, 01 Sep 2025 07:27:41 +0000</pubDate>
				<category><![CDATA[Bash Script]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[script]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=759</guid>

					<description><![CDATA[So we've all done a quick log from a bash script, which looks like this That's great for small scripts. But sometimes you find that its not logging errors, which are also helpful, and you need to add those: And different commands behave differently, so it all starts to become complicated. But there's an easier ... <a title="Advanced Bash logging to handle errors." class="read-more" href="https://play.datalude.com/blog/2025/09/advanced-bash-logging-to-handle-errors/" aria-label="Read more about Advanced Bash logging to handle errors.">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">So we've all done a quick log from a bash script, which looks like this </p>



<pre class="wp-block-code"><code>LOGFILE="logs/$(date +%F)_backup.log"
# Append to the file
restic backup /etc/ &gt;&gt; $LOGFILE
find /root/scripts/ -type f -name "restic*.log" -mtime +14 -delete -print &gt;&gt; $LOGFILE</code></pre>



<p class="wp-block-paragraph">That's great for small scripts. But sometimes you find that its not logging errors, which are also helpful, and you need to add those:</p>



<pre class="wp-block-code"><code>LOGFILE="logs/$(date +%F)_backup.log"
# Append to the file
restic backup /etc/ 2&gt;&amp;1 &gt;&gt; $LOGFILE
find /root/scripts/ -type f -name "restic*.log" -mtime +14 -delete -print 2&gt;&amp;1 &gt;&gt; $LOGFILE</code></pre>



<p class="wp-block-paragraph">And different commands behave differently, so it all starts to become complicated. But there's an easier way. Two actually. The first is to use curly brackets to contain all the commands you want to log, and then pipe it through tee at the end. Commands after that block won't be logged. </p>



<span id="more-759"></span>



<pre class="wp-block-code"><code>LOGFILE="logs/$(date +%F)_backup.log"
{
echo "Starting Restic backup at $(date)"
restic backup /etc/
find /root/scripts/ -type f -name "restic*.log" -mtime +14 -delete -print
echo "Restic backup and maintenance completed"
} 2>&amp;1 | tee -a "${LOGFILE}"</code></pre>



<p class="wp-block-paragraph">And then there's the slightly less readable command substitution method. </p>



<pre class="wp-block-code"><code>exec &gt; &gt;(tee -a "${LOGFILE}") 2&gt;&amp;1
echo "Starting Restic backup at $(date)"
$RESTIC backup /etc/
find /root/scripts/ -type f -name "restic*.log" -mtime +14 -delete -print
echo "Restic backup and maintenance completed"</code></pre>



<h3 class="wp-block-heading">Comparison</h3>



<p class="wp-block-paragraph">Both methods will log all output of all commands, including errors. I find the curly brackets way slightly easier to read. It also allows you to select which commands are logged, but putting them inside the brackets or not. On the other hand, the exec method will log everything until the end of the script, which may or may not be desirable. Curly braces also seems to be more widely compatible. </p>



<p class="wp-block-paragraph">There may be other considerations, different performance considerations and buffering for heavy duty scripts, but that's beyond my regular use cases. </p>



<p class="wp-block-paragraph">Will just add a poor-man's log rotation trick for compleness, and so I can copy and paste it. </p>



<pre class="wp-block-code"><code># Start logging to rolling logfile
LOGFILE=/path/to.log
KEEPLINES=5000
{
            ### commands
} 2>&amp;1 | tee -a "${LOGFILE}"

# Keep last XXXX lines of rolling logfile, defined above in KEEPLINES
tail -n $KEEPLINES "$LOGFILE" > "$LOGFILE".tmp
mv -f "$LOGFILE".tmp "$LOGFILE"</code></pre>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/09/advanced-bash-logging-to-handle-errors/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Google Gemini bad ssh advice locks people out of their servers.</title>
		<link>https://play.datalude.com/blog/2025/08/google-gemini-bad-ssh-advice-locks-people-out-of-their-servers/</link>
					<comments>https://play.datalude.com/blog/2025/08/google-gemini-bad-ssh-advice-locks-people-out-of-their-servers/#respond</comments>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Wed, 27 Aug 2025 02:21:34 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://play.datalude.com/blog/?p=756</guid>

					<description><![CDATA[I fell for this one myself, so I present it here as a cautionary tale. I've been running Linux servers for 30 years or so, and have a muscle memory associated with changing sshd ports. Edit sshd_config to change Port=123 , update firewall to allow port 123, restart ssh, test config from another box to ... <a title="Google Gemini bad ssh advice locks people out of their servers." class="read-more" href="https://play.datalude.com/blog/2025/08/google-gemini-bad-ssh-advice-locks-people-out-of-their-servers/" aria-label="Read more about Google Gemini bad ssh advice locks people out of their servers.">Read more</a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I fell for this one myself, so I present it here as a cautionary tale. </p>



<p class="wp-block-paragraph">I've been running Linux servers for 30 years or so, and have a muscle memory associated with changing sshd ports. Edit sshd_config to change Port=123 , update firewall to allow port 123, restart ssh, test config from another box to port 123, done. <br><br>Imagine my horror when that didn't work any more. On occasion, having done this hundreds or thousands of times, I might skip the 'test from another box' stage, but I did so this time and was unable to connect. </p>



<p class="wp-block-paragraph">After a lot of searching, I found the culprit was systemd, or rather Ubuntu's decision to move control of the sshd process to there, so that edits to /etc/ssh/sshd_config were ignored. So when you search for things these days on the internet, you get some AI busting in telling you how to fix it. In this case it was Google Gemini, who confidently told me:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">To change the port or address while keeping socket activation, you must modify the <code>sshd.socket</code> unit's configuration.<sup></sup> The best practice is to create a "drop-in" file to override the default settings without directly editing the main unit file.</p>



<ul class="wp-block-list">
<li>Create a directory: <code>sudo mkdir -p /etc/systemd/system/ssh.socket.d</code></li>



<li>Create a new drop-in file: <code>sudo nano /etc/systemd/system/ssh.socket.d/listen.conf</code></li>



<li>Add the following content, replacing <code>1234</code> with your desired port. The empty <code>ListenStream=</code> line is crucial as it clears the default port <code>22</code>.<code>[Socket] ListenStream= ListenStream=1234</code></li>



<li>Reload the systemd daemon: <code>sudo systemctl daemon-reload</code></li>



<li>Restart the socket: <code>sudo systemctl restart ssh.socket</code></li>



<li>Update your firewall rules to allow the new port.</li>
</ul>
</blockquote>



<p class="wp-block-paragraph">Well the only trouble is, that didn't work. It left my system's sshd listening on ipv6 ONLY. And although the server had ipv6, my connection did not. </p>



<p class="wp-block-paragraph">Luckily I was able to access the server via the host's console and fix it, but this is a massive gotcha for those VM hosts that don't facilitate that. </p>



<p class="wp-block-paragraph">So how to fix it, really Google? Well, if you want to stick with ssh.socket:</p>



<pre class="wp-block-code"><code>sudo systemctl edit ssh.socket
# Add
&#91;Socket]
ListenStream=
ListenStream=0.0.0.0:123
ListenStream=&#91;::]:123

# Restart 
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket

# Verify that sshd is now listening on your new port for both IPv4 and IPv6:
sudo ss -tulpn | grep 123</code></pre>



<p class="wp-block-paragraph">OR, if you want to go back to the way things were, so you don't get confused &#8230;.</p>



<pre class="wp-block-code"><code># Disable and stop the socket unit: <br>sudo systemctl disable --now ssh.socket<br><br>#Enable and start the service unit: <br>sudo systemctl enable --now ssh.service</code></pre>



<p class="wp-block-paragraph">Now you can edit /etc/ssh/sshd_config and simply run sudo systemctl restart ssh.service for changes to take effect.</p>



<p class="wp-block-paragraph">The crazy thing is, if you tell Google that their Recommended method doesn't work, it cheerfully acknowledges it and tells you the correct way to do it. </p>



<p class="wp-block-paragraph">Be careful out there with AI. </p>
]]></content:encoded>
					
					<wfw:commentRss>https://play.datalude.com/blog/2025/08/google-gemini-bad-ssh-advice-locks-people-out-of-their-servers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
