[admin_user]: Added support for password and static pubkey auth

This commit adds authentication optionally with passwords or static
ssh pubkeys for the admin user.

There are now three ways to grant access to the admin user:
-  Set a password for the user, or
-  Provide a ssh public key to allow remote ssh login, or
-  Use the EC2 public key (EC2 machines only)

If a password is provided, this plugin sets the admin password. This
also re-enables password login (off by default in Jessie).

If the optional argument pubkey is present (it should be a full path
to a ssh public key), it will ensure that the ssh public key is used
to set up password less remote login for the admin user.

Only one of these options (password, or pubkey) may be specified.

If neither the password not a ssh public key location are specified,
and if the EC2 init scripts are installed, the script for fetching the
SSH authorized keys will be adjust to match the username specified.

Fixes: https://github.com/andsens/bootstrap-vz/issues/248

Signed-off-by: Manoj Srivastava <srivasta@google.com>
This commit is contained in:
Manoj Srivastava 2015-09-28 11:49:50 -07:00
parent 67284eaae5
commit a56f20657b
No known key found for this signature in database
GPG key ID: 36BD720F6F576472
2 changed files with 125 additions and 96 deletions

View file

@ -1,28 +1,36 @@
def validate_manifest(data, validator, error): def validate_manifest(data, validator, error):
import os.path import os.path
schema_path = os.path.normpath(os.path.join(os.path.dirname(__file__), 'manifest-schema.yml')) schema_path = os.path.normpath(os.path.join(os.path.dirname(__file__), 'manifest-schema.yml'))
validator(data, schema_path) validator(data, schema_path)
if ('password' in data['plugins']['admin_user'] and 'pubkey' in data['plugins']['admin_user']):
msg = 'passwd and pubkey are mutually exclusive.'
error(msg, ['plugins', 'admin_user'])
full_path = data['plugins']['admin_user']['pubkey']
if not os.path.exists(full_path):
msg = 'Could not find public key at %s' % full_path
error(msg, ['plugins', 'admin_user'])
def resolve_tasks(taskset, manifest): def resolve_tasks(taskset, manifest):
import tasks import tasks
from bootstrapvz.common.tasks import ssh from bootstrapvz.common.tasks import ssh
from bootstrapvz.providers.ec2.tasks import initd
from bootstrapvz.common.releases import jessie from bootstrapvz.common.releases import jessie
if manifest.release < jessie: if manifest.release < jessie:
taskset.update([ssh.DisableRootLogin]) taskset.update([ssh.DisableRootLogin])
if 'password' in manifest.plugins['admin_user']: if 'password' in manifest.plugins['admin_user']:
taskset.discard(ssh.DisableSSHPasswordAuthentication) taskset.discard(ssh.DisableSSHPasswordAuthentication)
taskset.add(tasks.AdminUserCredentials) taskset.add(tasks.AdminUserCredentialsPassword)
else: else:
if initd.AddEC2InitScripts in taskset or 'pubkey' in manifest.plugins['admin_user']: if 'pubkey' in manifest.plugins['admin_user']:
taskset.add(tasks.AdminUserCredentials) taskset.add(tasks.AdminUserCredentialsPublicKey)
else:
taskset.add(tasks.AdminUserCredentialsEc2)
taskset.update([tasks.AddSudoPackage, taskset.update([tasks.AddSudoPackage,
tasks.CreateAdminUser, tasks.CreateAdminUser,
tasks.PasswordlessSudo, tasks.PasswordlessSudo,
]) ])

View file

