Commit 96a0b656 authored by Stavros Sachtouris's avatar Stavros Sachtouris
Browse files

Add ignore-ssl and ca-certs options to CLI

Refs #54

Both options are set either in the config file or as runtime
arguments.

ignore-ssl option allows connections without checking SSL certificates.

The ca-certs option provides a CA file for SSL authentication.

Normally, if no cert files are provided or they are invalid, kamaki
attempts to connect anyway, and an SSL Error ensues.

If ignore-ssl is set to "on", kamaki connects to servers regardless of
the existence or validity of a cert file and all SSL errors are ignored.

To help package maintainers to provide the correct CA file for their platform,
the default path is set in "kamaki/defaults.py" as CACERTS_DEFAULT_PATH
parent e70e95c2
...@@ -32,11 +32,47 @@ authentication URL is retrieved from the Synnefo Web UI and should be set as ...@@ -32,11 +32,47 @@ authentication URL is retrieved from the Synnefo Web UI and should be set as
the cloud URL for kamaki. Users of Synnefo clouds >=0.14 are advised against the cloud URL for kamaki. Users of Synnefo clouds >=0.14 are advised against
using any service-specific URLs. using any service-specific URLs.
SSL Authentication
------------------
HTTPS connections are authenticated with SSL since version 0.13, as long as a
file of CA Certificates is provided. The file can be set with the
`ca_certs configuration option <#available-options>`_ or with the *- -ca-certs*
runtime argument. Packages for various operating systems are built with a
default value for the 'ca_certs' configuration option, which is specific for
each platform.
If the CA Certificates are not provided or fail to authenticate a particular
cloud, ``kamaki`` will exit with an SSL error and instructions.
Users have the option to ignore SSL authentication errors with the
`ignore_ssl configuration option <#available-options>`_ or the *- -ignore-ssl*
runtime argument and connect to the cloud insecurely.
To check the SSL settings on an installation:
.. code-block:: console
$ kamaki config get ca_certs
$ kamaki config get ignore_ssl
To set a CA certificates path:
.. code-block:: console
$ kamaki config set ca_certs CA_FILE
To connect to clouds even when SSL authentication fails:
.. code-block:: console
$ kamaki config set ignore_ssl on
Migrating configuration file to latest version Migrating configuration file to latest version
---------------------------------------------- ----------------------------------------------
Each new version of kamaki might demand some changes to the configuration file. Each new version of kamaki might demand some changes to the configuration file.
Kamaki features a mechanism of automatic migration of the configration file to Kamaki features a mechanism of automatic migration of the configuration file to
the latest version, which involves heuristics for guessing and translating the the latest version, which involves heuristics for guessing and translating the
file. file.
...@@ -424,6 +460,12 @@ history and log files, log detail options and pithos-specific options. ...@@ -424,6 +460,12 @@ history and log files, log detail options and pithos-specific options.
* global.default_cloud <cloud name> * global.default_cloud <cloud name>
pick a cloud configuration as default. It must refer to an existing cloud. pick a cloud configuration as default. It must refer to an existing cloud.
* global.ca_certs <CA Certificates>
set the path of the file with the CA Certificates for SSL authentication
* global.ignore_ssl <on|off>
ignore / don't ignore SSL errors
* global.colors <on|off> * global.colors <on|off>
enable / disable colors in command line based uis. Requires ansicolors, enable / disable colors in command line based uis. Requires ansicolors,
otherwise it is ignored otherwise it is ignored
......
...@@ -45,7 +45,8 @@ from kamaki.cli.utils import ( ...@@ -45,7 +45,8 @@ from kamaki.cli.utils import (
from kamaki.cli.errors import CLIError, CLICmdSpecError from kamaki.cli.errors import CLIError, CLICmdSpecError
from kamaki.cli import logger from kamaki.cli import logger
from kamaki.clients.astakos import CachedAstakosClient from kamaki.clients.astakos import CachedAstakosClient
from kamaki.clients import ClientError from kamaki.clients import ClientError, KamakiSSLError
from kamaki.clients.utils import https
_debug = False _debug = False
...@@ -212,7 +213,7 @@ def _check_config_version(cnf): ...@@ -212,7 +213,7 @@ def _check_config_version(cnf):
'For automatic conversion, rerun and say Y']) 'For automatic conversion, rerun and say Y'])
def _init_session(arguments, is_non_API=False): def _init_session(arguments, is_non_api=False):
""" """
:returns: cloud name :returns: cloud name
""" """
...@@ -224,9 +225,21 @@ def _init_session(arguments, is_non_API=False): ...@@ -224,9 +225,21 @@ def _init_session(arguments, is_non_API=False):
_setup_logging(_debug, _verbose) _setup_logging(_debug, _verbose)
if _help or is_non_API: if _help or is_non_api:
return None return None
# Patch https for SSL Authentication
ca_file = arguments['ca_file'].value or _cnf.get('global', 'ca_certs')
ignore_ssl = arguments['ignore_ssl'].value or (
_cnf.get('global', 'ignore_ssl').lower() == 'on')
if ca_file:
https.patch_with_certs(ca_file)
else:
warn = red('WARNING: CA certifications path not set (insecure) ')
kloger.warning(warn)
https.patch_to_raise_ssl_errors(not ignore_ssl)
_check_config_version(_cnf.value) _check_config_version(_cnf.value)
_colors = _cnf.value.get('global', 'colors') _colors = _cnf.value.get('global', 'colors')
...@@ -252,7 +265,7 @@ def _init_session(arguments, is_non_API=False): ...@@ -252,7 +265,7 @@ def _init_session(arguments, is_non_API=False):
' kamaki config set default_cloud <cloud name>', ' kamaki config set default_cloud <cloud name>',
'To pick a cloud for the current session, use --cloud:', 'To pick a cloud for the current session, use --cloud:',
' kamaki --cloud=<cloud name> ...']) ' kamaki --cloud=<cloud name> ...'])
if not cloud in _cnf.value.keys('cloud'): if cloud not in _cnf.value.keys('cloud'):
raise CLIError( raise CLIError(
'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''), 'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''),
importance=3, details=[ importance=3, details=[
...@@ -482,11 +495,11 @@ def set_command_params(parameters): ...@@ -482,11 +495,11 @@ def set_command_params(parameters):
# CLI Choice: # CLI Choice:
def is_non_API(parser): def is_non_api(parser):
nonAPIs = ('history', 'config') non_apis = ('history', 'config')
for term in parser.unparsed: for term in parser.unparsed:
if not term.startswith('-'): if not term.startswith('-'):
if term in nonAPIs: if term in non_apis:
return True return True
return False return False
return False return False
...@@ -503,11 +516,11 @@ def main(func): ...@@ -503,11 +516,11 @@ def main(func):
except UnicodeDecodeError as ude: except UnicodeDecodeError as ude:
raise CLIError( raise CLIError(
'Invalid encoding in command', importance=3, details=[ 'Invalid encoding in command', importance=3, details=[
'The invalid term is #%s (with "%s" being 0)' % ( 'The invalid term is #%s (with "%s" being 0)' % (
i, exe), i, exe),
'Its encoding is invalid with current locale settings ' 'Encoding is invalid with current locale settings '
'(%s)' % pref_enc, '(%s)' % pref_enc,
'( %s )' % ude]) '( %s )' % ude])
for i, a in enumerate(internal_argv): for i, a in enumerate(internal_argv):
argv[i] = a argv[i] = a
...@@ -524,7 +537,12 @@ def main(func): ...@@ -524,7 +537,12 @@ def main(func):
'Print current version', ('-V', '--version')), 'Print current version', ('-V', '--version')),
options=RuntimeConfigArgument( options=RuntimeConfigArgument(
_config_arg, _config_arg,
'Override a config value', ('-o', '--options'))) 'Override a config value', ('-o', '--options')),
ignore_ssl=FlagArgument(
'Allow connections to SSL sites without certs',
('-k', '--ignore-ssl', '--insecure')),
ca_file=ValueArgument(
'CA certificates for SSL authentication', '--ca-certs'),)
) )
if parser.arguments['version'].value: if parser.arguments['version'].value:
exit(0) exit(0)
...@@ -546,6 +564,31 @@ def main(func): ...@@ -546,6 +564,31 @@ def main(func):
if _debug: if _debug:
raise err raise err
exit(1) exit(1)
except KamakiSSLError as err:
ca_arg = parser.arguments.get('ca_file')
ca = ca_arg.value if ca_arg and ca_arg.value else _cnf.get(
'global', 'ca_certs')
stderr.write(red('SSL Authentication failed\n'))
if ca:
stderr.write('Path used for CA certifications file: %s\n' % ca)
stderr.write('Please make sure the path is correct\n')
if not (ca_arg and ca_arg.value):
stderr.write('| To set the correct path:\n')
stderr.write('| kamaki config set ca_certs CA_FILE\n')
else:
stderr.write('| To use a CA certifications file:\n')
stderr.write('| kamaki config set ca_certs CA_FILE\n')
stderr.write('| OR run with --ca-certs=FILE_LOCATION\n')
stderr.write('| To ignore SSL errors and move on (%s):\n' % (
red('insecure')))
stderr.write('| kamaki config set ignore_ssl on\n')
stderr.write('| OR run with --ignore-ssl\n')
stderr.flush()
if _debug:
raise
stderr.write('| %s: %s\n' % (type(err), err))
stderr.flush()
exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
print('Canceled by user') print('Canceled by user')
exit(1) exit(1)
...@@ -576,7 +619,7 @@ def run_shell(exe, parser): ...@@ -576,7 +619,7 @@ def run_shell(exe, parser):
@main @main
def run_one_cmd(exe, parser): def run_one_cmd(exe, parser):
cloud = _init_session(parser.arguments, is_non_API(parser)) cloud = _init_session(parser.arguments, is_non_api(parser))
if parser.unparsed: if parser.unparsed:
global _history global _history
cnf = parser.arguments['config'] cnf = parser.arguments['config']
......
...@@ -62,6 +62,14 @@ CLOUD_PREFIX = 'cloud' ...@@ -62,6 +62,14 @@ CLOUD_PREFIX = 'cloud'
# Name of a shell variable to bypass the CONFIG_PATH value # Name of a shell variable to bypass the CONFIG_PATH value
CONFIG_ENV = 'KAMAKI_CONFIG' CONFIG_ENV = 'KAMAKI_CONFIG'
# Get default CA Certifications file path - created while packaging
try:
from kamaki import defaults
CACERTS_DEFAULT_PATH = getattr(defaults, 'CACERTS_DEFAULT_PATH', '')
except ImportError as ie:
log.debug('ImportError while loading default certs: %s' % ie)
CACERTS_DEFAULT_PATH = ''
version = '' version = ''
for c in '%s' % __version__: for c in '%s' % __version__:
if c not in '0.123456789': if c not in '0.123456789':
...@@ -99,26 +107,28 @@ DEFAULTS = { ...@@ -99,26 +107,28 @@ DEFAULTS = {
'image_cli': 'image', 'image_cli': 'image',
'imagecompute_cli': 'image', 'imagecompute_cli': 'image',
'config_cli': 'config', 'config_cli': 'config',
'history_cli': 'history' 'history_cli': 'history',
'ignore_ssl': 'off',
'ca_certs': CACERTS_DEFAULT_PATH,
# Optional command specs: # Optional command specs:
# 'service_cli': 'astakos' # 'service_cli': 'astakos'
# 'endpoint_cli': 'astakos' # 'endpoint_cli': 'astakos'
# 'commission_cli': 'astakos' # 'commission_cli': 'astakos'
}, },
CLOUD_PREFIX: { CLOUD_PREFIX: {
#'default': { # 'default': {
# 'url': '', # 'url': '',
# 'token': '' # 'token': ''
# 'pithos_container': 'THIS IS DANGEROUS' # 'pithos_container': 'THIS IS DANGEROUS'
# 'pithos_type': 'object-store', # 'pithos_type': 'object-store',
# 'pithos_version': 'v1', # 'pithos_version': 'v1',
# 'cyclades_type': 'compute', # 'cyclades_type': 'compute',
# 'cyclades_version': 'v2.0', # 'cyclades_version': 'v2.0',
# 'plankton_type': 'image', # 'plankton_type': 'image',
# 'plankton_version': '', # 'plankton_version': '',
# 'astakos_type': 'identity', # 'astakos_type': 'identity',
# 'astakos_version': 'v2.0' # 'astakos_version': 'v2.0'
#} # }
} }
} }
...@@ -192,22 +202,23 @@ class Config(RawConfigParser): ...@@ -192,22 +202,23 @@ class Config(RawConfigParser):
except KeyError: except KeyError:
cval = '' cval = ''
if gval and cval and ( if gval and cval and (
gval.lower().strip('/') != cval.lower().strip('/')): gval.lower().strip('/') != cval.lower().strip('/')
raise CLISyntaxError( ):
'Conflicting values for default %s' % (term), raise CLISyntaxError(
importance=2, details=[ 'Conflicting values for default %s' % (term),
' global.%s: %s' % (term, gval), importance=2, details=[
' %s.%s.%s: %s' % ( ' global.%s: %s' % (term, gval),
CLOUD_PREFIX, ' %s.%s.%s: %s' % (
default_cloud, CLOUD_PREFIX,
term, default_cloud,
cval), term,
'Please remove one of them manually:', cval),
' /config delete global.%s' % term, 'Please remove one of them manually:',
' or' ' /config delete global.%s' % term,
' /config delete %s.%s.%s' % ( ' or'
CLOUD_PREFIX, default_cloud, term), ' /config delete %s.%s.%s' % (
'and try again']) CLOUD_PREFIX, default_cloud, term),
'and try again'])
elif gval: elif gval:
err.write(u'... rescue %s.%s => %s.%s.%s\n' % ( err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
s, term, CLOUD_PREFIX, default_cloud, term)) s, term, CLOUD_PREFIX, default_cloud, term))
......
...@@ -41,7 +41,6 @@ from kamaki.cli.errors import CLIUnknownCommand, CLIError ...@@ -41,7 +41,6 @@ from kamaki.cli.errors import CLIUnknownCommand, CLIError
def run(cloud, parser): def run(cloud, parser):
group = get_command_group(list(parser.unparsed), parser.arguments) group = get_command_group(list(parser.unparsed), parser.arguments)
if not group: if not group:
#parser.parser.print_help()
parser.print_help() parser.print_help()
_groups_help(parser.arguments) _groups_help(parser.arguments)
exit(0) exit(0)
...@@ -100,8 +99,8 @@ def run(cloud, parser): ...@@ -100,8 +99,8 @@ def run(cloud, parser):
astakos, help_message = init_cached_authenticator(_cnf, cloud, kloger) if ( astakos, help_message = init_cached_authenticator(_cnf, cloud, kloger) if (
cloud) else (None, []) cloud) else (None, [])
if not astakos: if not astakos:
from kamaki.cli import is_non_API from kamaki.cli import is_non_api
if not is_non_API(parser): if not is_non_api(parser):
raise CLIError( raise CLIError(
'Failed to initialize an identity client', 'Failed to initialize an identity client',
importance=3, details=help_message) importance=3, details=help_message)
......
...@@ -174,6 +174,8 @@ class Shell(Cmd): ...@@ -174,6 +174,8 @@ class Shell(Cmd):
tmp_args.pop('debug', None) tmp_args.pop('debug', None)
tmp_args.pop('verbose', None) tmp_args.pop('verbose', None)
tmp_args.pop('config', None) tmp_args.pop('config', None)
tmp_args.pop('ignore_ssl', None)
tmp_args.pop('ca_file', None)
help_parser = ArgumentParseManager( help_parser = ArgumentParseManager(
cmd_name, tmp_args, required, cmd_name, tmp_args, required,
syntax=syntax, description=descr, check_required=False) syntax=syntax, description=descr, check_required=False)
......
...@@ -39,6 +39,7 @@ from httplib import ResponseNotReady, HTTPException ...@@ -39,6 +39,7 @@ from httplib import ResponseNotReady, HTTPException
from time import sleep from time import sleep
from random import random from random import random
from logging import getLogger from logging import getLogger
import ssl
from kamaki.clients.utils import https from kamaki.clients.utils import https
...@@ -93,6 +94,10 @@ class ClientError(Exception): ...@@ -93,6 +94,10 @@ class ClientError(Exception):
self.details = details if details else [] self.details = details if details else []
class KamakiSSLError(ClientError):
"""SSL Connection Error"""
class Logged(object): class Logged(object):
LOG_TOKEN = False LOG_TOKEN = False
...@@ -177,20 +182,23 @@ class RequestManager(Logged): ...@@ -177,20 +182,23 @@ class RequestManager(Logged):
""" """
self._encode_headers() self._encode_headers()
self.dump_log() self.dump_log()
conn.request( try:
method=self.method.upper(), conn.request(
url=self.path.encode('utf-8'), method=self.method.upper(),
headers=self.headers, url=self.path.encode('utf-8'),
body=self.data) headers=self.headers,
sendlog.info('') body=self.data)
keep_trying = TIMEOUT sendlog.info('')
while keep_trying > 0: keep_trying = TIMEOUT
try: while keep_trying > 0:
return conn.getresponse() try:
except ResponseNotReady: return conn.getresponse()
wait = 0.03 * random() except ResponseNotReady:
sleep(wait) wait = 0.03 * random()
keep_trying -= wait sleep(wait)
keep_trying -= wait
except ssl.SSLError as ssle:
raise KamakiSSLError('SSL Connection error (%s)' % ssle)
plog = ('\t[%s]' % self) if self.LOG_PID else '' plog = ('\t[%s]' % self) if self.LOG_PID else ''
logmsg = 'Kamaki Timeout %s %s%s' % (self.method, self.path, plog) logmsg = 'Kamaki Timeout %s %s%s' % (self.method, self.path, plog)
recvlog.debug(logmsg) recvlog.debug(logmsg)
......
...@@ -33,10 +33,13 @@ ...@@ -33,10 +33,13 @@
from logging import getLogger from logging import getLogger
import inspect import inspect
import ssl
from astakosclient import AstakosClientException, parse_endpoints from astakosclient import AstakosClientException, parse_endpoints
import astakosclient import astakosclient
from kamaki.clients import Client, ClientError, RequestManager, recvlog from kamaki.clients import (
Client, ClientError, KamakiSSLError, RequestManager, recvlog)
from kamaki.clients.utils import https from kamaki.clients.utils import https
...@@ -53,6 +56,8 @@ def _astakos_error(foo): ...@@ -53,6 +56,8 @@ def _astakos_error(foo):
try: try:
return foo(self, *args, **kwargs) return foo(self, *args, **kwargs)
except AstakosClientException as sace: except AstakosClientException as sace:
if isinstance(getattr(sace, 'errobject', None), ssl.SSLError):
raise KamakiSSLError('SSL Connection error (%s)' % sace)
raise AstakosClientError( raise AstakosClientError(
getattr(sace, 'message', '%s' % sace), getattr(sace, 'message', '%s' % sace),
details=sace.details, status=sace.status) details=sace.details, status=sace.status)
......
# Copyright 2014 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
# ABOUT THIS FILE
# Set default values for various settings for use in the code. For example, set
# OS-specific settings when packaging kamaki for a platform.
# The default path for the CA certifications file of the host system
CACERTS_DEFAULT_PATH = ''
# To overwrite any of the above, append new assignments bellow
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment