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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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.
+
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 @@
+
+
+
+
+
+
+
Generate Client Token
+
+
+
Your generated client token:
+
+
+
+
+
+
+
+
+
diff --git a/test/test_api_security.py b/test/test_api_security.py
index df01fe8c9..2560b7bcd 100644
--- a/test/test_api_security.py
+++ b/test/test_api_security.py
@@ -26,7 +26,8 @@ from endpoints.api.repoemail import RepositoryAuthorizedEmail
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
Signin, User, UserAuthorizationList, UserAuthorization, UserNotification,
- VerifyUser, DetachExternal, StarredRepositoryList, StarredRepository)
+ VerifyUser, DetachExternal, StarredRepositoryList, StarredRepository,
+ ClientKey)
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
@@ -528,6 +529,26 @@ class TestVerifyUser(ApiTestCase):
self._run_test('POST', 200, 'devtable', {u'password': 'password'})
+
+class TestClientKey(ApiTestCase):
+ def setUp(self):
+ ApiTestCase.setUp(self)
+ self._set_url(ClientKey)
+
+ def test_post_anonymous(self):
+ self._run_test('POST', 401, None, {u'password': 'LQ0N'})
+
+ def test_post_freshuser(self):
+ self._run_test('POST', 400, 'freshuser', {u'password': 'LQ0N'})
+
+ def test_post_reader(self):
+ self._run_test('POST', 200, 'reader', {u'password': 'password'})
+
+ def test_post_devtable(self):
+ self._run_test('POST', 200, 'devtable', {u'password': 'password'})
+
+
+
class TestListPlans(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
From 24cf27bd128204cfed8518e8f9254e2afecaf4a5 Mon Sep 17 00:00:00 2001
From: Jake Moshenko
Date: Thu, 26 Mar 2015 09:21:45 -0400
Subject: [PATCH 03/16] Route all of the logging through syslog-ng. Add the
ability to specify extra syslog-ng config. Simplify the Dockerfile.
---
Dockerfile | 14 ++++----------
conf/http-base.conf | 2 +-
conf/init/buildlogsarchiver/log/run | 2 --
conf/init/buildmanager/log/run | 2 --
conf/init/copy_syslog_config.sh | 6 ++++++
conf/init/diffsworker/log/run | 2 --
conf/init/gunicorn_registry/log/run | 2 --
conf/init/gunicorn_verbs/log/run | 2 --
conf/init/gunicorn_web/log/run | 2 --
conf/init/nginx/log/run | 2 --
conf/init/notificationworker/log/run | 2 --
conf/init/preplogsdir.sh | 10 ----------
conf/init/service/buildlogsarchiver/log/run | 2 ++
conf/init/{ => service}/buildlogsarchiver/run | 0
conf/init/service/buildmanager/log/run | 2 ++
conf/init/{ => service}/buildmanager/run | 0
conf/init/service/diffsworker/log/run | 2 ++
conf/init/{ => service}/diffsworker/run | 0
conf/init/service/gunicorn_registry/log/run | 2 ++
conf/init/{ => service}/gunicorn_registry/run | 0
conf/init/service/gunicorn_verbs/log/run | 2 ++
conf/init/{ => service}/gunicorn_verbs/run | 0
conf/init/service/gunicorn_web/log/run | 2 ++
conf/init/{ => service}/gunicorn_web/run | 0
conf/init/service/nginx/log/run | 2 ++
conf/init/{ => service}/nginx/run | 0
conf/init/service/notificationworker/log/run | 2 ++
conf/init/{ => service}/notificationworker/run | 0
conf/init/svlogd_config | 3 ---
conf/proxy-protocol.conf | 2 +-
conf/root-base.conf | 2 +-
conf/server-base.conf | 7 +++----
32 files changed, 32 insertions(+), 46 deletions(-)
delete mode 100755 conf/init/buildlogsarchiver/log/run
delete mode 100755 conf/init/buildmanager/log/run
create mode 100755 conf/init/copy_syslog_config.sh
delete mode 100755 conf/init/diffsworker/log/run
delete mode 100755 conf/init/gunicorn_registry/log/run
delete mode 100755 conf/init/gunicorn_verbs/log/run
delete mode 100755 conf/init/gunicorn_web/log/run
delete mode 100755 conf/init/nginx/log/run
delete mode 100755 conf/init/notificationworker/log/run
delete mode 100755 conf/init/preplogsdir.sh
create mode 100755 conf/init/service/buildlogsarchiver/log/run
rename conf/init/{ => service}/buildlogsarchiver/run (100%)
create mode 100755 conf/init/service/buildmanager/log/run
rename conf/init/{ => service}/buildmanager/run (100%)
create mode 100755 conf/init/service/diffsworker/log/run
rename conf/init/{ => service}/diffsworker/run (100%)
create mode 100755 conf/init/service/gunicorn_registry/log/run
rename conf/init/{ => service}/gunicorn_registry/run (100%)
create mode 100755 conf/init/service/gunicorn_verbs/log/run
rename conf/init/{ => service}/gunicorn_verbs/run (100%)
create mode 100755 conf/init/service/gunicorn_web/log/run
rename conf/init/{ => service}/gunicorn_web/run (100%)
create mode 100755 conf/init/service/nginx/log/run
rename conf/init/{ => service}/nginx/run (100%)
create mode 100755 conf/init/service/notificationworker/log/run
rename conf/init/{ => service}/notificationworker/run (100%)
delete mode 100644 conf/init/svlogd_config
diff --git a/Dockerfile b/Dockerfile
index 6a3fa1f2f..7ba5e7501 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -38,19 +38,13 @@ ADD . .
# Run grunt
RUN cd grunt && grunt
-ADD conf/init/svlogd_config /svlogd_config
ADD conf/init/doupdatelimits.sh /etc/my_init.d/
-ADD conf/init/preplogsdir.sh /etc/my_init.d/
+ADD conf/init/copy_syslog_config.sh /etc/my_init.d/
ADD conf/init/runmigration.sh /etc/my_init.d/
-ADD conf/init/gunicorn_web /etc/service/gunicorn_web
-ADD conf/init/gunicorn_registry /etc/service/gunicorn_registry
-ADD conf/init/gunicorn_verbs /etc/service/gunicorn_verbs
-ADD conf/init/nginx /etc/service/nginx
-ADD conf/init/diffsworker /etc/service/diffsworker
-ADD conf/init/notificationworker /etc/service/notificationworker
-ADD conf/init/buildlogsarchiver /etc/service/buildlogsarchiver
-ADD conf/init/buildmanager /etc/service/buildmanager
+ADD conf/init/service/ /etc/service/
+
+RUN rm -rf /etc/service/syslog-forwarder
# Download any external libs.
RUN mkdir static/fonts static/ldn
diff --git a/conf/http-base.conf b/conf/http-base.conf
index 8b7ff9e0b..3c3d57372 100644
--- a/conf/http-base.conf
+++ b/conf/http-base.conf
@@ -4,7 +4,7 @@ types_hash_max_size 2048;
include /usr/local/nginx/conf/mime.types.default;
default_type application/octet-stream;
-access_log /var/log/nginx/nginx.access.log;
+access_log /dev/stdout;
sendfile on;
gzip on;
diff --git a/conf/init/buildlogsarchiver/log/run b/conf/init/buildlogsarchiver/log/run
deleted file mode 100755
index c35fb1fb9..000000000
--- a/conf/init/buildlogsarchiver/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd /var/log/buildlogsarchiver/
\ No newline at end of file
diff --git a/conf/init/buildmanager/log/run b/conf/init/buildmanager/log/run
deleted file mode 100755
index 1dd4c3fef..000000000
--- a/conf/init/buildmanager/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd /var/log/buildmanager/
\ No newline at end of file
diff --git a/conf/init/copy_syslog_config.sh b/conf/init/copy_syslog_config.sh
new file mode 100755
index 000000000..7acd62b6b
--- /dev/null
+++ b/conf/init/copy_syslog_config.sh
@@ -0,0 +1,6 @@
+#! /bin/sh
+
+if [ -e /conf/stack/syslog-ng-extra.conf ]
+then
+ cp /conf/stack/syslog-ng-extra.conf /etc/syslog-ng/conf.d/
+fi
diff --git a/conf/init/diffsworker/log/run b/conf/init/diffsworker/log/run
deleted file mode 100755
index 066f7415a..000000000
--- a/conf/init/diffsworker/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd /var/log/diffsworker/
\ No newline at end of file
diff --git a/conf/init/gunicorn_registry/log/run b/conf/init/gunicorn_registry/log/run
deleted file mode 100755
index 1896ef533..000000000
--- a/conf/init/gunicorn_registry/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd /var/log/gunicorn_registry/
\ No newline at end of file
diff --git a/conf/init/gunicorn_verbs/log/run b/conf/init/gunicorn_verbs/log/run
deleted file mode 100755
index 2b061e193..000000000
--- a/conf/init/gunicorn_verbs/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd /var/log/gunicorn_verbs/
\ No newline at end of file
diff --git a/conf/init/gunicorn_web/log/run b/conf/init/gunicorn_web/log/run
deleted file mode 100755
index de17cdf61..000000000
--- a/conf/init/gunicorn_web/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd /var/log/gunicorn_web/
\ No newline at end of file
diff --git a/conf/init/nginx/log/run b/conf/init/nginx/log/run
deleted file mode 100755
index 30476f6e6..000000000
--- a/conf/init/nginx/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd /var/log/nginx/
\ No newline at end of file
diff --git a/conf/init/notificationworker/log/run b/conf/init/notificationworker/log/run
deleted file mode 100755
index 46f8431a7..000000000
--- a/conf/init/notificationworker/log/run
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-exec svlogd -t /var/log/notificationworker/
\ No newline at end of file
diff --git a/conf/init/preplogsdir.sh b/conf/init/preplogsdir.sh
deleted file mode 100755
index 93c3ee5af..000000000
--- a/conf/init/preplogsdir.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#! /bin/sh
-
-echo 'Linking config files to logs directory'
-for svc in `ls /etc/service/`
-do
- if [ ! -d /var/log/$svc ]; then
- mkdir -p /var/log/$svc
- ln -s /svlogd_config /var/log/$svc/config
- fi
-done
diff --git a/conf/init/service/buildlogsarchiver/log/run b/conf/init/service/buildlogsarchiver/log/run
new file mode 100755
index 000000000..3bcd9ba8a
--- /dev/null
+++ b/conf/init/service/buildlogsarchiver/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t buildlogsarchiver
\ No newline at end of file
diff --git a/conf/init/buildlogsarchiver/run b/conf/init/service/buildlogsarchiver/run
similarity index 100%
rename from conf/init/buildlogsarchiver/run
rename to conf/init/service/buildlogsarchiver/run
diff --git a/conf/init/service/buildmanager/log/run b/conf/init/service/buildmanager/log/run
new file mode 100755
index 000000000..b35e28af9
--- /dev/null
+++ b/conf/init/service/buildmanager/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t buildmanager
\ No newline at end of file
diff --git a/conf/init/buildmanager/run b/conf/init/service/buildmanager/run
similarity index 100%
rename from conf/init/buildmanager/run
rename to conf/init/service/buildmanager/run
diff --git a/conf/init/service/diffsworker/log/run b/conf/init/service/diffsworker/log/run
new file mode 100755
index 000000000..8e3dca5f3
--- /dev/null
+++ b/conf/init/service/diffsworker/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t diffsworker
\ No newline at end of file
diff --git a/conf/init/diffsworker/run b/conf/init/service/diffsworker/run
similarity index 100%
rename from conf/init/diffsworker/run
rename to conf/init/service/diffsworker/run
diff --git a/conf/init/service/gunicorn_registry/log/run b/conf/init/service/gunicorn_registry/log/run
new file mode 100755
index 000000000..5b5b37af9
--- /dev/null
+++ b/conf/init/service/gunicorn_registry/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t gunicorn_registry
\ No newline at end of file
diff --git a/conf/init/gunicorn_registry/run b/conf/init/service/gunicorn_registry/run
similarity index 100%
rename from conf/init/gunicorn_registry/run
rename to conf/init/service/gunicorn_registry/run
diff --git a/conf/init/service/gunicorn_verbs/log/run b/conf/init/service/gunicorn_verbs/log/run
new file mode 100755
index 000000000..d0bc335d7
--- /dev/null
+++ b/conf/init/service/gunicorn_verbs/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t gunicorn_verbs
\ No newline at end of file
diff --git a/conf/init/gunicorn_verbs/run b/conf/init/service/gunicorn_verbs/run
similarity index 100%
rename from conf/init/gunicorn_verbs/run
rename to conf/init/service/gunicorn_verbs/run
diff --git a/conf/init/service/gunicorn_web/log/run b/conf/init/service/gunicorn_web/log/run
new file mode 100755
index 000000000..c96d365a5
--- /dev/null
+++ b/conf/init/service/gunicorn_web/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t gunicorn_web
\ No newline at end of file
diff --git a/conf/init/gunicorn_web/run b/conf/init/service/gunicorn_web/run
similarity index 100%
rename from conf/init/gunicorn_web/run
rename to conf/init/service/gunicorn_web/run
diff --git a/conf/init/service/nginx/log/run b/conf/init/service/nginx/log/run
new file mode 100755
index 000000000..168af6d3e
--- /dev/null
+++ b/conf/init/service/nginx/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t nginx
\ No newline at end of file
diff --git a/conf/init/nginx/run b/conf/init/service/nginx/run
similarity index 100%
rename from conf/init/nginx/run
rename to conf/init/service/nginx/run
diff --git a/conf/init/service/notificationworker/log/run b/conf/init/service/notificationworker/log/run
new file mode 100755
index 000000000..49747f3ce
--- /dev/null
+++ b/conf/init/service/notificationworker/log/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec logger -i -t notificationworker
\ No newline at end of file
diff --git a/conf/init/notificationworker/run b/conf/init/service/notificationworker/run
similarity index 100%
rename from conf/init/notificationworker/run
rename to conf/init/service/notificationworker/run
diff --git a/conf/init/svlogd_config b/conf/init/svlogd_config
deleted file mode 100644
index 2ccee1e0c..000000000
--- a/conf/init/svlogd_config
+++ /dev/null
@@ -1,3 +0,0 @@
-s100000000
-t86400
-n4
diff --git a/conf/proxy-protocol.conf b/conf/proxy-protocol.conf
index 5897f1839..ba00507f5 100644
--- a/conf/proxy-protocol.conf
+++ b/conf/proxy-protocol.conf
@@ -5,4 +5,4 @@ real_ip_header proxy_protocol;
log_format elb_pp '$proxy_protocol_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
-access_log /var/log/nginx/nginx.access.log elb_pp;
+access_log /dev/stdout elb_pp;
diff --git a/conf/root-base.conf b/conf/root-base.conf
index 02c004564..357e6ed03 100644
--- a/conf/root-base.conf
+++ b/conf/root-base.conf
@@ -1,7 +1,7 @@
# vim: ft=nginx
pid /tmp/nginx.pid;
-error_log /var/log/nginx/nginx.error.log;
+error_log /dev/stdout;
worker_processes 2;
worker_priority -10;
diff --git a/conf/server-base.conf b/conf/server-base.conf
index 2f03b11b2..3853fbccf 100644
--- a/conf/server-base.conf
+++ b/conf/server-base.conf
@@ -1,6 +1,5 @@
# vim: ft=nginx
-client_body_temp_path /var/log/nginx/client_body 1 2;
server_name _;
keepalive_timeout 5;
@@ -36,7 +35,7 @@ location /v1/repositories/ {
proxy_pass http://registry_app_server;
proxy_read_timeout 2000;
- proxy_temp_path /var/log/nginx/proxy_temp 1 2;
+ proxy_temp_path /tmp 1 2;
limit_req zone=repositories burst=10;
}
@@ -47,7 +46,7 @@ location /v1/ {
proxy_request_buffering off;
proxy_pass http://registry_app_server;
- proxy_temp_path /var/log/nginx/proxy_temp 1 2;
+ proxy_temp_path /tmp 1 2;
client_max_body_size 20G;
}
@@ -58,7 +57,7 @@ location /c1/ {
proxy_request_buffering off;
proxy_pass http://verbs_app_server;
- proxy_temp_path /var/log/nginx/proxy_temp 1 2;
+ proxy_temp_path /tmp 1 2;
limit_req zone=verbs burst=10;
}
From 0f34b7d8e042c5314acc072a2fee8cde7f077ec3 Mon Sep 17 00:00:00 2001
From: Jake Moshenko
Date: Thu, 26 Mar 2015 09:22:31 -0400
Subject: [PATCH 04/16] Remove one of the last vestigal references to the
license system.
---
app.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/app.py b/app.py
index 78243de75..33eabf1f8 100644
--- a/app.py
+++ b/app.py
@@ -39,7 +39,6 @@ OVERRIDE_CONFIG_YAML_FILENAME = 'conf/stack/config.yaml'
OVERRIDE_CONFIG_PY_FILENAME = 'conf/stack/config.py'
OVERRIDE_CONFIG_KEY = 'QUAY_OVERRIDE_CONFIG'
-LICENSE_FILENAME = 'conf/stack/license.enc'
CONFIG_PROVIDER = FileConfigProvider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config.py')
From d23bb6616d15db045adb41d3859e37496edc696f Mon Sep 17 00:00:00 2001
From: Joseph Schorr
Date: Thu, 26 Mar 2015 13:22:16 -0400
Subject: [PATCH 05/16] Fix error message to exactly match current output
---
data/users.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/data/users.py b/data/users.py
index c7a6128db..f8fce77a5 100644
--- a/data/users.py
+++ b/data/users.py
@@ -161,7 +161,7 @@ class UserAuthentication(object):
if result:
return (result, '')
else:
- return (result, 'Invalid password')
+ return (result, 'Invalid password.')
def __getattr__(self, name):
From 0349f3f1a38e9d77548158a6e33749e01f30ddaa Mon Sep 17 00:00:00 2001
From: Jake Moshenko
Date: Thu, 26 Mar 2015 14:53:56 -0400
Subject: [PATCH 06/16] Handle the case where YAML config returns a list not a
tuple.
---
buildman/manager/ephemeral.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/buildman/manager/ephemeral.py b/buildman/manager/ephemeral.py
index 40876cdf5..7e24094c8 100644
--- a/buildman/manager/ephemeral.py
+++ b/buildman/manager/ephemeral.py
@@ -157,8 +157,12 @@ class EphemeralBuilderManager(BaseManager):
etcd_host = self._manager_config.get('ETCD_HOST', '127.0.0.1')
etcd_port = self._manager_config.get('ETCD_PORT', 2379)
- etcd_auth = self._manager_config.get('ETCD_CERT_AND_KEY', None)
etcd_ca_cert = self._manager_config.get('ETCD_CA_CERT', None)
+
+ etcd_auth = self._manager_config.get('ETCD_CERT_AND_KEY', None)
+ if etcd_auth is not None:
+ etcd_auth = tuple(etcd_auth) # Convert YAML list to a tuple
+
etcd_protocol = 'http' if etcd_auth is None else 'https'
logger.debug('Connecting to etcd on %s:%s', etcd_host, etcd_port)
From aaf1b23e98bc7643bdb75c99eef097f51b10d879 Mon Sep 17 00:00:00 2001
From: Joseph Schorr
Date: Thu, 26 Mar 2015 15:10:58 -0400
Subject: [PATCH 07/16] Address CL concerns and switch to a real encryption
system
---
data/users.py | 65 ++++++++++++++++---
endpoints/api/user.py | 10 +--
.../directives/config/config-setup-tool.html | 4 +-
static/js/pages/user-admin.js | 2 +-
static/partials/user-admin.html | 48 +++++++++-----
util/aes.py | 32 +++++++++
6 files changed, 124 insertions(+), 37 deletions(-)
create mode 100644 util/aes.py
diff --git a/data/users.py b/data/users.py
index f8fce77a5..bcd0fad62 100644
--- a/data/users.py
+++ b/data/users.py
@@ -1,7 +1,10 @@
import ldap
import logging
+import json
+import itertools
+import uuid
-from flask.sessions import SecureCookieSessionInterface, BadSignature
+from util.aes import AESCipher
from util.validation import generate_valid_usernames
from data import model
@@ -107,6 +110,7 @@ class LDAPUsers(object):
return found_user is not None
+
class UserAuthentication(object):
def __init__(self, app=None):
self.app = app
@@ -139,23 +143,68 @@ class UserAuthentication(object):
app.extensions['authentication'] = users
return users
+ def _get_secret_key(self):
+ """ Returns the secret key to use for encrypting and decrypting. """
+ from app import app
+ app_secret_key = app.config['SECRET_KEY']
+
+ # First try parsing the key as a float.
+ try:
+ secret_key = float(app_secret_key)
+ except ValueError:
+ secret_key = app_secret_key
+
+ # Next try parsing it as an UUID.
+ try:
+ secret_key = uuid.UUID(app_secret_key).bytes
+ except ValueError:
+ secret_key = app_secret_key
+
+ # Otherwise, use the bytes directly.
+ return ''.join(itertools.islice(itertools.cycle(secret_key), 32))
+
+ def encrypt_user_password(self, password):
+ """ Returns an encrypted version of the user's password. """
+ data = {
+ 'password': password
+ }
+
+ message = json.dumps(data)
+ cipher = AESCipher(self._get_secret_key())
+ return cipher.encrypt(message)
+
+ def _decrypt_user_password(self, encrypted):
+ """ Attempts to decrypt the given password and returns it. """
+ cipher = AESCipher(self._get_secret_key())
+
+ try:
+ message = cipher.decrypt(encrypted)
+ except ValueError:
+ return None
+ except TypeError:
+ return None
+
+ try:
+ data = json.loads(message)
+ except ValueError:
+ return None
+
+ return data.get('password', encrypted)
+
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:
+ decrypted = self._decrypt_user_password(password)
+ if decrypted is None:
# 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)
+ else:
+ password = decrypted
result = self.state.verify_user(username_or_email, password)
if result:
diff --git a/endpoints/api/user.py b/endpoints/api/user.py
index d10807940..9ccb1d7aa 100644
--- a/endpoints/api/user.py
+++ b/endpoints/api/user.py
@@ -5,7 +5,6 @@ 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
@@ -370,15 +369,8 @@ class ClientKey(ApiResource):
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
+ 'key': authentication.encrypt_user_password(password)
}
diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html
index 9be7ddd37..aa0c60e5d 100644
--- a/static/directives/config/config-setup-tool.html
+++ b/static/directives/config/config-setup-tool.html
@@ -311,13 +311,13 @@
- It is highly recommended to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in plain-text!
+ It is highly recommended to require encrypted client tokens. 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
- prevent LDAP passwords from being saved as plain-text by the Docker client.
+ prevent LDAP passwords from being saved as plaintext by the Docker client.
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 @@
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.
+
+
-
diff --git a/util/aes.py b/util/aes.py
new file mode 100644
index 000000000..10f1ac030
--- /dev/null
+++ b/util/aes.py
@@ -0,0 +1,32 @@
+import base64
+import hashlib
+from Crypto import Random
+from Crypto.Cipher import AES
+
+class AESCipher(object):
+ """ Helper class for encrypting and decrypting data via AES.
+
+ Copied From: http://stackoverflow.com/a/21928790
+ """
+ def __init__(self, key):
+ self.bs = 32
+ self.key = key
+
+ def encrypt(self, raw):
+ raw = self._pad(raw)
+ iv = Random.new().read(AES.block_size)
+ cipher = AES.new(self.key, AES.MODE_CBC, iv)
+ return base64.b64encode(iv + cipher.encrypt(raw))
+
+ def decrypt(self, enc):
+ enc = base64.b64decode(enc)
+ iv = enc[:AES.block_size]
+ cipher = AES.new(self.key, AES.MODE_CBC, iv)
+ return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')
+
+ def _pad(self, s):
+ return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)
+
+ @staticmethod
+ def _unpad(s):
+ return s[:-ord(s[len(s)-1:])]
\ No newline at end of file
From 4d1792db1cc89333744044910a7503892d461fe1 Mon Sep 17 00:00:00 2001
From: Joseph Schorr
Date: Thu, 26 Mar 2015 15:47:44 -0400
Subject: [PATCH 08/16] getrandbits creates an int, not a float
---
data/users.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/data/users.py b/data/users.py
index bcd0fad62..628f5a0c3 100644
--- a/data/users.py
+++ b/data/users.py
@@ -148,9 +148,9 @@ class UserAuthentication(object):
from app import app
app_secret_key = app.config['SECRET_KEY']
- # First try parsing the key as a float.
+ # First try parsing the key as an int.
try:
- secret_key = float(app_secret_key)
+ secret_key = int(app_secret_key)
except ValueError:
secret_key = app_secret_key
From f8afd8b5ce59fa4aeb8bf6b20a1b211a84073441 Mon Sep 17 00:00:00 2001
From: Joseph Schorr
Date: Thu, 26 Mar 2015 16:13:35 -0400
Subject: [PATCH 09/16] Make sure to parse the big int into a byte string
---
data/users.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/data/users.py b/data/users.py
index 628f5a0c3..13556a552 100644
--- a/data/users.py
+++ b/data/users.py
@@ -3,6 +3,7 @@ import logging
import json
import itertools
import uuid
+import struct
from util.aes import AESCipher
from util.validation import generate_valid_usernames
@@ -150,7 +151,8 @@ class UserAuthentication(object):
# First try parsing the key as an int.
try:
- secret_key = int(app_secret_key)
+ big_int = int(app_secret_key)
+ secret_key = bytearray.fromhex('{:02x}'.format(big_int))
except ValueError:
secret_key = app_secret_key
From c4a2574b0d87ec0a86383ee8c8eca90323cff620 Mon Sep 17 00:00:00 2001
From: Joseph Schorr
Date: Thu, 26 Mar 2015 16:23:28 -0400
Subject: [PATCH 10/16] Clarify unencrypted password error message
---
data/users.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/data/users.py b/data/users.py
index 13556a552..10c909cb8 100644
--- a/data/users.py
+++ b/data/users.py
@@ -202,8 +202,8 @@ class UserAuthentication(object):
if decrypted is None:
# 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.')
+ msg = ('Client login with unecrypted passwords is disabled. Please generate an ' +
+ 'encrypted password in the user admin panel for use here.')
return (None, msg)
else:
password = decrypted
From bcc7a9580b43a7ed47ef925977c5bbc192f7ee60 Mon Sep 17 00:00:00 2001
From: Jimmy Zelinskie
Date: Thu, 26 Mar 2015 16:30:41 -0400
Subject: [PATCH 11/16] models: change UUID of user on password change
This prevents old cookies from continuing to work after a password has
been changed.
---
data/model/legacy.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/data/model/legacy.py b/data/model/legacy.py
index aa968408c..2fcbfcf68 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -905,6 +905,7 @@ def change_password(user, new_password):
pw_hash = hash_password(new_password)
user.invalid_login_attempts = 0
user.password_hash = pw_hash
+ user.uuid = str(uuid4())
user.save()
# Remove any password required notifications for the user.
From 02bafb1613374cc62d17570cd1b3d338c36ffd80 Mon Sep 17 00:00:00 2001
From: Joseph Schorr
Date: Thu, 26 Mar 2015 16:31:19 -0400
Subject: [PATCH 12/16] Fix language in the config setup tool
---
static/directives/config/config-setup-tool.html | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html
index aa0c60e5d..04ac2abf2 100644
--- a/static/directives/config/config-setup-tool.html
+++ b/static/directives/config/config-setup-tool.html
@@ -47,16 +47,16 @@
-
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 %}