Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement client-side TLS certificates #233

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ MANIFEST
dist/
salt_pepper.egg-info/
.tox/
.eggs/
7 changes: 6 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Installation
Usage
-----

Basic usage is in heavy flux. You can run pepper using the script in %PYTHONHOME%/scripts/pepper (a pepper.cmd wrapper is provided for convenience to Windows users).
You can run pepper using the script in %PYTHONHOME%/scripts/pepper (a pepper.cmd wrapper is provided for convenience to Windows users).

.. code-block:: bash

Expand Down Expand Up @@ -70,6 +70,11 @@ or in a configuration file ``$HOME/.pepperrc`` with the following syntax :
SALTAPI_PASS=saltdev
SALTAPI_EAUTH=pam

# if you use client-side TLS certificates
SALTAPI_CA_BUNDLE=/path/to/ca-chain.cert.pem
SALTAPI_CLIENT_CERT=/path/to/client.cert.pem
SALTAPI_CLIENT_CERT_KEY=/path/to/client.key.pem

Contributing
------------

Expand Down
145 changes: 85 additions & 60 deletions pepper/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,12 @@
)


try:
# Python 3
if sys.version_info[0] == 3:
from configparser import ConfigParser, RawConfigParser
except ImportError:
# Python 2
JSONDecodeError = json.JSONDecodeError
elif sys.version_info[0] == 2:
from ConfigParser import ConfigParser, RawConfigParser

try:
# Python 3
JSONDecodeError = json.decode.JSONDecodeError
except AttributeError:
# Python 2
JSONDecodeError = ValueError

try:
input = raw_input
except NameError:
pass

if sys.version_info[0] == 2:
FileNotFoundError = IOError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -132,8 +118,48 @@ def parse(self):
self.parser.add_option(
'--ignore-ssl-errors', action='store_true', dest='ignore_ssl_certificate_errors', default=False,
help=textwrap.dedent('''
Ignore any SSL certificate that may be encountered. Note that it is
recommended to resolve certificate errors for production.
Ignore any SSL certificate that may be encountered. Note that
it is recommended to resolve certificate errors for production.
This option makes the `ca-bundle` flag ignored.
'''),
)

self.parser.add_option(
'--ca-bundle',
dest='ca_bundle',
default=None,
help=textwrap.dedent('''
The path to a file of concatenated CA certificates in PEM
format, or a directory of such files.
'''),
)

self.parser.add_option(
'--client-cert',
dest='client_cert',
default=None,
help=textwrap.dedent('''
Client side certificate to send with requests. Should be a path
to a single file in PEM format containing the certificate
as well as any number of CA certificates needed to establish
the certificate’s authenticity.

If `--client-cert-key` is not given, this file must also contain
the private key of the client certificate.
'''),
)

self.parser.add_option(
'--client-cert-key',
dest='client_cert_key',
default=None,
help=textwrap.dedent('''
Private key for the client side certificate given in
`--client-cert`.

If `--client-cert` is given but this argument is not, then the
client cert file given with `--client-cert` must contain the
private key.
'''),
)

Expand All @@ -145,6 +171,9 @@ def parse(self):
s = repr(toggled_options).strip("[]")
self.parser.error("Options %s are mutually exclusive" % s)

if self.options.client_cert_key and not self.options.client_cert:
self.parser.error("'--client-cert-key' given without '--client-cert'")

