Skip to content

Commit

Permalink
Enhance security of discovery server
Browse files Browse the repository at this point in the history
1) Discovery utility to require admin keystone credentials to
perform any actions such as load-balance, set admin state etc.
2) Discovery server to authenticate load-balance and API to
update publisher state (admin, operational) by requiring token
and validating it against keystone
2) To prevent unauthorized publish or subscribe requests to
effect discovery server state (and assuming such requests are
coming through load-balancer such ha-proxy), discovery server
to apply configured publish and subscribe white-lists to
incoming IP addresses as obtained from X-Forwarded-For header.
Load-Balancer must be enabled to forward client's real IP address
in X-Forwarded-For header to discovery servers. The whitelist
configuration in contrail-discovery.conf looks like this:

white_list_publish=127.0.0.1 10.84.20.0/24
white_list_subscribe=127.0.0.1 10.84.20.0/24

RHS is list of IP prefixes seperated by white space.
If X-Forwarded-For header is missing in incoming publish or subscribe
request, white list configuration is ignored.

Change-Id: If2bbe1d90ec93f0cf9f29ba8c7e768a6888de41b
Partial-Bug: #1546801
  • Loading branch information
Deepinder Setia committed Feb 24, 2016
1 parent 70a3afe commit 1f508c3
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 70 deletions.
4 changes: 4 additions & 0 deletions src/config/common/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,7 @@ def __init__(self, app, conf, *args, **kwargs):
auth_protocol = conf['auth_protocol']
auth_host = conf['auth_host']
auth_port = conf['auth_port']
self.delay_auth_decision = conf['delay_auth_decision']
self.request_uri = '%s://%s:%s' % (auth_protocol, auth_host, auth_port)
self.auth_uri = self.request_uri
# print 'FakeAuthProtocol init: auth-uri %s, conf %s' % (self.auth_uri, self.conf)
Expand Down Expand Up @@ -1077,6 +1078,9 @@ def __call__(self, env, start_response):
if user_token:
# print '****** user token %s ***** ' % user_token
pass
elif self.delay_auth_decision:
self._add_headers(env, {'X-Identity-Status': 'Invalid'})
return self.app(env, start_response)
else:
# print 'Missing token or Unable to authenticate token'
return self._reject_request(env, start_response)
Expand Down
70 changes: 39 additions & 31 deletions src/config/utils/discovery_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

EP_DELIM=','
PUBSUB_DELIM=' '
DEFAULT_HEADERS = {'Content-type': 'application/json; charset="UTF-8"'}

def show_usage():
print 'A rule string must be specified for this operation'
Expand Down Expand Up @@ -225,6 +226,43 @@ def get_ks_var(args, name):
server_ip = server[0]
server_port = server[1]

# Validate API server information
api_server = args.api_server.split(':')
if len(api_server) != 2:
print 'API server address must be of the form ip:port, '\
'for example 127.0.0.1:8082'
sys.exit(1)
api_server_ip = api_server[0]
api_server_port = api_server[1]

# Validate keystone credentials
conf = {}
for name in ['username', 'password', 'tenant_name']:
val, rsp = get_ks_var(args, name)
if val is None:
print rsp
sys.exit(1)
conf[name] = val

username = conf['username']
password = conf['password']
tenant_name = conf['tenant_name']

print 'API Server = ', args.api_server
print 'Discovery Server = ', args.server
print 'Username = ', username
print 'Tenant = ', tenant_name
print ''

try:
vnc = VncApi(username, password, tenant_name,
api_server[0], api_server[1])
except Exception as e:
print '*** %s' % str(e)
sys.exit(1)

headers = DEFAULT_HEADERS.copy()
headers['X-AUTH-TOKEN'] = vnc.get_auth_token()

if args.oper_state or args.admin_state or args.oper_state_reason:
if not args.service_id or not args.service_type:
Expand All @@ -240,9 +278,6 @@ def get_ks_var(args, name):
data['oper-state-reason'] = args.oper_state_reason
if args.admin_state:
data['admin-state'] = args.admin_state
headers = {
'Content-type': 'application/json',
}
url = "http://%s:%s/service/%s" % (server_ip, server_port, args.service_id)
r = requests.put(url, data=json.dumps(data), headers=headers)
if r.status_code != 200:
Expand All @@ -255,35 +290,11 @@ def get_ks_var(args, name):
if args.service_id:
print 'Specific service id %s ignored for this operation' % args.service_id
url = "http://%s:%s/load-balance/%s" % (server_ip, server_port, args.service_type)
r = requests.post(url)
r = requests.post(url, headers=headers)
if r.status_code != 200:
print "Operation status %d" % r.status_code
sys.exit(0)

