Secure Non-Interactive Password Management in Python: Best Practices for chpasswd and Alternatives


4 views

When automating server provisioning with Fabric, the standard approach of piping clear-text passwords to chpasswd raises legitimate security concerns. While convenient, this method exposes credentials in multiple vulnerable points:

# Vulnerable approach (shown in process listings and shell history)
run('echo "username:password" | chpasswd')

The cleartext exposure occurs in:

  • Fabric's command output logging
  • Remote system's process listing (ps aux)
  • Potential shell history files
  • SSH session logs

A more secure method involves generating the encrypted password hash locally before transmission:

import crypt
from fabric import Connection

def generate_sha512_hash(password):
    # Generate a random salt and SHA-512 hash
    return crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))

admin_password = getpass.getpass()
password_hash = generate_sha512_hash(admin_password)

with Connection(host) as conn:
    conn.run(f'usermod -p "{password_hash}" {admin_username}')

This approach avoids transmitting the cleartext password while maintaining compatibility with Linux's PAM system.

For production environments, consider these more robust alternatives:

1. SSH Certificate Authentication

# Generate keys locally first
ssh-keygen -t ed25519 -f admin_key

# Deploy public key
with Connection(host) as conn:
    conn.run(f'mkdir -p /home/{admin_username}/.ssh')
    conn.put('admin_key.pub', f'/home/{admin_username}/.ssh/authorized_keys')
    conn.run(f'chown -R {admin_username}:{admin_username} /home/{admin_username}/.ssh')
    conn.run('chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys')

2. Temporary Password with Forced Change

# Set random temporary password
temp_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
hash = generate_sha512_hash(temp_pass)

with Connection(host) as conn:
    conn.run(f'usermod -p "{hash}" {admin_username}')
    conn.run(f'chage -d 0 {admin_username}')  # Force password change on login

When implementing password changes programmatically:

# Example audit logging
with Connection(host) as conn:
    conn.run(f'''
        logger -t password_change "Password updated for {admin_username}";
        chage -l {admin_username} > /var/log/password_changes.log
    ''')

In modern infrastructure, consider these patterns:

# Kubernetes initContainer example
apiVersion: v1
kind: Pod
metadata:
  name: user-setup
spec:
  initContainers:
  - name: user-config
    image: alpine
    command: ["/bin/sh", "-c"]
    args:
      - apk add shadow;
        useradd -M -s /bin/bash appuser;
        echo "appuser:$(openssl rand -base64 32)" | chpasswd;
        exit 0

When automating server provisioning or user management tasks, system administrators often need to set passwords programmatically. The standard interactive tools like passwd don't work well in automated scripts, forcing us to find secure alternatives.

The common method using chpasswd presents several security issues:

  • Password appears in clear text in command history
  • Visible in process listings during execution
  • Potential exposure in log files
  • Command line arguments are world-readable via /proc

Method 1: Using File Descriptors

A more secure approach avoids passing the password as a command line argument:

from fabric import Connection
import getpass

def set_password(conn, username, password):
    cmd = f"chpasswd"
    with conn.popen(cmd, shell=True, stdin=conn.channel) as proc:
        proc.stdin.write(f"{username}:{password}\n")
        proc.stdin.flush()

admin_password = getpass.getpass("Admin password: ")
c = Connection('host.example.com', user='root')
set_password(c, 'newuser', admin_password)

Method 2: Python Crypt Module

For direct password hash generation:

import crypt
from fabric import Connection

def create_user(conn, username, password):
    # Generate secure hash
    salt = crypt.mksalt(crypt.METHOD_SHA512)
    pwhash = crypt.crypt(password, salt)
    
    # Create user with hashed password
    conn.run(f"useradd -m -p '{pwhash}' {username}")

c = Connection('host.example.com', user='root')
create_user(c, 'service_account', 'complex_password_123!')

Method 3: SSH-based Solutions

For environments where Python modules aren't available:

import subprocess

def ssh_set_password(host, username, password):
    ssh_cmd = [
        'ssh',
        '-o', 'StrictHostKeyChecking=no',
        f'root@{host}',
        f'echo "{username}:{password}" | sudo chpasswd'
    ]
    subprocess.run(ssh_cmd, check=True)

For production environments, consider these more robust approaches:

  • LDAP integration for centralized authentication
  • Configuration management tools like Ansible Vault
  • Secret management systems (Hashicorp Vault, AWS Secrets Manager)
  • Temporary SSH certificates for initial setup

Always implement proper logging when changing credentials:

import logging
from datetime import datetime

logging.basicConfig(filename='password_changes.log', level=logging.INFO)

def log_password_change(username, changed_by):
    timestamp = datetime.now().isoformat()
    logging.info(f"Password changed for {username} by {changed_by} at {timestamp}")
    # Obfuscate sensitive data
    logging.debug("Password change operation completed (sensitive details omitted)")
  1. Always use SSL/TLS for remote connections
  2. Implement proper secret rotation policies
  3. Restrict password change permissions
  4. Monitor for brute force attempts
  5. Consider multi-factor authentication where possible