From ee840c730c1e20fcf54c5fceeaf9158d94e1cd87 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 23 Mar 2015 20:24:08 -0400 Subject: [PATCH 01/16] status badges updated to use shields.io standard --- buildstatus/building.svg | 2 +- buildstatus/failed.svg | 2 +- buildstatus/none.svg | 2 +- buildstatus/ready.svg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/buildstatus/building.svg b/buildstatus/building.svg index dc7aeae7b..8e26edf87 100644 --- a/buildstatus/building.svg +++ b/buildstatus/building.svg @@ -1 +1 @@ -Docker ImageDocker Imagebuildingbuilding \ No newline at end of file +containercontainerbuildingbuilding \ No newline at end of file diff --git a/buildstatus/failed.svg b/buildstatus/failed.svg index 069d9f4e4..cc74c2381 100644 --- a/buildstatus/failed.svg +++ b/buildstatus/failed.svg @@ -1 +1 @@ -Docker ImageDocker Imagebuild failedbuild failed \ No newline at end of file +containercontainerfailedfailed \ No newline at end of file diff --git a/buildstatus/none.svg b/buildstatus/none.svg index 3c31d29b1..0e4680acf 100644 --- a/buildstatus/none.svg +++ b/buildstatus/none.svg @@ -1 +1 @@ -Docker ImageDocker Imagenonenone \ No newline at end of file +containercontainernonenone \ No newline at end of file diff --git a/buildstatus/ready.svg b/buildstatus/ready.svg index 111262e3b..50e451a01 100644 --- a/buildstatus/ready.svg +++ b/buildstatus/ready.svg @@ -1 +1 @@ -Docker ImageDocker Imagereadyready \ No newline at end of file +containercontainerreadyready \ No newline at end of file From e4b659f1071036805569afb9f564d9b961d36a48 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 25 Mar 2015 18:43:12 -0400 Subject: [PATCH 02/16] Add support for encrypted client tokens via basic auth (for the docker CLI) and a feature flag to disable normal passwords --- auth/auth.py | 3 +- config.py | 4 ++ data/users.py | 26 ++++++++++ endpoints/api/user.py | 51 ++++++++++++++++++- endpoints/index.py | 4 +- .../directives/config/config-setup-tool.html | 30 ++++++++++- static/js/pages/user-admin.js | 15 ++++++ static/js/services/ui-service.js | 42 +++++++++++++++ static/partials/user-admin.html | 32 ++++++++++++ test/test_api_security.py | 23 ++++++++- 10 files changed, 222 insertions(+), 8 deletions(-) diff --git a/auth/auth.py b/auth/auth.py index 79e07e3be..30e2f68db 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -114,7 +114,8 @@ def _process_basic_auth(auth): logger.debug('Invalid robot or password for robot: %s' % credentials[0]) else: - authenticated = authentication.verify_user(credentials[0], credentials[1]) + (authenticated, error_message) = authentication.verify_user(credentials[0], credentials[1], + basic_auth=True) if authenticated: logger.debug('Successfully validated user: %s' % authenticated.username) diff --git a/config.py b/config.py index 339ffca34..2d50138af 100644 --- a/config.py +++ b/config.py @@ -165,6 +165,10 @@ class DefaultConfig(object): # Feature Flag: Whether users can be renamed FEATURE_USER_RENAME = False + # Feature Flag: Whether non-encrypted passwords (as opposed to encrypted tokens) can be used for + # basic auth. + FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = False + BUILD_MANAGER = ('enterprise', {}) DISTRIBUTED_STORAGE_CONFIG = { diff --git a/data/users.py b/data/users.py index 9e01e4d45..c7a6128db 100644 --- a/data/users.py +++ b/data/users.py @@ -1,6 +1,7 @@ import ldap import logging +from flask.sessions import SecureCookieSessionInterface, BadSignature from util.validation import generate_valid_usernames from data import model @@ -138,5 +139,30 @@ class UserAuthentication(object): app.extensions['authentication'] = users return users + def verify_user(self, username_or_email, password, basic_auth=False): + # First try to decode the password as a signed token. + if basic_auth: + from app import app + import features + + ser = SecureCookieSessionInterface().get_signing_serializer(app) + + try: + token_data = ser.loads(password) + password = token_data.get('password', password) + except BadSignature: + # This is a normal password. + if features.REQUIRE_ENCRYPTED_BASIC_AUTH: + msg = ('Client login with passwords is disabled. Please generate a client token ' + + 'and use it in place of your password.') + return (None, msg) + + result = self.state.verify_user(username_or_email, password) + if result: + return (result, '') + else: + return (result, 'Invalid password') + + def __getattr__(self, name): return getattr(self.state, name, None) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 6c3cafd63..d10807940 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -1,9 +1,11 @@ import logging import json +from random import SystemRandom from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity +from flask.sessions import SecureCookieSessionInterface from peewee import IntegrityError from app import app, billing as stripe, authentication, avatar @@ -335,13 +337,58 @@ class PrivateRepositories(ApiResource): } +@resource('/v1/user/clientkey') +@internal_only +class ClientKey(ApiResource): + """ Operations for returning an encrypted key which can be used in place of a password + for the Docker client. """ + schemas = { + 'GenerateClientKey': { + 'id': 'GenerateClientKey', + 'type': 'object', + 'required': [ + 'password', + ], + 'properties': { + 'password': { + 'type': 'string', + 'description': 'The user\'s password', + }, + } + } + } + + @require_user_admin + @nickname('generateUserClientKey') + @validate_json_request('GenerateClientKey') + def post(self): + """ Return's the user's private client key. """ + username = get_authenticated_user().username + password = request.get_json()['password'] + + (result, error_message) = authentication.verify_user(username, password) + if not result: + raise request_error(message=error_message) + + ser = SecureCookieSessionInterface().get_signing_serializer(app) + data_to_sign = { + 'password': password, + 'nonce': SystemRandom().randint(0, 10000000000) + } + + encrypted = ser.dumps(data_to_sign) + return { + 'key': encrypted + } + + def conduct_signin(username_or_email, password): needs_email_verification = False invalid_credentials = False verified = None try: - verified = authentication.verify_user(username_or_email, password) + (verified, error_message) = authentication.verify_user(username_or_email, password) except model.TooManyUsersException as ex: raise license_error(exception=ex) @@ -407,7 +454,7 @@ class ConvertToOrganization(ApiResource): # Ensure that the sign in credentials work. admin_password = convert_data['adminPassword'] - admin_user = authentication.verify_user(admin_username, admin_password) + (admin_user, error_message) = authentication.verify_user(admin_username, admin_password) if not admin_user: raise request_error(reason='invaliduser', message='The admin user credentials are not valid') diff --git a/endpoints/index.py b/endpoints/index.py index 2df427601..e42696777 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -109,7 +109,7 @@ def create_user(): issue='robot-login-failure') if authentication.user_exists(username): - verified = authentication.verify_user(username, password) + (verified, error_message) = authentication.verify_user(username, password, basic_auth=True) if verified: # Mark that the user was logged in. event = userevents.get_event(username) @@ -121,7 +121,7 @@ def create_user(): event = userevents.get_event(username) event.publish_event_data('docker-cli', {'action': 'loginfailure'}) - abort(400, 'Invalid password.', issue='login-failure') + abort(400, error_message, issue='login-failure') elif not features.USER_CREATION: abort(400, 'User creation is disabled. Please speak to your administrator.') diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 6c774dcf7..9be7ddd37 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -34,7 +34,7 @@ - User Creation: + User Creation:
@@ -46,6 +46,23 @@
+ + Encrypted Client Tokens: + +
+ + +
+
+ If enabled, users will not be able to login from the Docker command + line with a non-encrypted password and must generate an encrypted + token to use. +
+
+ This feature is highly recommended for setups with LDAP authentication, as Docker currently stores passwords in plaintext on user's machines. +
+ + @@ -293,6 +310,16 @@