# Validate API server information
api_server = args.api_server.split(':')
if len(api_server) != 2:
print 'Discovery server address must be of the form ip:port, '\
'for example 127.0.0.1:5998'
sys.exit(1)
api_server_ip = api_server[0]
api_server_port = api_server[1]

# Validate keystone credentials
conf = {}
for name in ['username', 'password', 'tenant_name']:
val, rsp = get_ks_var(args, name)
if val is None:
print rsp
sys.exit(1)
conf[name] = val

username = conf['username']
password = conf['password']
tenant_name = conf['tenant_name']

vnc = VncApi(username, password, tenant_name,
api_server[0], api_server[1])

uuid = args.uuid
# transform uuid if needed
Expand All @@ -295,9 +306,6 @@ def get_ks_var(args, name):
print 'Oper = ', args.op
print 'Name = %s' % fq_name
print 'UUID = %s' % uuid
print 'API Server = ', args.server
print 'Discovery Server = ', args.server
print ''

if args.op == 'add-rule':
if not args.rule:
Expand Down
3 changes: 1 addition & 2 deletions src/discovery/SConscript
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@ for file in setup_sources:

local_sources = [
'__init__.py',
'disc_server_zk.py',
'disc_server.py',
'disc_utils.py',
'disc_consts.py',
'disc_exceptions.py',
'client.py',
'disc_zk.py',
'disc_cassdb.py',
'output.py',
'disc_auth_keystone.py',
]
local_sources_rules = []
for file in local_sources:
Expand Down
40 changes: 40 additions & 0 deletions src/discovery/disc_auth_keystone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#
# Copyright (c) 2013 Juniper Networks, Inc. All rights reserved.
#
#
# authentication/authorization functionality for discovery server
#

try:
from keystoneclient.middleware import auth_token
except ImportError:
from keystonemiddleware import auth_token
except Exception:
pass

class AuthServiceKeystone(object):

def __init__(self, conf):
self._conf_info = conf
# end __init__

# gets called from keystone middleware after token check
def token_valid(self, env, start_response):
status = env.get('HTTP_X_IDENTITY_STATUS')
return True if status != 'Invalid' else False

def validate_user_token(self, request):
# following config forces keystone middleware to always return the result
# back in HTTP_X_IDENTITY_STATUS env variable
conf_info = self._conf_info.copy()
conf_info['delay_auth_decision'] = True

auth_middleware = auth_token.AuthProtocol(self.token_valid, conf_info)
return auth_middleware(request.headers.environ, None)

def is_admin(self, request):
if not self.validate_user_token(request):
return False
roles = request.headers.environ.get('HTTP_X_ROLE', '').split(",")
return 'admin' in [x.lower() for x in roles]
# end class AuthServiceKeystone
68 changes: 68 additions & 0 deletions src/discovery/disc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from gevent.coros import BoundedSemaphore
from cfgm_common.rest import LinkObject

import disc_auth_keystone

def obj_to_json(obj):
# Non-null fields in object get converted to json fields
Expand Down Expand Up @@ -82,10 +83,15 @@ def __init__(self, args):
'auto_lb': 0,
'db_exc_unknown': 0,
'db_exc_info': '',
'wl_rejects_pub': 0,
'wl_rejects_sub': 0,
'auth_failures': 0,
}
self._ts_use = 1
self.short_ttl_map = {}
self._sem = BoundedSemaphore(1)
self._pub_wl = None
self._sub_wl = None

self._base_url = "http://%s:%s" % (self._args.listen_ip_addr,
self._args.listen_port)
Expand Down Expand Up @@ -225,6 +231,28 @@ def __init__(self, args):
self._sub_data = {}
for (client_id, service_type) in self._db_conn.subscriber_entries():
self.create_sub_data(client_id, service_type)

# build white list
if self._args.white_list_publish:
self._pub_wl = IPSet()
for prefix in self._args.white_list_publish.split(" "):
self._pub_wl.add(prefix)
if self._args.white_list_subscribe:
self._sub_wl = IPSet()
for prefix in self._args.white_list_subscribe.split(" "):
self._sub_wl.add(prefix)

