Skip to content

Commit

Permalink
Merge branch 'user-key-managment-exec-command' into 'master'
Browse files Browse the repository at this point in the history
Better user key management and exec command

See merge request systra/qeto/infra/deploy!5
  • Loading branch information
cpontvieux-systra committed Nov 25, 2020
2 parents dc60364 + d73ee1b commit 7e27387
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 21 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
CHANGELOG
=========

Next
----
- Security updates (docker images)
- Support for multiple (ssh) keys per user (`listkey`, `addkey`, `delkey`)
- Support for `exec` action to enter an application service

version 2.2.0
-------------
- Rights and user management
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Configuration
Users allowed to upload [*DCA*](https://github.com/jrd/dca_format) files should have their public keys in `/etc/dca-authorized-keys`.

Users allowed to act on deployments should have their public keys in `/etc/deploy-authorized-keys`.

You don't have to do anything after modifying this file for the content to be taken into account.

Admin usage
Expand Down Expand Up @@ -81,14 +82,19 @@ This will exit with `0` status if undeploy was ok, greater number in case of err

See `ssh deploy@node help` for an exhaustive list of all actions and options.

### Hacking as admin
### Exec-ing into containers

Be sure to force a TTY with ssh: `ssh -t` or `RequestTTY yes` in `.ssh/config`.

`sshi -t deploy@node exec my_app integ my_service`

If you can log into *node* and change to the `compose` account or your account is in the `docker` group:
### Hack for admin

- Connect into *node* and change to the `compose` account (or your account if in the `docker` group).
- Go into you app and environment folder, containing `docker-compose.yml` file, for instance `cd myapp/prod` from `compose` home dir.
- Then use any `docker-compose` command, like `exec` to enter a container.
- Use any `docker-compose` command, like `exec` to enter a container.

⚠ Keep it mind that the compose app is handled by a systemd service so **don't start or stop** the compose while the service is running.
⚠ Keep in mind that the compose app is handled by a systemd service so **don't start or stop** the compose while the service is running.
Better use the `capp start|stop` commands or `sudo systemctl start|stop` commands.

License and authors
Expand Down
99 changes: 87 additions & 12 deletions capp
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class Unit(Enum):
Env = EnumNames('Env', ('dev', 'integ', 'staging', 'demo', 'prod'))
Right = EnumNames('Right', (
'DCA_READ', 'DCA_WRITE', 'APP_LIST',
'DEPLOY', 'START', 'STOP', 'STATUS', 'LOGS',
'DEPLOY', 'START', 'STOP', 'STATUS', 'LOGS', 'EXEC',
'USER_LIST', 'USER_ADD', 'USER_CHANGE', 'USER_DELETE',
'RIGHT_LIST', 'RIGHT_ADD', 'RIGHT_DELETE',
))
Expand Down Expand Up @@ -343,6 +343,22 @@ class CApp:
sp.add_argument('--nopager', action='store_false', dest='pager',
help="Logs are output directly, without a pager")
sp.set_defaults(func=self.action_logs)
# exec
descr = (
"Enter a service container for the application specified."
"\nThe application should already exist and up on this node."
"\nWithout arguments, a shell (/bin/sh) will be used."
)
epilog = (
"Required rights:"
f"\n- {Right.EXEC.name} with env and app regexes matching the environment name and app name accordingly"
)
sp = sps.add_parser('exec', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr, epilog=epilog)
add_app_env_args(sp)
sp.add_argument('service', metavar='SERVICE', help="application service name")
sp.add_argument('--args', nargs='?', const=None, default='/bin/sh', dest='args', metavar='ARGS',
help="Any arguments to exec for the service. Default to /bin/sh")
sp.set_defaults(func=self.action_exec)
# users
descr = "User accounts management commands."
sp = sps.add_parser('users', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr)
Expand All @@ -365,23 +381,46 @@ class CApp:
rsp.add_argument('user', metavar='USER', help="User account name")
rsp.add_argument('pkey', metavar='PUBLIC KEY', help="user public key")
rsp.set_defaults(func=self.action_add_user)
descr = "Change the public key of a user account."
descr = "Delete a user account."
epilog = (
"Required rights:"
f"\n- {Right.USER_DELETE.name}"
)
rsp = usps.add_parser('delete', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr, epilog=epilog)
rsp.add_argument('user', metavar='USER', help="User account name")
rsp.set_defaults(func=self.action_delete_user)
# user keys
descr = "User public keys management commands."
sp = usps.add_parser('key', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr)
ksps = sp.add_subparsers(title='actions', metavar='ACTION', required=True,
description="Use ACTION -h|--help to get full help on any action",
help="One of the following action is required\n ")
descr = "List the user account public keys."
epilog = (
"Required rights:"
f"\n- {Right.USER_LIST.name}"
)
rsp = ksps.add_parser('list', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr, epilog=epilog)
rsp.add_argument('user', metavar='USER', help="User account name")
rsp.set_defaults(func=self.action_list_pkeys)
descr = "Add a public key to a user account."
epilog = (
"Required rights:"
f"\n- {Right.USER_CHANGE.name} when changing another user"
f"\n- {Right.USER_CHANGE.name} when adding a key to another user"
)
rsp = usps.add_parser('change', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr, epilog=epilog)
rsp = ksps.add_parser('add', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr, epilog=epilog)
rsp.add_argument('user', metavar='USER', help="User account name")
rsp.add_argument('pkey', metavar='PUBLIC KEY', help="new user public key")
rsp.set_defaults(func=self.action_change_pkey)
descr = "Delete a user account."
rsp.set_defaults(func=self.action_add_pkey)
descr = "Delete a public key from a user account."
epilog = (
"Required rights:"
f"\n- {Right.USER_DELETE.name}"
f"\n- {Right.USER_CHANGE.name} when deleting a key to another user"
)
rsp = usps.add_parser('delete', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr, epilog=epilog)
rsp = ksps.add_parser('delete', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr, epilog=epilog)
rsp.add_argument('user', metavar='USER', help="User account name")
rsp.set_defaults(func=self.action_delete_user)
rsp.add_argument('pkey', metavar='PUBLIC KEY', help="existing user public key")
rsp.set_defaults(func=self.action_delete_pkey)
# rights
descr = "Rights management commands."
sp = sps.add_parser('rights', formatter_class=PositionalFirstHelpFormatter, help=descr, description=descr)
Expand Down Expand Up @@ -672,6 +711,7 @@ class CApp:
svc_def['cpu_shares'] = limits.cpu[svc] * 1024 // 4
with open(target_dir / 'docker-compose.yml', 'w') as f:
dump(dc, f)
run(['docker-compose', '-f', str(target_dir / 'docker-compose.yml'), 'pull', '--ignore-pull-failures', '--quiet'], text=True)
deps_file = Path(self.compose_dirs_config['compose_dir']) / self.compose_dirs_config['deps_file']
with open(deps_file, 'r+') as f:
deps = [line.strip() for line in f if not line.startswith(f'{app}/{target_env}:')]
Expand All @@ -686,6 +726,7 @@ class CApp:
run(['sudo', 'systemctl', 'restart', systemd_svc], check=True, text=True)
elif run(['systemctl', 'is-active', systemd_svc], capture_output=True).returncode == 0:
run(['sudo', 'systemctl', 'stop', systemd_svc], check=True, text=True)
run(['docker', 'image', 'prune', '-f'], text=True)
self._post_deploy(app, target_env, version, target_dir / 'docker-compose.yml', target_dir)

def _get_systemd_service_name(self, app, env=None):
Expand Down Expand Up @@ -1017,6 +1058,15 @@ class CApp:
else:
execlp('journalctl', 'journalctl', '--no-pager', '-u', svc)

def action_exec(self, args):
app = args.app
target_env = args.env
self._check_app_name_target_env(app, target_env)
self.check_right(Right.EXEC, app=app, env=target_env)
self.trace_action('exec', vars(args))
target_dir = Path(self.compose_dirs_config['compose_dir']) / app / target_env
run(['docker-compose', 'exec', args.service, args.args], cwd=target_dir)

def action_list_users(self, args):
self.check_right(Right.USER_LIST)
self.trace_action('users-list', vars(args))
Expand Down Expand Up @@ -1045,17 +1095,42 @@ class CApp:
rights = {right: ('.*', '.*') for right in Right.names()}
self._update_user_rights(user, rights)

def action_change_pkey(self, args):
def action_list_pkeys(self, args):
user = args.user
if user != self.get_ssh_user():
self.check_right(Right.USER_LIST)
user_pkey_path = self.users_dir / user
if not user_pkey_path.exists():
raise ValueError(f"user '{user}' does not exist")
self.trace_action('users-listkeys', vars(args))
print(user_pkey_path.open().read())

def action_add_pkey(self, args):
user = args.user
pkey = args.pkey
if user != self.get_ssh_user():
self.check_right(Right.USER_CHANGE)
user_pkey_path = self.users_dir / user
if not user_pkey_path.exists():
raise ValueError(f"user '{user}' does not exist")
self.trace_action('users-change', vars(args))
self.trace_action('users-addkey', vars(args))
pkeys = set(user_pkey_path.open().read().split('\n'))
pkeys.add(pkey)
with open(user_pkey_path, 'w') as f:
f.write(pkey)
f.write('\n'.join(pkeys))

def action_delete_pkey(self, args):
user = args.user
pkey = args.pkey
if user != self.get_ssh_user():
self.check_right(Right.USER_CHANGE)
user_pkey_path = self.users_dir / user
if not user_pkey_path.exists():
raise ValueError(f"user '{user}' does not exist")
self.trace_action('users-delkey', vars(args))
pkeys = set(user_pkey_path.open().read().split('\n')) - set((pkey, ))
with open(user_pkey_path, 'w') as f:
f.write('\n'.join(pkeys))

def action_delete_user(self, args):
self.check_right(Right.USER_DELETE)
Expand Down
2 changes: 1 addition & 1 deletion dca/dca_keys_entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ if [ "$1" = "dca" ]; then
for f in /etc/capp/users/*; do
u=$(basename "$f")
if [ -e /etc/capp/rights/$u ] && awk '{print $1}' /etc/capp/rights/$u | grep -q 'DCA_WRITE'; then
echo "environment=\"SSH_USER=$u\" $(cat "$f")"
while IFS= read -r line; do echo "environment=\"SSH_USER=$u\" $line"; done <<< $(cat "$f")
fi
done
fi
Expand Down
2 changes: 1 addition & 1 deletion dca/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "2.4"
services:
scp:
image: panubo/sshd:1.2.1
image: panubo/sshd:1.3.0
environment:
- "SCP_MODE=true"
- "MOTD=Docker Compose Archives"
Expand Down
2 changes: 1 addition & 1 deletion get_deploy_keys
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
if [ "$1" = "deploy" ]; then
for f in /etc/capp/users/*; do
u=$(basename "$f")
echo "environment=\"SSH_USER=$u\" $(cat "$f")"
while IFS= read -r line; do echo "environment=\"SSH_USER=$u\" $line"; done <<< $(cat "$f")
done
fi
4 changes: 2 additions & 2 deletions proxy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '2.4'
services:
nginx:
image: nginx
image: nginx:alpine
container_name: nginx-proxy
networks:
- "network"
Expand All @@ -27,7 +27,7 @@ services:
- /var/docker-volumes/nginx-proxy/certs:/etc/nginx/certs:ro
- nginx-conf:/etc/nginx/conf.d:rw
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion:v1.13
image: jrcs/letsencrypt-nginx-proxy-companion:v1.13.1
container_name: le-gen
environment:
- "NGINX_PROXY_CONTAINER=nginx-proxy"
Expand Down

0 comments on commit 7e27387

Please sign in to comment.