def add_globalopts(self):
'''
Misc global options
Expand Down Expand Up @@ -377,6 +406,10 @@ def get_login_details(self):
'SALTAPI_USER': None,
'SALTAPI_PASS': None,
'SALTAPI_EAUTH': 'auto',

'SALTAPI_CA_BUNDLE': None,
'SALTAPI_CLIENT_CERT': None,
'SALTAPI_CLIENT_CERT_KEY': None,
}

try:
Expand All @@ -396,33 +429,37 @@ def get_login_details(self):
for key, value in list(results.items()):
results[key] = os.environ.get(key, results[key])

if results['SALTAPI_EAUTH'] == 'kerberos':
results['SALTAPI_PASS'] = None
ret = {}

if self.options.eauth:
results['SALTAPI_EAUTH'] = self.options.eauth
if self.options.token_expire:
results['SALTAPI_TOKEN_EXPIRE'] = self.options.token_expire
if self.options.username is None and results['SALTAPI_USER'] is None:
if not self.options.username and not results.get('SALTAPI_USER'):
if self.options.interactive:
results['SALTAPI_USER'] = input('Username: ')
ret['username'] = input('Username: ')
else:
raise PepperAuthException("SALTAPI_USER required")
else:
if self.options.username is not None:
results['SALTAPI_USER'] = self.options.username
if self.options.password is None and \
results['SALTAPI_PASS'] is None and \
ret['username'] = self.options.username or results["SALTAPI_USER"]

if not self.options.password and \
not results['SALTAPI_PASS'] and \
results['SALTAPI_EAUTH'] != 'kerberos':
if self.options.interactive:
results['SALTAPI_PASS'] = getpass.getpass(prompt='Password: ')
ret['password'] = getpass.getpass(prompt='Password: ')
else:
raise PepperAuthException("SALTAPI_PASS required")
else:
if self.options.password is not None:
results['SALTAPI_PASS'] = self.options.password
ret['password'] = self.options.password or results['SALTAPI_PASS']

if results['SALTAPI_EAUTH'] == 'kerberos':
ret['password'] = None

return results
ret['eauth'] = self.options.eauth or results.get('SALTAPI_EAUTH')
ret['token_expire'] = self.options.token_expire or results.get('SALTAPI_TOKEN_EXPIRE')
ret['token_expire'] = ret['token_expire'] and int(ret['token_expire'])
ret['ca_bundle'] = self.options.ca_bundle or results.get('SALTAPI_CA_BUNDLE')
ret['client_cert'] = self.options.client_cert or results.get('SALTAPI_CLIENT_CERT')
ret['client_cert_key'] = self.options.client_cert_key or results.get('SALTAPI_CLIENT_CERT_KEY')

return ret

def parse_url(self):
'''
Expand Down Expand Up @@ -451,25 +488,6 @@ def parse_url(self):

return url

def parse_login(self):
'''
Extract the authentication credentials
'''
login_details = self.get_login_details()

# Auth values placeholder; grab interactively at CLI or from config
username = login_details['SALTAPI_USER']
password = login_details['SALTAPI_PASS']
eauth = login_details['SALTAPI_EAUTH']

ret = dict(username=username, password=password, eauth=eauth)

token_expire = login_details.get('SALTAPI_TOKEN_EXPIRE', None)
if token_expire:
ret['token_expire'] = int(token_expire)

return ret

def parse_cmd(self, api):
'''
Extract the low data for a command from the passed CLI params
Expand Down Expand Up @@ -604,23 +622,24 @@ def poll_for_returns(self, api, load):
if failed:
yield exit_code, [{'Failed': failed}]

def login(self, api):
def login(self, api, login_details):
login = api.token if self.options.userun else api.login

if self.options.mktoken:
token_file = self.options.cache
try:
with open(token_file, 'rt') as f:
auth = json.load(f)
if auth['expire'] < time.time()+30:
if auth['expire'] < time.time() + 30:
logger.error('Login token expired')
raise Exception('Login token expired')
except Exception as e:
if e.args[0] != 2:
logger.error('Unable to load login token from {0} {1}'.format(token_file, str(e)))
if os.path.isfile(token_file):
os.remove(token_file)
auth = login(**self.parse_login())
auth = login(**login_details)

try:
oldumask = os.umask(0)
fdsc = os.open(token_file, os.O_WRONLY | os.O_CREAT, 0o600)
Expand All @@ -631,7 +650,7 @@ def login(self, api):
finally:
os.umask(oldumask)
else:
auth = login(**self.parse_login())
auth = login(**login_details)

api.auth = auth
self.auth = auth
Expand Down Expand Up @@ -662,12 +681,18 @@ def run(self):
rootLogger.addHandler(logging.StreamHandler())
rootLogger.setLevel(max(logging.ERROR - (self.options.verbose * 10), 1))

login_details = self.get_login_details()

api = pepper.Pepper(
self.parse_url(),
debug_http=self.options.debug_http,
ignore_ssl_errors=self.options.ignore_ssl_certificate_errors)
ignore_ssl_errors=self.options.ignore_ssl_certificate_errors,
ca_bundle=login_details.get("ca_bundle"),
client_cert=login_details.get("client_cert"),
client_cert_key=login_details.get("client_cert_key"),
)

self.login(api)
self.login(api, login_details)

load = self.parse_cmd(api)

Expand Down
Loading