Skip to content

3 - Git

Ansible
Git
Linux


Info This is me documenting my journey of learning Ansible that is focused on network engineering. It’s not a “how-to guide” per-say, more of a diary. Each part will build upon the last. A lot of information on here is so I can come back to and reference later. I also learn best when teaching someone, and this is kind of me teaching.

Git & GitHub

I already know the basics (add, commit, push). But there’s a big difference between using Git and using Git well. Playbooks are infrastructure code. A bad commit that gets pushed and run in production can take down a network segment. A missing .gitignore can leak credentials to a public repository.


Why Version Control

Before Git, network changes worked like this: an engineer SSHs into a device, makes changes, types write mem, and hopes nothing breaks. If it does break, the rollback is “remember what you changed and undo it manually.” The config backup, if it exists, is a text file on a shared drive with a name like R1-config-final-FINAL-v3-USE-THIS-ONE.txt.

Ansible without version control isn’t much better. I have a playbook that works today. I change it. It breaks something in production. I can’t remember exactly what I changed or when.

Version control changes everything:

  • Every change is recorded: who changed what, when, and why
  • Every change is reversible: I can roll back to any previous state in seconds
  • Changes are reviewed before they run: Pull Requests mean a second set of eyes before anything touches production
  • The history is the documentation: commit messages explain decisions that comments in code never would
  • Collaboration is structured: multiple engineers can work on the same codebase without overwriting each other

Installing and Configuring Git

Installing Git

First, I updated the packages and then install git.

Bash
sudo apt update
sudo apt install -y git

Verify:

Bash
git --version
# git version 2.34.x

Configuring Git Identity

The first thing I do after installing Git is tell it who I am. Every commit I make is stamped with this name and email, it’s how the team knows who made each change.

Bash
git config --global user.name "Ernesto Diaz"
git config --global user.email "[email protected]"

Without this configuration, Git will either refuse to commit or use a default that looks unprofessional in the team’s commit history.


Setting the Default Branch Name

GitHub’s default branch name is main. Older Git versions default to master. I align them to avoid confusion:

Bash
git config --global init.defaultBranch main

Setting the Default Editor

When Git needs me to write a commit message in an editor (e.g., during a merge), it opens the default editor. I set it to nano since it’s what I’m used to:

Bash
git config --global core.editor nano

Setting Up a Credential Helper

When I push to GitHub over HTTPS (before SSH keys are set up), Git will ask for my credentials. The credential helper caches them so I’m not asked every single time:

Bash
git config --global credential.helper store
Danger credential.helper store saves credentials in plaintext at ~/.git-credentials. This is acceptable for a private VM on a home lab network, but not for a shared server or any machine others have access to. On shared machines, use credential.helper cache instead, it stores credentials in memory for a configurable timeout (default 15 minutes) and never writes them to disk.

Verifying the Configuration

Bash
git config --global --list
Expected Output
user.name=First Last
[email protected]
init.defaultBranch=main
core.editor=nano
credential.helper=store

All global Git settings are stored in ~/.gitconfig:

Bash
cat ~/.gitconfig
Expected Output
[user]
    name = First Last
    email = [email protected]
[init]
    defaultBranch = main
[core]
    editor = nano
[credential]
    helper = store

Connecting VM to GitHub via SSH

I want Git operations to work without typing a password every time. The cleanest way is SSH key authentication between my Ubuntu VM and GitHub. The same concept as SSH keys for server access, but this time the “server” is GitHub.


Generating an SSH Key

This key is specifically for GitHub. I generate it on the Ubuntu VM itself:

Bash
1
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/github_key
-t ed25519:
Ed25519 is the modern, recommended key type for GitHub
-C “[email protected]:
the comment becomes a label in GitHub’s SSH key list, helping me identify which key is which
-f ~/.ssh/github_key:
saves the key pair as github_key (private) and github_key.pub (public)

When prompted for a passphrase, I set one.


Setting Correct Permissions
Bash
chmod 600 ~/.ssh/github_key
chmod 644 ~/.ssh/github_key.pub

Configuring SSH to Use This Key for Github

I edit create ~/.ssh/config on the Ubuntu VM to tell SSH which key to use when connecting to GitHub:

Bash
nano ~/.ssh/config

I add the following:

/.ssh/config
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_key
    IdentitiesOnly yes

I set correct permissions on the config file:

Bash
chmod 600 ~/.ssh/config

Adding the SSH Agent

Since my key has a passphrase, I’d have to type it on every Git push without an SSH agent. The agent holds the decrypted key in memory for the session:

To start the SSH agent:

Bash
eval "$(ssh-agent -s)"

Add my GitHub key to the agent (will prompt for passphrase once):

Bash
ssh-add ~/.ssh/github_key

Verify it’s loaded

Bash
ssh-add -l

To make this automatic on every new shell session, I add it to ~/.bashrc:

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cat >> ~/.bashrc << 'EOF'

# Start SSH agent and add GitHub key if not already running
if [ -z "$SSH_AUTH_SOCK" ]; then
    eval "$(ssh-agent -s)" > /dev/null
    ssh-add ~/.ssh/github_key 2>/dev/null
fi
EOF

source ~/.bashrc
Line 4:
checks if the SSH agent is already running. Without this check, a new agent process would start every time I open a terminal, leaking memory over time.

Adding the Public Key to Github

Now I copy the public key and add it to my GitHub account.

Bash
cat ~/.ssh/github_key.pub

The output looks like:

Bash
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... [email protected]

I copy this entire line, then:

  1. Go to GitHub.com → click my profile picture → Settings
  2. In the left sidebar, click SSH and GPG keys
  3. Click New SSH key
  4. Title: ansible-ubuntu-vm
  5. Key type: Authentication Key
  6. Key: paste the public key
  7. Click Add SSH key

Testing the Connection

Bash
Expected Output
Hi myusername! You've successfully authenticated, but GitHub does not provide shell access.

That message confirms SSH authentication to GitHub is working. The “does not provide shell access” part is normal. GitHub only accepts Git operations over SSH, not interactive shell sessions.


Initializing the Project Repository

Now I set up the Git repository for the Ansible project I’ll build throughout this lab.


Creating the Project Directory Structure

Create the project directory

Bash
mkdir -p ~/projects/ansible-network
cd ~/projects/ansible-network

Create the initial directory structure

Bash
mkdir -p {playbooks,inventory/{group_vars,host_vars},roles,collections,templates,files,vars}

Verify the structure

Bash
tree .
Expected Output
ansible-network/
├── collections/
│   └── requirements.yml
├── files/
├── inventory/
│   ├── group_vars/
│   ├── host_vars/
│   └── hosts.yml
├── playbooks/
├── roles/
├── templates/
└── vars/

Initializing Git
Bash
cd ~/projects/ansible-network
git init
Expected Output
Initialized empty Git repository in /home/ansible/projects/ansible-network/.git/

Git creates a hidden .git/ directory that contains the entire history of the repository. I never manually edit anything inside .git/.

Check the current state of the repository:

Bash
git status
Expected Output
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        collections/
        inventory/
        playbooks/

nothing added to commit but untracked files present

“Untracked files” means Git sees these files exist but isn’t tracking changes to them yet. Nothing is version-controlled until I explicitly git add it. This is by design, Git never assumes I want to track a file. I choose what gets tracked.


Creating the .gitignore File

Before I make a single commit, I create the .gitignore file. This is non-negotiable since it prevents secrets, temporary files, and regeneratable artifacts from ever entering the repository.

Bash
nano ~/projects/ansible-network/.gitignore
Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
venv/
.venv/
env/
venvs/
__pycache__/
*.py[cod]
*.pyc
*.pyo
*.pyd
.Python

*.retry

.ansible/
fact_cache/

.vault_pass
.vault_password
vault_pass.txt
*.vault_pass

.env
.env.*
*.env

secrets.yml
secrets.yaml
secrets/

*.pem
*.key
id_rsa
id_ed25519
*_key
!*_key.pub

.vscode/extensions.json
.history/

*.swp
*.swo
*~

*.log
*.tmp
*.temp
/tmp/

.pytest_cache/
.coverage
htmlcov/
.tox/

tower_export/
awx_export/

# host_vars/
# group_vars/
Line 1:
The venv is outside this project directory (~/venvs/) but adding these patterns protects against accidental venv creation inside the project.
Line 12:
Retry files are created when a playbook fails mid-run. They contain hostnames and should never be committed.
Line 15:
Ansible fact cache can contain sensitive device information.
Lines 17-20:
Secrets, never commit these.
Lines 22-24:
Environment variable files containing tokens/passwords.
Lines 26-28:
Any file named secrets.
Lines 30-35:
SSH private keys, just in case.
Line 37:
VS Code, individual user settings are not committed.
Lines 40-42:
VIM swap files.
Lines 44-47:
Logs and temporary files.
Lines 49-52:
Test and cover reports.
Lines 54-55:
AWX / Tower export files. These can contain credentials if exported carelessly.
Warning The .gitignore file only prevents untracked files from being added to Git. If I accidentally commit a secret file before adding it to .gitignore, the secret is now in the Git history. Even if I delete the file afterward. Git history is permanent. The correct remediation is git filter-repo to rewrite history (which destroys all commit SHAs and requires every team member to re-clone), plus immediately rotating the compromised credential. The lesson: set up .gitignore before the first commit, every single time.

host_vars/ and group_vars/

I left these two lines commented out intentionally:

# host_vars/
# group_vars/

If I uncomment them, Git ignores the entire host_vars/ and group_vars/ directories, which means my variable files never get committed. That’s too aggressive. Most content in these directories (VLAN lists, interface names, routing configs) is not sensitive and should be in version control.

The correct approach is to use Ansible Vault to encrypt only the sensitive values within those files. I commit the encrypted vault files. Git stores the ciphertext, which is safe. The vault password itself goes in .vault_pass, which is in .gitignore.


The Basic Git Workflow

With the .gitignore in place, I’m ready to start tracking files. The core Git workflow I use daily is: modify → stage → commit → push.


Three States of a File in Git
Working Directory  →  Staging Area (Index)  →  Repository (.git/)
   (modified)           (git add)               (git commit)
  • Working Directory - files on my filesystem as I edit them
  • Staging Area - files I’ve marked as ready to be included in the next commit
  • Repository - committed snapshots permanently stored in Git history

Understanding the staging area is the key to Git. It lets me commit only specific changes even if I’ve modified multiple files. I stage exactly what I want in each commit.


First Commit
Bash
cd ~/projects/ansible-network
  1. Check current state
Bash
git status
  1. Stage the .gitignore file first
Bash
git add .gitignore
git status
  1. Commit it
Bash
git commit -m "Initial commit: add .gitignore for Ansible project"
  1. Stage the collections requirements file
Bash
git add collections/requirements.yml
git commit -m "Add Ansible collections requirements file"
  1. Stage the entire project structure at once
Bash
git add .
git status
git commit -m "Add initial project directory structure"
  • git add . - stages all untracked and modified files in the current directory and below. The . means “here and everything beneath.”
  • git add .gitignore - stages only that specific file. Precise staging like this produces cleaner commits.

Viewing What Changed Before Staging

See unstaged changes (what I’ve modified but not yet staged)

Bash
git diff

See staged changes (what will go into the next commit)

Bash
git diff --staged

See changes in a specific file

Bash
git diff playbooks/site.yml

git diff output reads like this:

Bash
diff --git a/playbooks/site.yml b/playbooks/site.yml
index a1b2c3d..d4e5f6g 100644
--- a/playbooks/site.yml
+++ b/playbooks/site.yml
@@ -5,6 +5,8 @@
   hosts: cisco_ios
   gather_facts: false
+  connection: network_cli
+  become: false
   tasks:
  • Lines starting with + are additions (shown in green in VS Code)
  • Lines starting with - are deletions (shown in red)
  • The @@ line shows which line numbers are affected

Unstaging a File

If I staged something by mistake:

Bash
git restore --staged filename.yml

The file goes back to “modified but unstaged”. The change is still there, it just won’t be in the next commit.


Discarding Changes Entirely

Discard all changes to a file since the last commit (CANNOT BE UNDONE):

Bash
git restore filename.yml
Warning git restore filename.yml permanently discards uncommitted changes to that file. There is no undo. This is one of the few Git operations that loses work irreversibly. I always run git diff filename.yml first to confirm what I’m about to lose before running restore.

Writing Meaningful Commit Messages

A commit message is a letter to my future self and my teammates explaining why a change was made. The code shows what changed and the commit message explains why.


Standard Format
<type>(<scope>): <short summary — 50 chars or less>

<body — optional, wrap at 72 chars>
Explain the motivation for the change. What problem does this solve?
What was the previous behavior, and why was it wrong?

<footer — optional>
Refs: CHG0012345
Closes: #42

Commit Types

These types follow the Conventional Commits specification.

TypeWhen to Use
featA new playbook, role, or feature
fixA bug fix in an existing playbook
refactorRestructuring code without changing behavior
docsREADME, comments, or documentation changes
choreMaintenance tasks (updating requirements, .gitignore)
testAdding or updating test playbooks
revertReverting a previous commit

Good vs Bad Commit Messages
Bad
git commit -m "fix"
git commit -m "update playbook"
git commit -m "changes"
git commit -m "WIP"
Good
git commit -m "fix(ios): correct interface description task to use ios_config not raw"
git commit -m "feat(nxos): add VLAN provisioning playbook for datacenter fabric"
git commit -m "chore: update ansible from 9.7.0 to 9.8.0, pin in requirements.txt"
git commit -m "fix(bgp): add missing neighbor activate under address-family for R3"

Commit Message with a Body
Bash
git commit

This opens nano for a multi-line message

In the editor
fix(ospf): increase dead interval to prevent flapping on WAN links

The OSPF dead interval was set to the default 40 seconds. WAN links
between HQ and Branch sites experience periodic latency spikes that
cause hello packets to be dropped, triggering OSPF neighbor drops
and route reconvergence.

Increased dead interval to 120 seconds and hello interval to 30
seconds on all WAN-facing interfaces. Verified no change to LAN
interfaces (still using defaults).

Tested against: R1, R2, R3 in staging environment.
Change window: CHG0019823
Refs: #87

Connecting to GitHub and Pushing

Creating the Remote Repository on GitHub
  1. Log in to GitHub.com
  2. Click the + in the top right → New repository
  3. Configure:
    • Repository name: ansible-network
    • Description: Network automation playbooks and roles for Cisco IOS/NX-OS, Juniper, and Palo Alto
    • Visibility: Private (always private for infrastructure code)
    • Do NOT initialize with README, .gitignore, or license (I already have these locally)
  4. Click Create repository

GitHub shows the “Quick setup” page with instructions. I’ll use the SSH URL.


Connecting the Local Repository to GitHub
Bash
cd ~/projects/ansible-network

Add GitHub as the remote repository (named “origin” by convention)

Bash
git remote add origin [email protected]:myusername/ansible-network.git

Verify the remote was added

Bash
git remote -v
Expected Output
origin  [email protected]:myusername/ansible-network.git (fetch)
origin  [email protected]:myusername/ansible-network.git (push)

origin is the conventional name for the primary remote. I can name it anything, but origin is what every tool expects.


Pushing for the First Time

Push the main branch to GitHub and set it as the upstream tracking branch

Bash
git push -u origin main

-u origin main - sets the upstream tracking relationship. After this first push, I can just type git push and Git knows where to push to.

Expected Output
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Writing objects: 100% (12/12), 1.23 KiB | 1.23 MiB/s, done.
To [email protected]:myusername/ansible-network.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

I can now go to github.com/myusername/ansible-network and see my files online.


Git Workflow

These are the commands I run every day.

  1. Start the session (activate the virtualenv and navigate to the lab)
Bash
source ~/venvs/ansible-network/bin/activate
cd ~/projects/ansible-network
  1. Pull the latest changes from GitHub before starting work
Bash
git pull
  1. Do my work (edit playbooks, add roles, update inventory)

  2. Check what changed

Bash
git status
git diff
  1. Stage specific files
Bash
git add playbooks/deploy_vlans.yml
  1. Commit with a meaningful message
Bash
git commit -m "feat(vlans): add VLAN deployment playbook for IOS access switches"
  1. Push to GitHub
Bash
git push

git pull is actually two operations in one: git fetch (download changes from GitHub) followed by git merge (apply them to my local branch). For solo work, git pull is fine. On a team, some engineers prefer git pull --rebase which replays my local commits on top of the fetched commits, keeping a cleaner linear history.


Branching Strategy

On a team with a formal review process, nobody pushes directly to main. Changes go through branches and Pull Requests. Here’s the lightweight strategy I use, tailored specifically for network automation work.


Branch Structure
main          # Production-ready code only. Protected branch.
│             # All changes enter via Pull Request and peer review
├── feature/add-bgp-role
├── feature/nxos-vlan-playbook
├── fix/ospf-dead-interval-wan
├── hotfix/acl-blocking-monitoring
└── chore/update-ansible-9.8
Branch Types
Branch TypeNaming ConventionPurpose
Featurefeature/short-descriptionNew playbooks, roles, or capabilities
Fixfix/short-descriptionBug fixes in existing playbooks
Hotfixhotfix/short-descriptionUrgent fixes that need to bypass normal review timelines
Chorechore/short-descriptionDependency updates, documentation, .gitignore changes
Testtest/short-descriptionExperimental changes, not intended for merge

Creating and Working on a Branch

I always start from an up-to-date main

Bash
git checkout main
git pull

Create a new branch and switch to it in one command

Bash
git checkout -b feature/add-ios-vlan-playbook

Verify which branch I’m on

Bash
git branch
# * feature/add-ios-vlan-playbook
#   main
  • git checkout -b - creates the branch AND switches to it. Without -b, I’d switch to an existing branch.
  • The * in git branch output marks the current branch.

I then do my work on this branch


Stage and commit as normal. Commits go to the feature branch, not main

Bash
git add playbooks/deploy_vlans.yml
git commit -m "feat(vlans): add IOS VLAN deployment playbook with trunk/access support"

Push the feature branch to GitHub

Bash
git push -u origin feature/add-ios-vlan-playbook

Switching Between Branches

Switch back to main (e.g., to pull updates or start a different task)

Bash
git checkout main

Switch back to my feature branch

Bash
git checkout feature/add-ios-vlan-playbook

List all branches (local and remote)

Bash
git branch -a

Before switching branches, I always commit or stash my work-in-progress. Git will refuse to switch branches if I have uncommitted changes that conflict with the target branch. If I’m mid-task and need to switch:

Bash
1
2
3
4
5
git stash
git checkout main
# ... do what I need to do on main ...
git checkout feature/add-ios-vlan-playbook
git stash pop
Line 1:
Temporarily shelve uncommitted changes
Line 5:
Restore my shelved changes

Cleaning Up

Switch back to main and pull the merged changes

Bash
git checkout main
git pull

Delete the feature branch locally (it’s been merged, no longer needed)

Bash
git branch -d feature/add-ios-vlan-playbook

Delete the remote branch on GitHub

Bash
git push origin --delete feature/add-ios-vlan-playbook

Peer Review Process

A Pull Request (PR) is a formal request to merge a branch into main. On a team with a formal review process, no code reaches main without at least one approval.

Creating a Pull Request on GitHub

After pushing a feature branch:

  1. Go to the repository on GitHub.com
  2. GitHub usually shows a yellow banner: “feature/add-ios-vlan-playbook had recent pushes” → click Compare & pull request
  3. Fill in the PR template:

PR Title:

feat(vlans): Add IOS VLAN deployment playbook with trunk/access support

PR Description:

Markdown
## Summary
Adds a new playbook `playbooks/deploy_vlans.yml` for deploying VLAN
configurations to Cisco IOS access layer switches.

## Changes
- New playbook: `playbooks/deploy_vlans.yml`
- New vars file: `inventory/group_vars/cisco_ios_access.yml`
- Updated: `collections/requirements.yml` (added cisco.ios 8.0.1)

## Testing
- Tested against 3x CSR1000v nodes in Containerlab
- Ran with `--check` first, then applied
- Verified VLAN database and trunk configurations post-apply

## How to Test
`ansible-playbook playbooks/deploy_vlans.yml --limit lab_switches --check`
`ansible-playbook playbooks/deploy_vlans.yml --limit lab_switches`

Checklist
  • ansible-lint passes with no violations
  • yamllint passes with no violations
  • No secrets or credentials in this PR
  • requirements.txt updated if new Python packages added
  • collections/requirements.yml updated if new collections added
  1. Assign a Reviewer at least one other engineer
  2. Click Create pull request
Info I add a PR template to the repository so every PR automatically gets the checklist structure. I create the file .github/pull_request_template.md in the repository root and GitHub will use it for every new PR automatically. This standardizes what every reviewer expects to see and makes the checklist a habit rather than an afterthought.

What the Reviewer Does

The reviewer:

  • Reads through every changed file in the Files changed tab
  • Looks for logic errors, missing error handling, hardcoded values that should be variables
  • Verifies no secrets are present
  • Checks that the playbook follows the project’s naming and structure conventions
  • Leaves inline comments on specific lines if something needs changing
  • Either Approves, Requests changes, or Comments without a verdict

Responding to Review Feedback

Make the requested changed on my feature branch

Bash
git add playbooks/deploy_vlans.yml
git commit -m "fix: address review feedback — parameterize VLAN range, add error handling"
git push

The PR on GitHub automatically updates with the new commit. The reviewer can see the changes and re-review. Once approved, the PR is merged.


Branch Protection Rules

For a team with formal review, I configure branch protection on main in GitHub:

  1. Go to repository → SettingsBranches
  2. Click Add branch protection rule
  3. Branch name pattern: main
  4. Enable:
    • Require a pull request before merging
    • Require approvals set to 1 minimum (or 2 for higher-risk repos)
    • Dismiss stale pull request approvals when new commits are pushed
    • Require status checks to pass before merging (for CI/CD in Part 32)
    • Restrict who can push to matching branches (only senior engineers or automation service accounts)
  5. Click Create

git log and git diff

Viewing Commit History

Basic log:

Bash
git log

Compact one-line format:

Bash
git log --oneline

Compact with branch graph:

Bash
git log --oneline --graph --all

Last 5 commits:

Bash
git log -5 --oneline

Commits by a specific author:

Bash
git log --author="First Last" --oneline

Commits that touched a specific file:

Bash
git log --oneline -- playbooks/deploy_vlans.yml

Commits in a date range:

Bash
git log --oneline --after="2024-01-01" --before="2024-12-31"

Search commit messages for a keyword:

Bash
git log --grep="bgp" --oneline

Sample git log --oneline --graph --all output:

Expected Output
* a3f2b1c (HEAD -> main, origin/main) fix(bgp): add missing neighbor activate for R3
* 9d4e5f2 feat(vlans): add IOS VLAN deployment playbook
* 7c8b3a1 chore: update ansible from 9.7.0 to 9.8.0
* 4f1d9e8 feat(ospf): add multi-area OSPF role for IOS
* 2a3c7b0 Initial commit: add .gitignore for Ansible project

Inspecting a Specific Commit

Show the full diff of a specific commit:

Bash
git show a3f2b1c

Show just the files that changed in a commit:

Bash
git show a3f2b1c --stat

Show what changed in a specific file in a specific commit:

Bash
git show a3f2b1c -- playbooks/bgp.yml

Comparing States

What changed between two commits:

Bash
git diff 4f1d9e8 a3f2b1c

What changed between a commit and the current working state:

Bash
git diff a3f2b1c

What changed between two branches:

Bash
git diff main feature/add-ios-vlan-playbook

What changed in a specific file between two branches:

Bash
git diff main feature/add-ios-vlan-playbook -- playbooks/deploy_vlans.yml
Reverting a Bad Commit

If a commit that was already pushed to main turns out to be wrong:

Bash
git revert a3f2b1c
git push

Never use git reset --hard or git push --force on the main branch on a shared repository. These commands rewrite history, which invalidates every team member’s local copy of the repository and can cause data loss. git revert is always the safe way to undo a change that’s already been pushed. Reserve git reset for cleaning up commits that have NOT yet been pushed.


Security Best Practices

What Never Goes Into Git
  • Passwords (device passwords, API tokens, RADIUS secrets)
  • SSH private keys
  • Ansible Vault passwords (.vault_pass)
  • .env files with credentials
  • AWS/cloud credentials
  • Private IP addressing schemes of production networks (debatable, but cautious teams exclude this)
  • Anything that would give an attacker a foothold if the repo were made public

Scanning for Accidentlly Committed Secrets

Before pushing, I can scan for secrets using git-secrets or trufflehog:

Install trufflehog (a secrets scanner)

Bash
pip install trufflehog

Scan the entire repository history for secrets

Bash
trufflehog git file://. --only-verified
Setting Up a Pre-commit Hook to Block Secret Commits

A pre-commit hook runs automatically before every git commit and can block the commit if it finds problems:

Install pre-commit framework

Bash
pip install pre-commit

Create .pre-commit-config.yaml in the project root

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
cat > ~/projects/ansible-network/.pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: check-merge-conflict

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets 
        args: ['--baseline', '.secrets.baseline']

  - repo: https://github.com/ansible/ansible-lint
    rev: v24.9.2
    hooks:
      - id: ansible-lint
EOF
id: check-yaml:
Validates YAML syntax
id:end-of-file-fixer:
Ensures files end with newline
id:trailing-whitespace:
Removes trailing whitespace
id: check-merge-conflict:
Blocks commits with merge conflic markers
id: detect-secrets:
Scans for hardcoded secrets
id: ansible-link:
Runs ansible-lint before every commit

Install the hooks (runs once — creates hooks in .git/hooks/)

Bash
pre-commit install
  • pre-commit install - installs the hooks into .git/hooks/pre-commit. Now every git commit automatically runs these checks first.
  • If any check fails, the commit is blocked until I fix the issue.

Test it manually

Bash
pre-commit run --all-files

If a Secret Was Already Committed

This is the procedure if I accidentally committed a credential:

  1. Rotate the credential immediately - assume it’s compromised. Change the password, revoke the API token, generate a new SSH key. Do this first, before anything else.
  2. Remove it from history using git filter-repo (not git filter-branch which is deprecated):
Bash
pip install git-filter-repo
git filter-repo --path secrets.yml --invert-paths
git push --force-with-lease origin main
  1. Notify the team - everyone must re-clone the repository because the history has changed.
  2. Add the file to .gitignore immediately.
  3. Conduct a post-incident review - how did this happen and what process change prevents it next time?

Every change I make from this point forward goes through Git. Playbooks, inventory files, variable files, roles, templates. All of it is version-controlled, reviewed, and traceable.

Last updated on • Ernesto Diaz