+
+ It is highly recommended to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in plain-text! + Enable this requirement now. +
+ +
+ Note: The "Require Encrypted Client Tokens" feature is currently enabled which will + prevent LDAP passwords from being saved as plain-text by the Docker client. +
+ @@ -305,7 +332,6 @@
Authentication:
- diff --git a/static/js/pages/user-admin.js b/static/js/pages/user-admin.js index 6af34d264..43a56388a 100644 --- a/static/js/pages/user-admin.js +++ b/static/js/pages/user-admin.js @@ -196,6 +196,21 @@ }); }; + $scope.generateClientToken = function() { + var generateToken = function(password) { + var data = { + 'password': password + }; + + ApiService.generateUserClientKey(data).then(function(resp) { + $scope.generatedClientToken = resp['key']; + $('#clientTokenModal').modal({}); + }, ApiService.errorDisplay('Could not generate token')); + }; + + UIService.showPasswordDialog('Enter your password to generate a client token:', generateToken); + }; + $scope.detachExternalLogin = function(kind) { var params = { 'servicename': kind diff --git a/static/js/services/ui-service.js b/static/js/services/ui-service.js index 2e857e8fa..4b8464e9e 100644 --- a/static/js/services/ui-service.js +++ b/static/js/services/ui-service.js @@ -92,5 +92,47 @@ angular.module('quay').factory('UIService', [function() { return new CheckStateController(items, opt_checked); }; + uiService.showPasswordDialog = function(message, callback, opt_canceledCallback) { + var success = function() { + var password = $('#passDialogBox').val(); + $('#passDialogBox').val(''); + callback(password); + }; + + var canceled = function() { + $('#passDialogBox').val(''); + opt_canceledCallback && opt_canceledCallback(); + }; + + var box = bootbox.dialog({ + "message": message + + '' + + '' + + '', + "title": 'Please Verify', + "buttons": { + "verify": { + "label": "Verify", + "className": "btn-success", + "callback": success + }, + "close": { + "label": "Cancel", + "className": "btn-default", + "callback": canceled + } + } + }); + + box.bind('shown.bs.modal', function(){ + box.find("input").focus(); + box.find("form").submit(function() { + if (!$('#passDialogBox').val()) { return; } + box.modal('hide'); + success(); + }); + }); + }; + return uiService; }]); diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 2e3249764..4d803689d 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -33,6 +33,7 @@
  • Account E-mail
  • Robot Accounts
  • Change Password
  • +
  • Client Token
  • External Logins
  • Authorized Applications
  • @@ -99,6 +100,16 @@ + +
    +
    Click the "Generate" button below to generate a client token that can be used in place of your password for the Docker + command line.
    + + +
    +
    @@ -370,6 +381,27 @@
    + + + + +
  • LDAP URI:
    diff --git a/static/js/pages/user-admin.js b/static/js/pages/user-admin.js index 43a56388a..8be13df30 100644 --- a/static/js/pages/user-admin.js +++ b/static/js/pages/user-admin.js @@ -208,7 +208,7 @@ }, ApiService.errorDisplay('Could not generate token')); }; - UIService.showPasswordDialog('Enter your password to generate a client token:', generateToken); + UIService.showPasswordDialog('Enter your password to generated an encrypted version:', generateToken); }; $scope.detachExternalLogin = function(kind) { diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 4d803689d..9dee0f5ca 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -32,8 +32,7 @@
  • Account E-mail
  • Robot Accounts
  • -
  • Change Password
  • -
  • Client Token
  • +
  • Password
  • External Logins
  • Authorized Applications
  • @@ -100,16 +99,6 @@ - -
    -
    Click the "Generate" button below to generate a client token that can be used in place of your password for the Docker - command line.
    - - -
    -
    @@ -150,8 +139,31 @@
    - +
    + +
    +
    +
    Generate Encrypted Password
    + +
    +
    + Due to Docker storing passwords entered on the command line in plaintext, it is highly recommended to use the button below to generate an an encrypted version of your password. +
    + +
    + This installation is set to require encrypted passwords when + using the Docker command line interface. To generate an encrypted password, click the button below. +
    + + +
    +
    +
    + +
    Change Password
    @@ -163,6 +175,9 @@ Password changed successfully
    +
    Note: Changing your password will also invalidate any generated encrypted passwords.
    + +
    -
  • - +
    Encrypted Client Tokens:Encrypted Client Password:
    - +
    If enabled, users will not be able to login from the Docker command line with a non-encrypted password and must generate an encrypted - token to use. + password to use.
    This feature is highly recommended for setups with LDAP authentication, as Docker currently stores passwords in plaintext on user's machines. @@ -311,12 +311,12 @@
    - It is highly recommended to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in plaintext! + It is highly recommended to require encrypted client passwords. LDAP passwords used in the Docker client will be stored in plaintext! Enable this requirement now.
    - Note: The "Require Encrypted Client Tokens" feature is currently enabled which will + Note: The "Require Encrypted Client Passwords" feature is currently enabled which will prevent LDAP passwords from being saved as plaintext by the Docker client.
    From a7b6cb5c23deea976783e3d16655992d61dc2144 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 26 Mar 2015 17:45:43 -0400 Subject: [PATCH 13/16] Fix handling of byte strings and large ints --- data/users.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/data/users.py b/data/users.py index 10c909cb8..3d763c9b6 100644 --- a/data/users.py +++ b/data/users.py @@ -148,19 +148,24 @@ class UserAuthentication(object): """ Returns the secret key to use for encrypting and decrypting. """ from app import app app_secret_key = app.config['SECRET_KEY'] + secret_key = None # First try parsing the key as an int. try: big_int = int(app_secret_key) - secret_key = bytearray.fromhex('{:02x}'.format(big_int)) + secret_key = str(bytearray.fromhex('{:02x}'.format(big_int))) except ValueError: - secret_key = app_secret_key + pass # Next try parsing it as an UUID. - try: - secret_key = uuid.UUID(app_secret_key).bytes - except ValueError: - secret_key = app_secret_key + if secret_key is None: + try: + secret_key = uuid.UUID(app_secret_key).bytes + except ValueError: + pass + + if secret_key is None: + secret_key = str(bytearray(map(ord, app_secret_key))) # Otherwise, use the bytes directly. return ''.join(itertools.islice(itertools.cycle(secret_key), 32)) From 384d6083c4297412d38376c742a8b50eb92d6480 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 26 Mar 2015 20:04:32 -0400 Subject: [PATCH 14/16] Make sure to conduct login after the password change now that the session will be invalidated for the user --- endpoints/api/user.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 9ccb1d7aa..b5d260516 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -225,8 +225,13 @@ class User(ApiResource): if 'password' in user_data: logger.debug('Changing password for user: %s', user.username) log_action('account_change_password', user.username) + + # Change the user's password. model.change_password(user, user_data['password']) + # Login again to reset their session cookie. + common_login(user) + if features.MAILING: send_password_changed(user.username, user.email) From 6eead7c860076123a305b249d338fac6bd5b5f8c Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Fri, 27 Mar 2015 15:28:08 -0400 Subject: [PATCH 15/16] Add logentries reporting to the ephemeral builders. --- buildman/manager/executor.py | 1 + buildman/templates/cloudconfig.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/buildman/manager/executor.py b/buildman/manager/executor.py index b548420f5..b6a293fc0 100644 --- a/buildman/manager/executor.py +++ b/buildman/manager/executor.py @@ -69,6 +69,7 @@ class BuilderExecutor(object): manager_hostname=manager_hostname, coreos_channel=coreos_channel, worker_tag=self.executor_config['WORKER_TAG'], + logentries_token=self.executor_config.get('LOGENTRIES_TOKEN', None), ) diff --git a/buildman/templates/cloudconfig.yaml b/buildman/templates/cloudconfig.yaml index 51bb2f090..29f7ccc5a 100644 --- a/buildman/templates/cloudconfig.yaml +++ b/buildman/templates/cloudconfig.yaml @@ -12,6 +12,9 @@ write_files: REALM={{ realm }} TOKEN={{ token }} SERVER=wss://{{ manager_hostname }} + {% if logentries_token -%} + LOGENTRIES_TOKEN={{ logentries_token }} + {%- endif %} coreos: update: @@ -29,3 +32,10 @@ coreos: flattened=True, restart_policy='no' ) | indent(4) }} + {% if logentries_token -%} + {{ dockersystemd('builder-logs', + 'quay.io/kelseyhightower/journal-2-logentries', + extra_args='--env-file /root/overrides.list -v /run/journald.sock:/run/journald.sock', + after_units=['quay-builder'] + ) | indent(4) }} + {%- endif %} From b10fd4ff22e957adc5a64e338a4956f72905f7de Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Fri, 27 Mar 2015 16:31:35 -0400 Subject: [PATCH 16/16] Tell the journal on the builders to listen on the proper socket. --- buildman/templates/cloudconfig.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/buildman/templates/cloudconfig.yaml b/buildman/templates/cloudconfig.yaml index 29f7ccc5a..2f274361a 100644 --- a/buildman/templates/cloudconfig.yaml +++ b/buildman/templates/cloudconfig.yaml @@ -22,6 +22,17 @@ coreos: group: {{ coreos_channel }} units: + - name: systemd-journal-gatewayd.socket + command: start + enable: yes + content: | + [Unit] + Description=Journal Gateway Service Socket + [Socket] + ListenStream=/var/run/journald.sock + Service=systemd-journal-gatewayd.service + [Install] + WantedBy=sockets.target {{ dockersystemd('quay-builder', 'quay.io/coreos/registry-build-worker', quay_username, @@ -36,6 +47,6 @@ coreos: {{ dockersystemd('builder-logs', 'quay.io/kelseyhightower/journal-2-logentries', extra_args='--env-file /root/overrides.list -v /run/journald.sock:/run/journald.sock', - after_units=['quay-builder'] + after_units=['quay-builder.service'] ) | indent(4) }} {%- endif %}