djuntgen@juntgen.com
← all posts

Building a Homelab with AI · part 22

Security Audit: Scrubbing Usernames and Secrets from a Public Git Repository


The Problem We Found

Deep into building out the Phase 2 monitoring stack, we hit a moment of clarity: every command example, every Ansible task, every blog post in this repository contained the actual system username and GitHub username for this homelab. Hardcoded. Plaintext. In a public repository.

This matters more than it might seem at first. Username enumeration is a real attack surface. Knowing a valid username on a system short-circuits the first step of a brute-force or credential stuffing attack. Combined with the public-facing nature of the blog content, this was a meaningful operational security gap.

The rule we established: no usernames, no API keys, no secrets in git — ever. These are HIGH SENSITIVE information.


What We Found

A git grep sweep revealed the username appearing in:

  • Ansible operational configansible_user, become_user, owner, group hardcoded in task files throughout the openclaw role
  • Group variables/home/<username>/.openclaw paths hardcoded in group_vars/ai/vars.yml, admin_user defined in plaintext in group_vars/all/vars.yml
  • Inventoryansible_user hardcoded in hosts.yml
  • Docs and setup guides — SSH command examples, cloud-init parameters, sudoers examples throughout docs/
  • 18 blog posts — the username used as a consistent teaching example across posts on sudoers, file permissions, Ansible, and OpenClaw deployment
  • API examples — GitHub username hardcoded in Portainer stack registration commands in CLAUDE.md and the monitoring blog post
  • The agent filename itself<github-user>-homelab-sre.md embedded the GitHub username in the filename

No actual secrets (passwords, tokens, private keys) were found in plaintext git history — those had been in Ansible Vault from the start. But the username exposure was pervasive.


The Fix

1. Ansible Vault for admin_user

The system username was previously defined in two places: group_vars/all/vars.yml (plaintext) and roles/base/defaults/main.yml (as a fallback). We moved it into the encrypted vault file and removed it from both plaintext locations:

ansible-vault edit inventory/group_vars/all/vault.yml
# Added: admin_user: <value>

All other references in the codebase already used {{ admin_user }} (or were updated to), so this change propagated automatically at runtime with no functional impact.

2. Parameterize the Ansible openclaw Role

The openclaw role had the username hardcoded in six task files:

# Before
become_user: <user>
owner: <user>
group: <user>
path: "/home/<user>/.config/systemd/user/openclaw-gateway.service"
ansible.builtin.command: loginctl enable-linger <user>
# After
become_user: "{{ admin_user }}"
owner: "{{ admin_user }}"
group: "{{ admin_user }}"
path: "/home/{{ admin_user }}/.config/systemd/user/openclaw-gateway.service"
ansible.builtin.command: loginctl enable-linger {{ admin_user }}

The group_vars/ai/vars.yml config paths were also updated:

# Before
openclaw_config_dir: /home/<user>/.openclaw

# After
openclaw_config_dir: "/home/{{ admin_user }}/.openclaw"

3. Inventory and Connection Variables

ansible/inventory/hosts.yml had ansible_user hardcoded at the all:vars level. The Apple group vars had a separate hardcoded username for macOS. Both now use {{ admin_user }}:

# hosts.yml
all:
  vars:
    ansible_user: "{{ admin_user }}"

4. API Examples — Env Vars Instead of Literals

The Portainer stack registration command in CLAUDE.md had the GitHub username hardcoded in the repository URL. We replaced it with env vars sourced from ~/.portainer.env, which already contains GITHUB_USER and GITHUB_PAT:

# Before
"repositoryURL": "https://github.com/<github-user>/docker-homelab",
"repositoryUsername": "<github-user>",

# After
source ~/.portainer.env
GITHUB_REPO_URL="https://github.com/$GITHUB_USER/docker-homelab"
# ...
"repositoryURL": "'"$GITHUB_REPO_URL"'",
"repositoryUsername": "'"$GITHUB_USER"'",

5. Rename the Agent File

The Claude Code agent definition file was named <github-user>-homelab-sre.md — embedding the GitHub username in the filename. We renamed it to homelab-agent.md and updated every reference across docs, README, and the installed copy at ~/.claude/agents/.

6. Bulk Scrub of Docs and Blog Posts

18 files had the username embedded in command examples, file paths, or prose. Rather than editing each individually, we used a targeted sed sweep:

find docs/ terraform/ ansible/playbooks/workstation.yml -name "*.md" -o -name "*.sh" | \
  xargs grep -l "<user>\|<github-user>" | grep -v vault.yml | \
  while read f; do
    sed -i \
      -e 's|/home/<user>/|/home/<user>/|g' \
      -e 's|<user> ALL=(ALL)|<user> ALL=(ALL)|g' \
      -e 's|<user>:<user>|<user>:<user>|g' \
      -e 's|<github-user>|<github-user>|g' \
      -e 's|<user>|<user>|g' \
      "$f"
  done

This covered blog posts 2, 3, 4, 8, 9, 10, 11, 14, 17, and 18 — all of which used the real username as a consistent teaching example. The content remains valid; command examples that used a specific username now use <user> as a generic placeholder, which is better practice for published documentation anyway.


What Remains

One item intentionally left in plaintext: ssh_public_key in group_vars/all/vars.yml. A public key is, by definition, public — it is safe to share. Only the corresponding private key is secret, and that never touches the repository.

The real domain name was subsequently scrubbed from all blog posts and replaced with example.com (an IANA-reserved documentation domain). While the domain is public-facing, its presence in blog posts enabled subdomain enumeration and infrastructure mapping — unnecessary exposure for published documentation.


Lessons

Audit early, audit often. Usernames and email addresses are not as benign as they feel when you’re writing them. In a public repository, they become permanent, searchable, indexed facts about your infrastructure.

Ansible Vault handles more than passwords. Moving admin_user to vault was a small change that centralised a previously-scattered value and encrypted it at rest. The pattern — define once in vault, reference everywhere as {{ variable }} — scales to any value you want to keep out of plaintext.

The agent file name was a blind spot. We would never hardcode a username in file content without thinking about it, but the filename itself slipped past initial review. Filenames are git-tracked too.

git grep is your security scanner. Before any commit to a public repository, running git grep -n "specific-term" across the working tree takes five seconds and catches what review misses. Add it to your pre-commit mental checklist.