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)")
- Always use SSL/TLS for remote connections
- Implement proper secret rotation policies
- Restrict password change permissions
- Monitor for brute force attempts
- Consider multi-factor authentication where possible