self._auth_svc = None
if self._args.auth == 'keystone':
ks_conf = {
'auth_host': self._args.auth_host,
'auth_port': self._args.auth_port,
'auth_protocol': self._args.auth_protocol,
'admin_user': self._args.admin_user,
'admin_password': self._args.admin_password,
'admin_tenant_name': self._args.admin_tenant_name,
}
self._auth_svc = disc_auth_keystone.AuthServiceKeystone(ks_conf)
# end __init__

def config_log(self, msg, level):
Expand Down Expand Up @@ -374,6 +402,15 @@ def error_handler(self, *args, **kwargs):
raise
return error_handler

# decorator to authenticate request
def authenticate(func):
def wrapper(self, *args, **kwargs):
if self._auth_svc and not self._auth_svc.is_admin(bottle.request):
self._debug['auth_failures'] += 1
bottle.abort(401, 'Unauthorized')
return func(self, *args, **kwargs)
return wrapper

# 404 forces republish
def heartbeat(self, sig):
# self.syslog('heartbeat from "%s"' % sig)
Expand Down Expand Up @@ -425,6 +462,12 @@ def api_heartbeat(self):
@db_error_handler
def api_publish(self, end_point = None):
self._debug['msg_pubs'] += 1

source = bottle.request.headers.get('X-Forwarded-For', None)
if source and self._pub_wl and source not in self._pub_wl:
self._debug['wl_rejects_pub'] += 1
bottle.abort(401, 'Unauthorized request')

ctype = bottle.request.headers['content-type']
json_req = {}
try:
Expand Down Expand Up @@ -670,6 +713,12 @@ def adjust_in_use_list(self, pubs, in_use_list):
@db_error_handler
def api_subscribe(self):
self._debug['msg_subs'] += 1

source = bottle.request.headers.get('X-Forwarded-For', None)
if source and self._sub_wl and source not in self._sub_wl:
self._debug['wl_rejects_sub'] += 1
bottle.abort(401, 'Unauthorized request')

ctype = bottle.request.headers['content-type']
if 'application/json' in ctype:
json_req = bottle.request.json
Expand Down Expand Up @@ -846,6 +895,7 @@ def api_subscribe(self):

# on-demand API to load-balance existing subscribers across all currently available
# publishers. Needed if publisher gets added or taken down
@authenticate
def api_lb_service(self, service_type):
if service_type is None:
bottle.abort(405, "Missing service")
Expand Down Expand Up @@ -998,6 +1048,7 @@ def services_json(self, service_type=None):
return {'services': rsp}
# end services_json

@authenticate
def service_http_put(self, id):
self.syslog('Update service %s' % (id))
try:
Expand Down Expand Up @@ -1270,6 +1321,8 @@ def parse_args(args_str):
'logger_class': None,
'sandesh_send_rate_limit': SandeshSystem.get_sandesh_send_rate_limit(),
'cluster_id': None,
'white_list_publish': None,
'white_list_subscribe': None,
}

# per service options
Expand All @@ -1282,6 +1335,14 @@ def parse_args(args_str):
'cassandra_user' : None,
'cassandra_password' : None,
}
keystone_opts = {
'auth_host': '127.0.0.1',
'auth_port': '35357',
'auth_protocol': 'http',
'admin_user': '',
'admin_password': '',
'admin_tenant_name': '',
}

service_config = {}
cassandra_config = {}
Expand All @@ -1299,11 +1360,15 @@ def parse_args(args_str):
if section == "DEFAULTS":
defaults.update(dict(config.items("DEFAULTS")))
continue
if 'KEYSTONE' in config.sections():
keystone_opts.update(dict(config.items("KEYSTONE")))
continue
service_config[
section.lower()] = default_service_opts.copy()
service_config[section.lower()].update(
dict(config.items(section)))

defaults.update(keystone_opts)
parser.set_defaults(**defaults)

parser.add_argument(
Expand Down Expand Up @@ -1384,6 +1449,9 @@ def parse_args(args_str):
help="Sandesh send rate limit in messages/sec")
parser.add_argument("--cluster_id",
help="Used for database keyspace separation")
parser.add_argument(
"--auth", choices=['keystone'],
help="Type of authentication for user-requests")

args = parser.parse_args(remaining_argv)
args.conf_file = args.conf_file
Expand Down
1 change: 1 addition & 0 deletions src/discovery/test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ vnc_api
discoveryclient
sandesh
sandesh-common
keystonemiddleware

0 comments on commit 1f508c3

Please sign in to comment.