Pass & Passage Credential Management

Comprehensive guide to managing credentials in pass (GPG-based) and passage (age-based) password stores using heredocs for structured, multi-line entries.

Quick Reference

Generate password + hash + store in one workflow

PASS=$(openssl rand -base64 16 | tr -d '/+=' | head -c 16)
HASH=$(echo "$PASS" | openssl passwd -6 -stdin)
pass insert -m PATH/TO/ENTRY << EOF
$PASS
---
username: user
password: $PASS
hash: $HASH
EOF

Quick retrieval

pass show PATH/TO/ENTRY
pass show -c PATH/TO/ENTRY
pass show PATH/TO/ENTRY | head -1

Understanding Pass vs Passage

Feature pass passage

Encryption

GPG

age

Key Management

GPG keyring

age identity files

Yubikey Support

Yes (GPG)

Yes (age plugin)

Speed

Slower

Faster

Store Location

~/.password-store/

~/.passage/store/

Recipients File

.gpg-id

.age-recipients

When to Use Each

  • pass: Team environments, existing GPG infrastructure, GPG smartcards

  • passage: Personal use, simpler key management, faster operations

Entry Format Convention

Standard format for infrastructure credentials:

PASSWORD_ON_FIRST_LINE
---
hostname: server-name
fqdn: server.domain.com
ip: 10.0.0.1
username: admin
password: same-as-first-line
hash: $6$...SHA512-hash-for-cloud-init...
ssh_port: 22
os: Fedora/Ubuntu/Arch
purpose: Brief description
created: YYYY-MM-DD
notes: Additional information

Why password on first line? Convention for pass -c (clipboard) which copies only the first line.

Complete Workflows

Workflow 1: New Server/VM Credential

1. Generate secure password

PASS=$(openssl rand -base64 16 | tr -d '/+=' | head -c 16)

2. Generate hash for cloud-init/kickstart

HASH=$(echo "$PASS" | openssl passwd -6 -stdin)

3. Store in pass (heredoc)

pass insert -m ADMINISTRATIO/servers/myserver-01 << EOF
$PASS
---
hostname: myserver-01
fqdn: myserver-01.inside.domain.com
ip: 10.50.1.100
username: ansible
password: $PASS
hash: $HASH
ssh_port: 22
os: Fedora Cloud 41
purpose: Application server
created: $(date +%Y-%m-%d)
notes: Created via automation
EOF

4. Store in passage (same format)

passage insert -m servers/myserver-01 << EOF
$PASS
---
hostname: myserver-01
fqdn: myserver-01.inside.domain.com
ip: 10.50.1.100
username: ansible
password: $PASS
hash: $HASH
ssh_port: 22
os: Fedora Cloud 41
purpose: Application server
created: $(date +%Y-%m-%d)
notes: Created via automation
EOF

5. Verify

pass show ADMINISTRATIO/servers/myserver-01
passage show servers/myserver-01

Workflow 2: API Credentials

pass insert -m ARCANA/api/cloudflare << 'EOF'
cf_api_token_here
---
service: Cloudflare
type: API Token
scope: Zone DNS Edit - domusdigitalis.dev
username: admin@domain.com
token: cf_api_token_here
created: 2026-01-13
expires: never
notes: Used for Let's Encrypt DNS-01 challenge
EOF

Workflow 3: Network Device Credentials

pass insert -m ADMINISTRATIO/network/switch-core-01 << 'EOF'
enable_password_here
---
hostname: switch-core-01
ip: 10.50.1.1
type: Cisco 3560CX
username: admin
password: login_password
enable: enable_password_here
ssh_port: 22
snmp_community: readonly_community
created: 2026-01-13
notes: Core switch - management VLAN
EOF

Workflow 4: Database Credentials

pass insert -m ARCANA/api/ise-dataconnect << 'EOF'
db_password_here
---
service: ISE DataConnect
type: Oracle Database
host: 10.50.1.21
port: 2484
database: cpm
username: dataconnect
password: db_password_here
ssl: required
created: 2026-01-13
notes: Read-only access to ISE operational data
EOF

Heredoc Variants

Literal Heredoc (No Variable Expansion)

Use << 'EOF' (quoted) when the content contains $ that should NOT be expanded:

pass insert -m example << 'EOF'
password123
---
notes: Cost is $500 per month
regex: ^[a-z]+$
EOF

Expanding Heredoc (With Variables)

Use << EOF (unquoted) when you want variable expansion:

MYPASS="generated_password"
MYDATE=$(date +%Y-%m-%d)
pass insert -m example << EOF
$MYPASS
---
created: $MYDATE
password: $MYPASS
EOF

Mixed Approach

For entries with both variables and literal $:

PASS="mypassword"
pass insert -m example << EOF
$PASS
---
password: $PASS
notes: Cost is \$500 per month
regex: ^[a-z]+\$
EOF

Scripted Entry Creation

Function for Servers

Add to your shell config (~/.bashrc, ~/.zshrc, ~/.config/fish/config.fish):

Bash/Zsh version

new-server-cred() {
    local name="$1"
    local ip="$2"
    local purpose="${3:-General purpose server}"

    if [[ -z "$name" || -z "$ip" ]]; then
        echo "Usage: new-server-cred <hostname> <ip> [purpose]"
        return 1
    fi

    local PASS=$(openssl rand -base64 16 | tr -d '/+=' | head -c 16)
    local HASH=$(echo "$PASS" | openssl passwd -6 -stdin)

    pass insert -m "ADMINISTRATIO/servers/$name" << EOF
$PASS
---
hostname: $name
fqdn: $name.inside.domusdigitalis.dev
ip: $ip
username: ansible
password: $PASS
hash: $HASH
ssh_port: 22
os: Fedora Cloud 41
purpose: $purpose
created: $(date +%Y-%m-%d)
notes: Auto-generated credentials
EOF

    echo "Created: ADMINISTRATIO/servers/$name"
    echo "Password: $PASS"
    echo "Hash stored for cloud-init use"
}

Fish version

function new-server-cred
    set name $argv[1]
    set ip $argv[2]
    set purpose (test -n "$argv[3]" && echo $argv[3] || echo "General purpose server")

    if test -z "$name" -o -z "$ip"
        echo "Usage: new-server-cred <hostname> <ip> [purpose]"
        return 1
    end

    set PASS (openssl rand -base64 16 | tr -d '/+=' | head -c 16)
    set HASH (echo "$PASS" | openssl passwd -6 -stdin)

    pass insert -m "ADMINISTRATIO/servers/$name" << EOF
$PASS
---
hostname: $name
fqdn: $name.inside.domusdigitalis.dev
ip: $ip
username: ansible
password: $PASS
hash: $HASH
ssh_port: 22
os: Fedora Cloud 41
purpose: $purpose
created: (date +%Y-%m-%d)
notes: Auto-generated credentials
EOF

    echo "Created: ADMINISTRATIO/servers/$name"
    echo "Password: $PASS"
end

Usage

new-server-cred vault-01 10.50.1.60 "Let's Encrypt certificate automation"
new-server-cred monitoring-01 10.50.1.70 "Prometheus/Grafana stack"

Extracting Fields

Get Specific Field

Get just the IP

pass show ADMINISTRATIO/servers/vault-01 | grep "^ip:" | cut -d' ' -f2

Get the hash for cloud-init

pass show ADMINISTRATIO/servers/vault-01 | grep "^hash:" | cut -d' ' -f2

Get password only (first line)

pass show ADMINISTRATIO/servers/vault-01 | head -1

Get all metadata (skip password and ---)

pass show ADMINISTRATIO/servers/vault-01 | tail -n +3

Parse with yq (if installed)

Extract as YAML (skip first 2 lines)

pass show ADMINISTRATIO/servers/vault-01 | tail -n +3 | yq '.ip'

Function for Field Extraction

pass-get() {
    local path="$1"
    local field="$2"

    if [[ -z "$field" ]]; then
        pass show "$path" | head -1  # Just password
    else
        pass show "$path" | grep "^${field}:" | cut -d' ' -f2-
    fi
}

Usage

pass-get ADMINISTRATIO/servers/vault-01
pass-get ADMINISTRATIO/servers/vault-01 ip
pass-get ADMINISTRATIO/servers/vault-01 hash

Directory Organization

~/.password-store/
├── ADMINISTRATIO/
│   ├── servers/
│   │   ├── vault-01
│   │   ├── monitoring-01
│   │   ├── home-dc01
│   │   └── template
│   ├── network/
│   │   ├── switch-core-01
│   │   ├── ise-02
│   │   └── template
│   ├── devices/
│   │   └── template
│   └── work/
│       └── template
├── ARCANA/
│   ├── api/
│   │   ├── cloudflare
│   │   ├── ise-02
│   │   ├── ise-dataconnect
│   │   └── template
│   ├── ssh/
│   │   ├── github
│   │   ├── gitlab
│   │   └── template
│   └── crypto/
│       └── template
└── .gpg-id
~/.passage/store/
├── servers/
│   ├── vault-01
│   └── monitoring-01
├── network/
│   └── switch-core-01
├── api/
│   ├── cloudflare
│   └── ise-dataconnect
└── .age-recipients

Syncing Between Pass and Passage

One-time Migration

for entry in $(pass ls | grep -v "^Password Store" | grep -v "^├" | grep -v "^│" | tr -d '└── '); do
    pass show "$entry" | passage insert -m "$entry"
done

Keep Both in Sync (Function)

dual-store() {
    local action="$1"
    local path="$2"
    shift 2

    case "$action" in
        insert)
            pass insert "$@" "$path"
            passage insert "$@" "$path"
            ;;
        rm)
            pass rm "$path"
            passage rm "$path"
            ;;
        *)
            echo "Usage: dual-store [insert|rm] path [options]"
            ;;
    esac
}

Security Best Practices

1. Git-Track Your Stores (Encrypted)

pass auto-tracks with git

pass git log --oneline -5

passage manual git setup

cd ~/.passage/store
git init
git add -A
git commit -m "Initial import"

2. Backup Recipients/Keys

Backup GPG keys

gpg --export-secret-keys --armor > ~/backup/gpg-secret-keys.asc

Backup age identity

cp ~/.age/identities/personal.key ~/backup/age-identity.key

3. Never Echo Passwords

BAD - shows in terminal history

echo "mypassword" | pass insert entry

GOOD - use heredoc or -m flag

pass insert -m entry << 'EOF'
mypassword
EOF

4. Use Clipboard Wisely

Copies to clipboard, clears after 45 seconds:

pass -c ADMINISTRATIO/servers/vault-01

Troubleshooting

"gpg: decryption failed: No secret key"

Check GPG keys

gpg --list-secret-keys

Re-import if needed

gpg --import ~/backup/gpg-secret-keys.asc

"age: error: no identity matched any of the recipients"

Check age identity exists

ls -la ~/.age/identities/

Verify recipients file

cat ~/.passage/store/.age-recipients

Entry Already Exists

Force overwrite

pass insert -f PATH/TO/ENTRY << 'EOF'
new_content
EOF

Or edit existing

pass edit PATH/TO/ENTRY

Real-World Examples

Example: ISE Certificate Manager VM

PASS=$(openssl rand -base64 16 | tr -d '/+=' | head -c 16)
HASH=$(echo "$PASS" | openssl passwd -6 -stdin)
pass insert -m ADMINISTRATIO/servers/vault-01 << EOF
$PASS
---
hostname: vault-01
fqdn: vault-01.inside.domusdigitalis.dev
ip: 10.50.1.60
username: ansible
password: $PASS
hash: $HASH
ssh_port: 22
os: Fedora Cloud 41
purpose: Let's Encrypt certificate automation for ISE portals
created: $(date +%Y-%m-%d)
notes: DNS-01 challenge via Cloudflare API
EOF

Example: Cloudflare API Token

pass insert -m ARCANA/api/cloudflare-dns << 'EOF'
your_cloudflare_api_token_here
---
service: Cloudflare
type: API Token
zone: domusdigitalis.dev
permissions: Zone DNS Edit, Zone Read
created: 2026-01-13
expires: never
usage: certbot DNS-01 challenge for Let's Encrypt
notes: Created from dash.cloudflare.com > My Profile > API Tokens
EOF

Example: Network Switch

pass insert -m ADMINISTRATIO/network/switch-3560cx-01 << 'EOF'
enable_secret_here
---
hostname: switch-3560cx-01
ip: 10.50.1.1
model: Cisco 3560CX-8PC-S
ios_version: 15.2(7)E7
username: admin
password: login_password_here
enable: enable_secret_here
console_port: /dev/ttyUSB0
console_speed: 9600
mgmt_vlan: 1
created: 2026-01-13
notes: Core switch - 8-port PoE+
EOF

Quick Command Reference

Task Command

List all entries

pass ls / passage ls

Show entry

pass show PATH / passage show PATH

Copy password to clipboard

pass -c PATH / passage -c PATH

Insert with heredoc

pass insert -m PATH << 'EOF'

Generate and store

pass generate PATH 32

Edit existing

pass edit PATH

Delete entry

pass rm PATH

Search entries

pass find TERM / passage find TERM

Git operations (pass)

pass git pull / pass git push