Change Unix password from command line over Python/Fabric
Asked Answered
H

5

8

I would like a way to update my password on a remote Ubuntu 10.4 box with fabric.

I would expect my fabfile.py would look something like this:

def update_password(old_pw, new_pw):
    # Connects over ssh with a public key authentication
    run("some_passwd_cmd --old %s --new %s" % (old_pw, new_pd))

Unfortunately the only command I know of that lets one change the password is passwd, and on Ubuntu 10.4 there doesn't seem to be any way to pass in the new (or old) password as an argument to passwd.

What command could one use to change a user's password on Ubuntu 10.4 via fabric?

EDIT: I've had a look at usermod -p, and that may work but it isn't recommended by the man page.

EDIT: For some reason usermod -p wasn't working either over fabric.

As well, I've tried a (somewhat insecure) variation on mikej's answer that did solve the problem:

# connecting & running as root.
from fabric.api import *
from fabric.contrib import files

files.append("%s\n%s" % (passwd, passwd), '.pw.tmp')
# .pw.tmp:
# PASSWD
# PASSWD

run("passwd %s < .pw.tmp" % user)

run("rm .pw.tmp")

It's not a very elegant solution, but it works.

Thank you for reading.

Brian

Horvitz answered 20/6, 2010 at 19:11 Comment(1)
Note that on Lucid, the argument to usermod -p is "The encrypted password, as returned by crypt(3)" using SHA-512 not the plaintext. The caveat in the usermod page is equivalent to saying "you'd be putting the (normally hidden) hashed content of /etc/shadow in the process table for a brief time", which depending on your security requirements, may not be all that revealing.Cartwheel
S
15

You could feed the new and old passwords into passwd using echo e.g.

echo -e "oldpass\\nnewpass\\nnewpass" | passwd

(the -e option for echo enables interpretation of backslash escapes so the newlines are interpreted as such)

Sodomite answered 20/6, 2010 at 19:30 Comment(6)
@Mikej - thanks for the reply. I've been trying this, but I'm having what I think is trouble with escaping. In particular run("echo -e \"%s\\n%s\\n%s\" | /usr/bin/passwd" % (old_pw, new_pw, new_pw)) doesn't work (i.e. returns "UNIX password: passwd: Authentication token manipulation error")Horvitz
You might need to double escape the \ (once for Python and once for echo) e.g. \\\\n for each newlineSodomite
@Mikej: When I run this from the the command line, it works fine. However, when I run it over fabric, I get the following: UNIX password: Enter new UNIX password: Retype new UNIX password: passwd: Authentication token manipulation errorHorvitz
I've tried run("echo -e \"%s\\\\n%s\" | passwd %s" % (passwd, passwd, user), shell=False) results in the command echo -e "PASSWD\\nPASSWD" | passwd USER (for passwd=PASSWD, user=USER). Which, alas, results in 'UNIX password: passwd: Authentication token manipulation error'. When I run the echo command from the shell it works as expected.Horvitz
Are there any characters in the new or old password that might also need escaping (try temporarily changing the password to a simple alphanumeric one.) Also, I presume you are including the new password confirmation as in your first comment even though you haven't included it in the most recent comment?Sodomite
@mijek: I get the same results with passwords that are [a-zA-Z] - but good suggestion to check. Quite correct on the password confirmation; in the interim (to simplify things) I switched from fabric's run to sudo (so the user's current password is not required).Horvitz
J
11

The trick is to use a combination of usermod and Python’s crypt to change your password:

from crypt import crypt
from getpass import getpass
from fabric.api import *

def change_password(user):
    password = getpass('Enter a new password for user %s:' % user)
    crypted_password = crypt(password, 'salt')
    sudo('usermod --password %s %s' % (crypted_password, user), pty=False)
Jiminez answered 28/2, 2011 at 2:29 Comment(3)
Nice! I would like to add that it's maybe a good idea to use getpass instead of prompt when asking for the password.Horrendous
salt for crypt can only be two characters from [a–zA–Z0–9./], so 'salt' is the same as 'sa', see docs.Thundering
Good call. Thanks for that.Jiminez
M
5

I use chpasswd on Ubuntu 11.04

fabric.api.sudo('echo %s:%s | chpasswd' % (user, pass))

Note: Normally this pattern doesn't work:

$ sudo echo bla | restricted_command

because only the 'echo' gets elevated privileges, not the 'restricted_command'.

However, here it works because when fabric.api.sudo is caled with shell=True (the default), fabric assembles the command like this:

$ sudo -S -p <sudo_prompt> /bin/bash -l -c "<command>"

sudo spawns a new shell (/bin/bash), running with root privileges, and then that escalated shell runs the command.

Another way to pipe with sudo is to use sudo tee:

Maye answered 18/6, 2011 at 4:9 Comment(0)
I
3

Out of interest, I have to do a similar task on a collection of Solaris boxes (add a whole lot of users, set their password). Solaris usermod doesn't have a --password option, so in the past I've used Expect to do this, but writing Expect scripts can be painful.

So this time I'm going to use Python's crypt.crypt, edit /etc/shadow directly (with backups, of course). http://docs.python.org/release/2.6.1/library/crypt.html

Commenters have suggested using various echo incantations piped to passwd. AFAIK this will never work, as passwd is programmed to ignore input from stdin and only accept input from an interactive tty. See http://en.wikipedia.org/wiki/Expect

Israel answered 15/3, 2011 at 3:43 Comment(0)
C
1

I had no luck with the other methods. Thought I would share my method that I used for a once-off throwaway script.

It uses auto-responder to type in passwords at the prompts. I then immediately expire all the passwords so that users have a chance to choose their own.

This is not the most secure method, but depending on your use case it may be useful.

from collections import namedtuple
from getpass import getpass
import hashlib
from invoke import Responder
import uuid

from fabric import Connection, Config


User = namedtuple('UserRecord', ('name', 'password'))


def set_passwords(conn, user):
    print(f'Setting password for user, {user.name}')
    responder = Responder(
        pattern=r'(?:Enter|Retype) new UNIX password:',
        response=f'{user.password}\n',
    )
    result = conn.sudo(f'passwd {user.name}', warn=True, hide='both',
                       user='root', pty=True, watchers = [responder])
    if result.exited is not 0:
        print(f'Error, could not set password for user, "{user.name}". command: '
              f'{result.command}; exit code: {result.exited}; stderr: '
              f'{result.stderr}')
    else:
        print(f'Successfully set password for {user.name}')


def expire_passwords(conn, user):
    print(f'Expiring password for user, {user.name}')
    cmd = f'passwd --expire {user.name}'
    result = conn.sudo(cmd, warn=True, user='root')
    if result.exited is not 0:
        print(f'Error, could not expire password for user, "{user.name}". '
              f'command: {result.command}; exit code: {result.exited}; stderr: '
              f'{result.stderr}')
    else:
        print(f'Successfully expired password for {user.name}')


def gen_password(seed_string):
    # Don't roll your own crypto. This is for demonstration only and it is
    # expected to only create a temporary password that requires changing upon
    # initial login. I am no cryptography expert, hence this alternative
    # simplified answer to the one that uses crypt, salt, etc - 
    # https://mcmap.net/q/1239763/-change-unix-password-from-command-line-over-python-fabric.
    seed_str_enc = seed_string.encode(encoding='UTF-8')
    uuid_obj = uuid.UUID(int=int(hashlib.md5(seed_str_enc).hexdigest(), 16))
    return str(uuid_obj)[:8]


def some_function_that_returns_something_secret(conn):
    return f'dummy-seed-{conn}'

sudo_pass = getpass('Enter your sudo password:')
config = Config(overrides={'sudo': {'password': sudo_pass}})
with Connection('vm', config=config) as vm_conn:
    print(f'Making a new connection to {vm_conn.host}.')
    # I usually use the sudo connection here to run a command that returns a
    # reproducible string that only the sudo user could get access to be used 
    # for user_record.password bellow. Proceed with caution, this is not a 
    # recommended approach
    seed = some_function_that_returns_something_secret(vm_conn)
    user_record = User(name='linux_user', password=gen_password(seed))
    set_passwords(vm_conn, user_record)
    expire_passwords(vm_conn, user_record)
    print(f'Done! Disconnecting from {vm_conn.host}.')

# So that you know the temporary password, print user_record or save to file
# `ssh linux_user@vm` and it should insist that you change password
print(user_record)

Castilian answered 22/6, 2019 at 9:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.