@ -1,104 +1,125 @@
from bootstrapvz.base import Task from bootstrapvz.base import Task
from bootstrapvz.common import phases from bootstrapvz.common import phases
from bootstrapvz.common.tasks.initd import InstallInitScripts from bootstrapvz.common.tasks.initd import InstallInitScripts
from bootstrapvz.providers.ec2.tasks.initd import AddEC2InitScripts
import logging import logging
import os import os
class AddSudoPackage(Task): class AddSudoPackage(Task):
description = 'Adding `sudo\' to the image packages' description = 'Adding `sudo\' to the image packages'
phase = phases.preparation phase = phases.preparation
@classmethod @classmethod
def run(cls, info): def run(cls, info):
info.packages.add('sudo') info.packages.add('sudo')
class CreateAdminUser(Task): class CreateAdminUser(Task):
description = 'Creating the admin user' description = 'Creating the admin user'
phase = phases.system_modification phase = phases.system_modification
@classmethod @classmethod
def run(cls, info): def run(cls, info):
from bootstrapvz.common.tools import log_check_call from bootstrapvz.common.tools import log_check_call
log_check_call(['chroot', info.root, log_check_call(['chroot', info.root,
'useradd', 'useradd',
'--create-home', '--shell', '/bin/bash', '--create-home', '--shell', '/bin/bash',
info.manifest.plugins['admin_user']['username']]) info.manifest.plugins['admin_user']['username']])
class PasswordlessSudo(Task): class PasswordlessSudo(Task):
description = 'Allowing the admin user to use sudo without a password' description = 'Allowing the admin user to use sudo without a password'
phase = phases.system_modification phase = phases.system_modification
@classmethod @classmethod
def run(cls, info): def run(cls, info):
sudo_admin_path = os.path.join(info.root, 'etc/sudoers.d/99_admin') sudo_admin_path = os.path.join(info.root, 'etc/sudoers.d/99_admin')
username = info.manifest.plugins['admin_user']['username'] username = info.manifest.plugins['admin_user']['username']
with open(sudo_admin_path, 'w') as sudo_admin: with open(sudo_admin_path, 'w') as sudo_admin:
sudo_admin.write('{username} ALL=(ALL) NOPASSWD:ALL'.format(username=username)) sudo_admin.write('{username} ALL=(ALL) NOPASSWD:ALL'.format(username=username))
import stat import stat
ug_read_only = (stat.S_IRUSR | stat.S_IRGRP) ug_read_only = (stat.S_IRUSR | stat.S_IRGRP)
os.chmod(sudo_admin_path, ug_read_only) os.chmod(sudo_admin_path, ug_read_only)
class AdminUserCredentials(Task): class AdminUserCredentialsPassword(Task):
description = 'Set up access credentials for the admin user' description = 'Set up access credentials for the admin user with a given password'
phase = phases.system_modification phase = phases.system_modification
predecessors = [InstallInitScripts, CreateAdminUser] predecessors = [InstallInitScripts, CreateAdminUser]
@classmethod @classmethod
def run(cls, info): def run(cls, info):
from bootstrapvz.common.exceptions import TaskError
from bootstrapvz.common.tools import sed_i
from bootstrapvz.common.tools import log_check_call from bootstrapvz.common.tools import log_check_call
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
if 'password' in info.manifest.plugins['admin_user']: log.debug('Setting the password for the admin user.')
if 'pubkey' in info.manifest.plugins['admin_user']: log_check_call(['chroot', info.root, 'chpasswd'],
msg = 'The options password and pubkey are mutually exclusive' info.manifest.plugins['admin_user']['username'] +
raise TaskError(msg) ':' + info.manifest.plugins['admin_user']['password'])
log_check_call(['chroot', info.root, 'chpasswd'], return
info.manifest.plugins['admin_user']['username'] +
':' + info.manifest.plugins['admin_user']['password'])
return class AdminUserCredentialsPublicKey(Task):
description = 'Set up access credentials for the admin user with a given public key'
phase = phases.system_modification
predecessors = [AddEC2InitScripts, CreateAdminUser]
successors = [InstallInitScripts]
@classmethod
def run(cls, info):
from bootstrapvz.common.tools import log_check_call
log = logging.getLogger(__name__)
import stat
from shutil import copy
full_path = info.manifest.plugins['admin_user']['pubkey']
log.debug('Copying public key from {path}'.format(path=full_path))
if 'ec2-get-credentials' in info.initd['install']:
log.warn('You are using a static public key for the admin account.'
' This will conflict with the ec2 public key injection mechanisn.'
' The ec2-get-credentials startup script has therefore been disabled.')
del info.initd['install']['ec2-get-credentials']
username = info.manifest.plugins['admin_user']['username']
ssh_file = os.path.join('/home/', username, '/.ssh/authorized_keys')
rel_ssh_file = os.path.realpath(info.root + '/%s' % ssh_file)
ssh_dir = os.path.dirname(ssh_file)
rel_ssh_dir = os.path.realpath(info.root + '/%s' % ssh_dir)
if not os.path.exists(rel_ssh_dir):
log.debug('Creating %s mode 700' % rel_ssh_dir)
os.mkdir(rel_ssh_dir, 0700)
else:
log.debug('setting %s mode 700' % rel_ssh_dir)
os.chmod(rel_ssh_dir, 0700)
copy(full_path, rel_ssh_file)
mode = (stat.S_IRUSR | stat.S_IWUSR)
os.chmod(rel_ssh_file, mode)
log_check_call(['chroot', info.root, 'chown', '-R', username, ssh_dir])
return
class AdminUserCredentialsEC2(Task):
description = 'Set up access credentials for the admin user using the EC2 credentials'
phase = phases.system_modification
predecessors = [InstallInitScripts, CreateAdminUser]
@classmethod
def run(cls, info):
from bootstrapvz.common.exceptions import TaskError
from bootstrapvz.common.tools import sed_i
log = logging.getLogger(__name__)
getcreds_path = os.path.join(info.root, 'etc/init.d/ec2-get-credentials') getcreds_path = os.path.join(info.root, 'etc/init.d/ec2-get-credentials')
if 'pubkey' in info.manifest.plugins['admin_user']: if os.path.exists(getcreds_path):
import stat log.debug('Updating EC2 get credentials script.')
from shutil import copy
full_path = info.manifest.plugins['admin_user']['pubkey']
if not os.path.exists(full_path):
msg = 'Could not find public key at {full_path}'.format(full_path=full_path)
raise TaskError(msg)
log.debug('Copying public key from {path}'.format(path=full_path))
if os.path.exists(getcreds_path):
log.warn('You are using a static public key for the admin account.'
' This will conflict with the ec2 public key njection mechanisn.'
' The ec2-get-credentials startup script has therefore been disabled.')
log_check_call(['chroot', info.root, 'insserv', '--remove',
'ec2-get-credentials'])
username = info.manifest.plugins['admin_user']['username'] username = info.manifest.plugins['admin_user']['username']
sed_i(getcreds_path, "username='root'",
ssh_file = os.path.join('/home/' "username='{username}'".format(username=username))
'{username}/.ssh/authorized_keys'.format(username=username)) else:
rel_ssh_file = os.path.realpath(info.root + '/{ssh_file}'.format(ssh_file=ssh_file)) raise TaskError('Could not find EC2 get credentials script.')
ssh_dir = os.path.dirname(ssh_file)
rel_ssh_dir = os.path.realpath(info.root + '/{ssh_dir}'.format(ssh_dir=ssh_dir))
if not os.path.exists(rel_ssh_dir):
log.debug('Creating {ssh_dir} mode 700'.format(ssh_dir=rel_ssh_dir))
os.mkdir(rel_ssh_dir, 0700)
else:
log.debug('setting {ssh_dir} mode 700'.format(ssh_dir=rel_ssh_dir))
os.chmod(rel_ssh_dir, 0700)
copy(full_path, rel_ssh_file)
mode = (stat.S_IRUSR | stat.S_IWUSR)
os.chmod(rel_ssh_file, mode)
log_check_call(['chroot', info.root, 'chown', '-R', username, ssh_dir])
return
log.debug('Updating EC2 get credentials script.')
username = info.manifest.plugins['admin_user']['username']
sed_i(getcreds_path, 'username=\'root\'', 'username=\'{username}\''.format(username=username))