使用Python/Fabric在命令行中更改Unix密码

8

我希望找到一种使用fabric在远程Ubuntu 10.4盒子上更新密码的方法。

我期望我的fabfile.py看起来像这样:

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))

很遗憾,我所知道的唯一可以更改密码的命令是passwd,但在Ubuntu 10.4上似乎没有办法将新(或旧)密码作为参数传递给passwd
有什么命令可以通过fabric更改Ubuntu 10.4上用户的密码?
编辑:我看了一下usermod -p,那可能行得通,但man页面不推荐使用。
编辑:由于某种原因,usermod -p在fabric上也无法工作。
此外,我尝试了mikej答案的一种(有些不安全的)变体,它确实解决了问题。
# 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")

这不是一个非常优雅的解决方案,但它有效。

谢谢阅读。

Brian


请注意,在Lucid上,usermod -p的参数是“由crypt(3)返回的加密密码”,使用SHA-512而不是明文。在usermod页面中的警告相当于说“您将在进程表中短暂地放置/etc/shadow的(通常隐藏的)哈希内容”,这取决于您的安全要求,可能并不会全部暴露。 - msw
5个回答

15
你可以使用echo将新旧密码输入到passwd中。例如:
echo -e "oldpass\\nnewpass\\nnewpass" | passwd

(-e 选项可以启用 echo 命令对反斜杠转义符的解释,使得换行符被正确地解释为换行符)


@Mikej - 感谢回复。我一直在尝试这个,但我认为我在转义方面遇到了麻烦。特别是 run("echo -e \"%s\\n%s\\n%s\" | /usr/bin/passwd" % (old_pw, new_pw, new_pw)) 不起作用(即返回“UNIX密码:passwd:身份验证令牌操纵错误”)。 - Brian M. Hunt
1
你可能需要双重转义 \ (一次用于 Python,一次用于 echo),例如每个换行符 \\\\n - mikej
@Mikej:当我从命令行运行时,它可以正常工作。然而,当我使用 fabric 运行它时,我得到以下提示:“UNIX 密码:输入新的 UNIX 密码:重复输入新的 UNIX 密码:passwd:身份验证令牌操作错误”。 - Brian M. Hunt
1
我尝试过运行run("echo -e \"%s\\\\n%s\" | passwd %s" % (passwd, passwd, user), shell=False),结果是命令echo -e "PASSWD\\nPASSWD" | passwd USER(对于passwd=PASSWDuser=USER)。然而,这导致了“UNIX密码:passwd:身份验证令牌操作错误”的结果。当我从shell中运行echo命令时,它按预期工作。 - Brian M. Hunt
1
新密码或旧密码中是否有任何需要转义的字符(尝试将密码暂时更改为简单的字母数字组合)?此外,我假设您在第一条评论中包括了新密码确认,即使您最近的评论中没有包括它? - mikej
@mijek:我使用[a-zA-Z]的密码也获得了相同的结果——但是检查是个好建议。在密码确认方面十分正确;为了简化事情,我临时从fabric的“run”切换到“sudo”(因此不需要用户当前的密码)。 - Brian M. Hunt

11

关键在于使用usermod和Python的crypt结合起来更改密码:

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)

1
不错!我想补充一下,当询问密码时,使用getpass而不是prompt可能是一个好主意。 - Manuel Ceron
1
crypt 函数中的盐值只能使用 [a–zA–Z0–9./] 中的两个字符,因此 'salt' 相当于 'sa',请参阅文档 - schemacs
好的,谢谢你。 - Roshambo

5

我在Ubuntu 11.04上使用chpasswd


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

注意: 通常这种模式不起作用:
$ sudo echo bla | restricted_command

因为只有“echo”获得了特权,而“restricted_command”没有。

然而,在这里它能够工作,是因为当使用shell=True(默认情况下)调用fabric.api.sudo时,fabric会像这样组装命令:

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

sudo会生成一个新的shell(/bin/bash),并以root特权运行,然后提升了权限的shell执行命令。

另一种使用sudo进行管道传输的方法是使用sudo tee:sudo tee


3

出于兴趣,我需要在一组Solaris系统上执行类似的任务(添加大量用户并设置其密码)。 Solaris usermod没有--password选项,因此过去我使用Expect来完成这个任务,但编写Expect脚本可能很痛苦。

因此,这次我将使用Python的crypt.crypt,直接编辑/ etc / shadow文件(当然备份)。 http://docs.python.org/release/2.6.1/library/crypt.html

评论者建议使用各种echo命令传输到passwd中。据我所知,这永远不起作用,因为passwd程序被编程忽略从stdin输入,并且只接受来自交互式tty的输入。请参阅http://en.wikipedia.org/wiki/Expect


1
我尝试了其他方法,但没有成功。我想分享一下我用于一次性丢弃脚本的方法。它使用自动回复程序在提示框中输入密码。然后我立即使所有密码过期,这样用户就有机会选择自己的密码。这不是最安全的方法,但根据您的使用情况,可能会有用。
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://dev59.com/bU7Sa4cB1Zd3GeqP4Y7o#5137688.
    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)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接