diff --git a/etc/container-server.conf-sample b/etc/container-server.conf-sample index 7a69495c16..d1fce9882d 100644 --- a/etc/container-server.conf-sample +++ b/etc/container-server.conf-sample @@ -7,6 +7,9 @@ # swift_dir = /etc/swift # devices = /srv/node # mount_check = true +# This is a comma separated list of hosts allowed in the X-Container-Sync-To +# field for containers. +# allowed_sync_hosts = 127.0.0.1 # You can specify default log routing here if you want: # log_name = swift # log_facility = LOG_LOCAL0 diff --git a/etc/proxy-server.conf-sample b/etc/proxy-server.conf-sample index 3af7db0f8a..c786fde001 100644 --- a/etc/proxy-server.conf-sample +++ b/etc/proxy-server.conf-sample @@ -63,6 +63,9 @@ use = egg:swift#auth # ssl = false # prefix = / # node_timeout = 10 +# This is a comma separated list of hosts allowed to send X-Container-Sync-Key +# requests. +# allowed_sync_hosts = 127.0.0.1 # Only needed for Swauth [filter:swauth] @@ -91,6 +94,9 @@ use = egg:swift#swauth # default_swift_cluster = local#https://public.com:8080/v1#http://private.com:8080/v1 # token_life = 86400 # node_timeout = 10 +# This is a comma separated list of hosts allowed to send X-Container-Sync-Key +# requests. +# allowed_sync_hosts = 127.0.0.1 # Highly recommended to change this. super_admin_key = swauthkey diff --git a/swift/common/middleware/auth.py b/swift/common/middleware/auth.py index ba9f639cdc..bef1280a97 100644 --- a/swift/common/middleware/auth.py +++ b/swift/common/middleware/auth.py @@ -37,6 +37,9 @@ class DevAuth(object): self.ssl = conf.get('ssl', 'false').lower() in TRUE_VALUES self.auth_prefix = conf.get('prefix', '/') self.timeout = int(conf.get('node_timeout', 10)) + self.allowed_sync_hosts = [h.strip() + for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') + if h.strip()] def __call__(self, env, start_response): """ @@ -184,12 +187,11 @@ class DevAuth(object): # account DELETE or PUT... req.environ['swift_owner'] = True return None - # TODO: Restrict this further to only authenticated folks in the .sync - # group. Currently, anybody with the x-container-sync-key can do a - # sync. - if 'swift_sync_key' in req.environ and \ - req.environ['swift_sync_key'] == \ - req.headers.get('x-container-sync-key', None): + if ('swift_sync_key' in req.environ and + req.environ['swift_sync_key'] == + req.headers.get('x-container-sync-key', None) and + (req.remote_addr in self.allowed_sync_hosts or + get_remote_client(req) in self.allowed_sync_hosts)): return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): diff --git a/swift/common/middleware/swauth.py b/swift/common/middleware/swauth.py index 8467d43b5c..0d889e2dbf 100644 --- a/swift/common/middleware/swauth.py +++ b/swift/common/middleware/swauth.py @@ -101,6 +101,9 @@ class Swauth(object): self.timeout = int(conf.get('node_timeout', 10)) self.itoken = None self.itoken_expires = None + self.allowed_sync_hosts = [h.strip() + for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') + if h.strip()] def __call__(self, env, start_response): """ @@ -277,12 +280,11 @@ class Swauth(object): # account DELETE or PUT... req.environ['swift_owner'] = True return None - # TODO: Restrict this further to only authenticated folks in the .sync - # group. Currently, anybody with the x-container-sync-key can do a - # sync. - if 'swift_sync_key' in req.environ and \ - req.environ['swift_sync_key'] == \ - req.headers.get('x-container-sync-key', None): + if ('swift_sync_key' in req.environ and + req.environ['swift_sync_key'] == + req.headers.get('x-container-sync-key', None) and + (req.remote_addr in self.allowed_sync_hosts or + get_remote_client(req) in self.allowed_sync_hosts)): return None referrers, groups = parse_acl(getattr(req, 'acl', None)) if referrer_allowed(req.referer, referrers): diff --git a/swift/common/utils.py b/swift/common/utils.py index 744f65c997..c5a5955534 100644 --- a/swift/common/utils.py +++ b/swift/common/utils.py @@ -952,3 +952,26 @@ def urlparse(url): :param url: URL to parse. """ return ModifiedParseResult(*stdlib_urlparse(url)) + + +def validate_sync_to(value, allowed_sync_hosts): + p = urlparse(value) + if p.scheme not in ('http', 'https'): + return _('Invalid scheme %r in X-Container-Sync-To, must be "http" ' + 'or "https".') % p.scheme + if not p.path: + return _('Path required in X-Container-Sync-To') + if p.params or p.query or p.fragment: + return _('Params, queries, and fragments not allowed in ' + 'X-Container-Sync-To') + if p.hostname not in allowed_sync_hosts: + return _('Invalid host %r in X-Container-Sync-To') % p.hostname + + +def get_remote_client(req): + # remote host for zeus + client = req.headers.get('x-cluster-client-ip') + if not client and 'x-forwarded-for' in req.headers: + # remote host for other lbs + client = req.headers['x-forwarded-for'].split(',')[0].strip() + return client diff --git a/swift/container/server.py b/swift/container/server.py index 44af493f12..bea35a6d61 100644 --- a/swift/container/server.py +++ b/swift/container/server.py @@ -32,7 +32,8 @@ from webob.exc import HTTPAccepted, HTTPBadRequest, HTTPConflict, \ from swift.common.db import ContainerBroker from swift.common.utils import get_logger, get_param, hash_path, \ - normalize_timestamp, storage_directory, split_path + normalize_timestamp, storage_directory, split_path, urlparse, \ + validate_sync_to from swift.common.constraints import CONTAINER_LISTING_LIMIT, \ check_mount, check_float, check_utf8 from swift.common.bufferedhttp import http_connect @@ -56,6 +57,9 @@ class ContainerController(object): ('true', 't', '1', 'on', 'yes', 'y') self.node_timeout = int(conf.get('node_timeout', 3)) self.conn_timeout = float(conf.get('conn_timeout', 0.5)) + self.allowed_sync_hosts = [h.strip() + for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') + if h.strip()] self.replicator_rpc = ReplicatorRpc(self.root, DATADIR, ContainerBroker, self.mount_check) @@ -175,6 +179,11 @@ class ContainerController(object): not check_float(req.headers['x-timestamp']): return HTTPBadRequest(body='Missing timestamp', request=req, content_type='text/plain') + if 'x-container-sync-to' in req.headers: + err = validate_sync_to(req.headers['x-container-sync-to'], + self.allowed_sync_hosts) + if err: + return HTTPBadRequest(err) if self.mount_check and not check_mount(self.root, drive): return Response(status='507 %s is not mounted' % drive) timestamp = normalize_timestamp(req.headers['x-timestamp']) @@ -370,6 +379,11 @@ class ContainerController(object): not check_float(req.headers['x-timestamp']): return HTTPBadRequest(body='Missing or bad timestamp', request=req, content_type='text/plain') + if 'x-container-sync-to' in req.headers: + err = validate_sync_to(req.headers['x-container-sync-to'], + self.allowed_sync_hosts) + if err: + return HTTPBadRequest(err) if self.mount_check and not check_mount(self.root, drive): return Response(status='507 %s is not mounted' % drive) broker = self._get_container_broker(drive, part, account, container) diff --git a/swift/container/sync.py b/swift/container/sync.py index a4b450eb84..9286228c11 100644 --- a/swift/container/sync.py +++ b/swift/container/sync.py @@ -22,7 +22,7 @@ from swift.common import client, direct_client from swift.common.ring import Ring from swift.common.db import ContainerBroker from swift.common.utils import audit_location_generator, get_logger, \ - normalize_timestamp, TRUE_VALUES + normalize_timestamp, TRUE_VALUES, validate_sync_to from swift.common.daemon import Daemon @@ -59,13 +59,16 @@ class ContainerSync(Daemon): conf.get('mount_check', 'true').lower() in TRUE_VALUES self.interval = int(conf.get('interval', 300)) self.container_time = int(conf.get('container_time', 60)) - swift_dir = conf.get('swift_dir', '/etc/swift') + self.allowed_sync_hosts = [h.strip() + for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',') + if h.strip()] self.container_syncs = 0 self.container_deletes = 0 self.container_puts = 0 self.container_skips = 0 self.container_failures = 0 self.reported = time.time() + swift_dir = conf.get('swift_dir', '/etc/swift') self.object_ring = object_ring or \ Ring(os.path.join(swift_dir, 'object.ring.gz')) @@ -151,6 +154,14 @@ class ContainerSync(Daemon): self.container_skips += 1 return sync_to = sync_to.rstrip('/') + err = validate_sync_to(sync_to, self.allowed_sync_hosts) + if err: + self.logger.info( + _('ERROR %(db_file)s: %(validate_sync_to_err)s'), + {'db_file': broker.db_file, + 'validate_sync_to_err': err}) + self.container_failures += 1 + return stop_at = time.time() + self.container_time while time.time() < stop_at: rows = broker.get_items_since(sync_row, 1) diff --git a/swift/proxy/server.py b/swift/proxy/server.py index af71346348..11051ad094 100644 --- a/swift/proxy/server.py +++ b/swift/proxy/server.py @@ -42,7 +42,7 @@ from webob import Request, Response from swift.common.ring import Ring from swift.common.utils import get_logger, normalize_timestamp, split_path, \ - cache_from_env + cache_from_env, get_remote_client from swift.common.bufferedhttp import http_connect from swift.common.constraints import check_metadata, check_object_creation, \ check_utf8, CONTAINER_LISTING_LIMIT, MAX_ACCOUNT_NAME_LENGTH, \ @@ -1834,11 +1834,7 @@ class Application(BaseApplication): the_request = quote(unquote(req.path)) if req.query_string: the_request = the_request + '?' + req.query_string - # remote user for zeus - client = req.headers.get('x-cluster-client-ip') - if not client and 'x-forwarded-for' in req.headers: - # remote user for other lbs - client = req.headers['x-forwarded-for'].split(',')[0].strip() + client = get_remote_client(req) logged_headers = None if self.log_headers: logged_headers = '\n'.join('%s: %s' % (k, v)