mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge pull request #38664 from clinta/x509-passphrase2
X509 Improvements. Expose setting permissions, encrypted private keys, and combined key and cert management in one state
This commit is contained in:
commit
6663107021
2 changed files with 326 additions and 235 deletions
|
@ -74,11 +74,6 @@ CERT_DEFAULTS = {
|
|||
'serial_bits': 64,
|
||||
'algorithm': 'sha256'
|
||||
}
|
||||
PEM_RE = re.compile(
|
||||
r"\s*(?P<pem_header>-----(?:.+?)-----)\s+"
|
||||
r"(?P<pem_body>.+?)\s+(?P<pem_footer>-----(?:.+?)-----)\s*",
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def __virtual__():
|
||||
|
@ -146,11 +141,13 @@ def _new_extension(name, value, critical=0, issuer=None, _pyfree=1):
|
|||
lhash = None
|
||||
except AttributeError:
|
||||
lhash = M2Crypto.m2.x509v3_lhash() # pylint: disable=no-member
|
||||
ctx = M2Crypto.m2.x509v3_set_conf_lhash(lhash) # pylint: disable=no-member
|
||||
#ctx not zeroed
|
||||
ctx = M2Crypto.m2.x509v3_set_conf_lhash(
|
||||
lhash) # pylint: disable=no-member
|
||||
# ctx not zeroed
|
||||
_fix_ctx(ctx, issuer)
|
||||
x509_ext_ptr = M2Crypto.m2.x509v3_ext_conf(lhash, ctx, name, value) # pylint: disable=no-member
|
||||
#ctx,lhash freed
|
||||
x509_ext_ptr = M2Crypto.m2.x509v3_ext_conf(
|
||||
lhash, ctx, name, value) # pylint: disable=no-member
|
||||
# ctx,lhash freed
|
||||
|
||||
if x509_ext_ptr is None:
|
||||
raise M2Crypto.X509.X509Error(
|
||||
|
@ -353,18 +350,28 @@ def _get_certificate_obj(cert):
|
|||
return M2Crypto.X509.load_cert_string(text)
|
||||
|
||||
|
||||
def _get_private_key_obj(private_key):
|
||||
def _get_private_key_obj(private_key, passphrase=None):
|
||||
'''
|
||||
Returns a private key object based on PEM text.
|
||||
'''
|
||||
private_key = _text_or_file(private_key)
|
||||
private_key = get_pem_entry(private_key, pem_type='RSA PRIVATE KEY')
|
||||
rsaprivkey = M2Crypto.RSA.load_key_string(private_key)
|
||||
rsaprivkey = M2Crypto.RSA.load_key_string(
|
||||
private_key, callback=_passphrase_callback(passphrase))
|
||||
evpprivkey = M2Crypto.EVP.PKey()
|
||||
evpprivkey.assign_rsa(rsaprivkey)
|
||||
return evpprivkey
|
||||
|
||||
|
||||
def _passphrase_callback(passphrase):
|
||||
'''
|
||||
Returns a callback function used to supply a passphrase for private keys
|
||||
'''
|
||||
def f(*args):
|
||||
return passphrase
|
||||
return f
|
||||
|
||||
|
||||
def _get_request_obj(csr):
|
||||
'''
|
||||
Returns a CSR object based on PEM text.
|
||||
|
@ -397,6 +404,8 @@ def _make_regex(pem_type):
|
|||
'''
|
||||
return re.compile(
|
||||
r"\s*(?P<pem_header>-----BEGIN {0}-----)\s+"
|
||||
r"(?:(?P<proc_type>Proc-Type: 4,ENCRYPTED)\s*)?"
|
||||
r"(?:(?P<dek_info>DEK-Info: (?:DES-[3A-Z\-]+,[0-9A-F]{{16}}|[0-9A-Z\-]+,[0-9A-F]{{32}}))\s*)?"
|
||||
r"(?P<pem_body>.+?)\s+(?P<pem_footer>"
|
||||
r"-----END {1}-----)\s*".format(pem_type, pem_type),
|
||||
re.DOTALL
|
||||
|
@ -424,18 +433,21 @@ def get_pem_entry(text, pem_type=None):
|
|||
MIICyzCC Ar8CAQI...-----END CERTIFICATE REQUEST"
|
||||
'''
|
||||
text = _text_or_file(text)
|
||||
# Replace encoded newlines
|
||||
text = text.replace('\\n', '\n')
|
||||
|
||||
_match = None
|
||||
|
||||
if len(text.splitlines()) == 1 and text.startswith('-----') and text.endswith('-----'):
|
||||
if len(text.splitlines()) == 1 and text.startswith(
|
||||
'-----') and text.endswith('-----'):
|
||||
# mine.get returns the PEM on a single line, we fix this
|
||||
pem_fixed = []
|
||||
pem_temp = text
|
||||
while len(pem_temp) > 0:
|
||||
if pem_temp.startswith('-----'):
|
||||
# Grab ----(.*)---- blocks
|
||||
pem_fixed.append(pem_temp[:pem_temp.index('-----', 5)+5])
|
||||
pem_temp = pem_temp[pem_temp.index('-----', 5)+5:]
|
||||
pem_fixed.append(pem_temp[:pem_temp.index('-----', 5) + 5])
|
||||
pem_temp = pem_temp[pem_temp.index('-----', 5) + 5:]
|
||||
else:
|
||||
# grab base64 chunks
|
||||
if pem_temp[:64].count('-') == 0:
|
||||
|
@ -446,39 +458,34 @@ def get_pem_entry(text, pem_type=None):
|
|||
pem_temp = pem_temp[pem_temp.index('-'):]
|
||||
text = "\n".join(pem_fixed)
|
||||
|
||||
if not pem_type:
|
||||
# Find using a regex iterator, pick the first match
|
||||
for _match in PEM_RE.finditer(text):
|
||||
if _match:
|
||||
break
|
||||
if not _match:
|
||||
raise salt.exceptions.SaltInvocationError(
|
||||
'PEM text not valid:\n{0}'.format(text)
|
||||
)
|
||||
_match_dict = _match.groupdict()
|
||||
pem_header = _match_dict['pem_header']
|
||||
pem_footer = _match_dict['pem_footer']
|
||||
pem_body = _match_dict['pem_body']
|
||||
else:
|
||||
_dregex = _make_regex('[0-9A-Z ]+')
|
||||
errmsg = 'PEM text not valid:\n{0}'.format(text)
|
||||
if pem_type:
|
||||
_dregex = _make_regex(pem_type)
|
||||
for _match in _dregex.finditer(text):
|
||||
if _match:
|
||||
break
|
||||
if not _match:
|
||||
raise salt.exceptions.SaltInvocationError(
|
||||
'PEM does not contain a single entry of type {0}:\n'
|
||||
'{1}'.format(pem_type, text)
|
||||
)
|
||||
_match_dict = _match.groupdict()
|
||||
pem_header = _match_dict['pem_header']
|
||||
pem_footer = _match_dict['pem_footer']
|
||||
pem_body = _match_dict['pem_body']
|
||||
errmsg = ('PEM does not contain a single entry of type {0}:\n'
|
||||
'{1}'.format(pem_type, text))
|
||||
|
||||
for _match in _dregex.finditer(text):
|
||||
if _match:
|
||||
break
|
||||
if not _match:
|
||||
raise salt.exceptions.SaltInvocationError(errmsg)
|
||||
_match_dict = _match.groupdict()
|
||||
pem_header = _match_dict['pem_header']
|
||||
proc_type = _match_dict['proc_type']
|
||||
dek_info = _match_dict['dek_info']
|
||||
pem_footer = _match_dict['pem_footer']
|
||||
pem_body = _match_dict['pem_body']
|
||||
|
||||
# Remove all whitespace from body
|
||||
pem_body = ''.join(pem_body.split())
|
||||
|
||||
# Generate correctly formatted pem
|
||||
ret = pem_header + '\n'
|
||||
if proc_type:
|
||||
ret += proc_type + '\n'
|
||||
if dek_info:
|
||||
ret += dek_info + '\n' + '\n'
|
||||
for i in range(0, len(pem_body), 64):
|
||||
ret += pem_body[i:i + 64] + '\n'
|
||||
ret += pem_footer + '\n'
|
||||
|
@ -649,7 +656,7 @@ def read_crl(crl):
|
|||
return crlparsed
|
||||
|
||||
|
||||
def get_public_key(key, asObj=False):
|
||||
def get_public_key(key, passphrase=None, asObj=False):
|
||||
'''
|
||||
Returns a string containing the public key in PEM format.
|
||||
|
||||
|
@ -687,7 +694,8 @@ def get_public_key(key, asObj=False):
|
|||
rsa = csr.get_pubkey().get_rsa()
|
||||
if (text.startswith('-----BEGIN PRIVATE KEY-----') or
|
||||
text.startswith('-----BEGIN RSA PRIVATE KEY-----')):
|
||||
rsa = M2Crypto.RSA.load_key_string(text)
|
||||
rsa = M2Crypto.RSA.load_key_string(
|
||||
text, callback=_passphrase_callback(passphrase))
|
||||
|
||||
if asObj:
|
||||
evppubkey = M2Crypto.EVP.PKey()
|
||||
|
@ -698,7 +706,7 @@ def get_public_key(key, asObj=False):
|
|||
return bio.read_all()
|
||||
|
||||
|
||||
def get_private_key_size(private_key):
|
||||
def get_private_key_size(private_key, passphrase=None):
|
||||
'''
|
||||
Returns the bit length of a private key in PEM format.
|
||||
|
||||
|
@ -711,7 +719,7 @@ def get_private_key_size(private_key):
|
|||
|
||||
salt '*' x509.get_private_key_size /etc/pki/mycert.key
|
||||
'''
|
||||
return _get_private_key_obj(private_key).size() * 8
|
||||
return _get_private_key_obj(private_key, passphrase).size() * 8
|
||||
|
||||
|
||||
def write_pem(text, path, overwrite=True, pem_type=None):
|
||||
|
@ -768,7 +776,12 @@ def write_pem(text, path, overwrite=True, pem_type=None):
|
|||
return 'PEM written to {0}'.format(path)
|
||||
|
||||
|
||||
def create_private_key(path=None, text=False, bits=2048, verbose=True):
|
||||
def create_private_key(path=None,
|
||||
text=False,
|
||||
bits=2048,
|
||||
passphrase=None,
|
||||
cipher='aes_128_cbc',
|
||||
verbose=True):
|
||||
'''
|
||||
Creates a private key in PEM format.
|
||||
|
||||
|
@ -783,6 +796,12 @@ def create_private_key(path=None, text=False, bits=2048, verbose=True):
|
|||
bits:
|
||||
Length of the private key in bits. Default 2048
|
||||
|
||||
passphrase:
|
||||
Passphrase for encryting the private key
|
||||
|
||||
cipher:
|
||||
Cipher for encrypting the private key. Has no effect if passhprase is None.
|
||||
|
||||
verbose:
|
||||
Provide visual feedback on stdout. Default True
|
||||
|
||||
|
@ -810,7 +829,12 @@ def create_private_key(path=None, text=False, bits=2048, verbose=True):
|
|||
rsa = M2Crypto.RSA.gen_key(bits, M2Crypto.m2.RSA_F4, _callback_func)
|
||||
# pylint: enable=no-member
|
||||
bio = M2Crypto.BIO.MemoryBuffer()
|
||||
rsa.save_key_bio(bio, cipher=None)
|
||||
if passphrase is None:
|
||||
cipher = None
|
||||
rsa.save_key_bio(
|
||||
bio,
|
||||
cipher=cipher,
|
||||
callback=_passphrase_callback(passphrase))
|
||||
|
||||
if path:
|
||||
return write_pem(
|
||||
|
@ -951,10 +975,20 @@ def create_crl( # pylint: disable=too-many-arguments,too-many-locals
|
|||
get_pem_entry(signing_private_key))
|
||||
|
||||
try:
|
||||
crltext = crl.export(cert, key, OpenSSL.crypto.FILETYPE_PEM, days=days_valid, digest=bytes(digest))
|
||||
crltext = crl.export(
|
||||
cert,
|
||||
key,
|
||||
OpenSSL.crypto.FILETYPE_PEM,
|
||||
days=days_valid,
|
||||
digest=bytes(digest))
|
||||
except TypeError:
|
||||
log.warning('Error signing crl with specified digest. Are you using pyopenssl 0.15 or newer? The default md5 digest will be used.')
|
||||
crltext = crl.export(cert, key, OpenSSL.crypto.FILETYPE_PEM, days=days_valid)
|
||||
log.warning(
|
||||
'Error signing crl with specified digest. Are you using pyopenssl 0.15 or newer? The default md5 digest will be used.')
|
||||
crltext = crl.export(
|
||||
cert,
|
||||
key,
|
||||
OpenSSL.crypto.FILETYPE_PEM,
|
||||
days=days_valid)
|
||||
|
||||
if text:
|
||||
return crltext
|
||||
|
@ -1125,6 +1159,9 @@ def create_certificate(
|
|||
certificate, and the public key matching ``signing_private_key`` will
|
||||
be used to create the certificate.
|
||||
|
||||
signing_private_key_passphrase:
|
||||
Passphrase used to decrypt the signing_private_key.
|
||||
|
||||
signing_cert:
|
||||
A certificate matching the private key that will be used to sign this
|
||||
certificate. This is used to populate the issuer values in the
|
||||
|
@ -1147,6 +1184,10 @@ def create_certificate(
|
|||
to the certificate, subject or extension information in the CSR will
|
||||
be lost.
|
||||
|
||||
public_key_passphrase:
|
||||
If the public key is supplied as a private key, this is the passphrase
|
||||
used to decrypt it.
|
||||
|
||||
csr:
|
||||
A file or PEM string containing a certificate signing request. This
|
||||
will be used to supply the subject, extensions and public key of a
|
||||
|
@ -1296,6 +1337,8 @@ def create_certificate(
|
|||
raise salt.exceptions.SaltInvocationError(
|
||||
'Either path or text must be specified, not both.')
|
||||
|
||||
if 'public_key_passphrase' not in kwargs:
|
||||
kwargs['public_key_passphrase'] = None
|
||||
if ca_server:
|
||||
if 'signing_policy' not in kwargs:
|
||||
raise salt.exceptions.SaltInvocationError(
|
||||
|
@ -1309,13 +1352,15 @@ def create_certificate(
|
|||
if 'public_key' in kwargs:
|
||||
# Strip newlines to make passing through as cli functions easier
|
||||
kwargs['public_key'] = get_public_key(
|
||||
kwargs['public_key']).replace('\n', '')
|
||||
kwargs['public_key'],
|
||||
passphrase=kwargs['public_key_passphrase']).replace('\n', '')
|
||||
|
||||
# Remove system entries in kwargs
|
||||
# Including listen_in and preqreuired because they are not included
|
||||
# in STATE_INTERNAL_KEYWORDS
|
||||
# for salt 2014.7.2
|
||||
for ignore in list(_STATE_INTERNAL_KEYWORDS) + ['listen_in', 'preqrequired', '__prerequired__']:
|
||||
for ignore in list(_STATE_INTERNAL_KEYWORDS) + \
|
||||
['listen_in', 'preqrequired', '__prerequired__']:
|
||||
kwargs.pop(ignore, None)
|
||||
|
||||
cert_txt = __salt__['publish.publish'](
|
||||
|
@ -1380,7 +1425,8 @@ def create_certificate(
|
|||
cert.set_subject(csr.get_subject())
|
||||
csrexts = read_csr(kwargs['csr'])['X509v3 Extensions']
|
||||
|
||||
cert.set_pubkey(get_public_key(kwargs['public_key'], asObj=True))
|
||||
cert.set_pubkey(get_public_key(kwargs['public_key'],
|
||||
passphrase=kwargs['public_key_passphrase'], asObj=True))
|
||||
|
||||
subject = cert.get_subject()
|
||||
|
||||
|
@ -1429,13 +1475,19 @@ def create_certificate(
|
|||
|
||||
cert.add_ext(ext)
|
||||
|
||||
if 'signing_private_key_passphrase' not in kwargs:
|
||||
kwargs['signing_private_key_passphrase'] = None
|
||||
if 'testrun' in kwargs and kwargs['testrun'] is True:
|
||||
cert_props = read_certificate(cert)
|
||||
cert_props['Issuer Public Key'] = get_public_key(
|
||||
kwargs['signing_private_key'])
|
||||
kwargs['signing_private_key'],
|
||||
passphrase=kwargs['signing_private_key_passphrase'])
|
||||
return cert_props
|
||||
|
||||
if not verify_private_key(kwargs['signing_private_key'], signing_cert):
|
||||
if not verify_private_key(private_key=kwargs['signing_private_key'],
|
||||
passphrase=kwargs[
|
||||
'signing_private_key_passphrase'],
|
||||
public_key=signing_cert):
|
||||
raise salt.exceptions.SaltInvocationError(
|
||||
'signing_private_key: {0} '
|
||||
'does no match signing_cert: {1}'.format(
|
||||
|
@ -1445,7 +1497,8 @@ def create_certificate(
|
|||
)
|
||||
|
||||
cert.sign(
|
||||
_get_private_key_obj(kwargs['signing_private_key']),
|
||||
_get_private_key_obj(kwargs['signing_private_key'],
|
||||
passphrase=kwargs['signing_private_key_passphrase']),
|
||||
kwargs['algorithm']
|
||||
)
|
||||
|
||||
|
@ -1459,7 +1512,7 @@ def create_certificate(
|
|||
else:
|
||||
prepend = ''
|
||||
write_pem(text=cert.as_pem(), path=os.path.join(kwargs['copypath'],
|
||||
prepend + kwargs['serial_number']+'.crt'),
|
||||
prepend + kwargs['serial_number'] + '.crt'),
|
||||
pem_type='CERTIFICATE')
|
||||
|
||||
if path:
|
||||
|
@ -1519,7 +1572,8 @@ def create_csr(path=None, text=False, **kwargs):
|
|||
|
||||
if 'private_key' not in kwargs and 'public_key' in kwargs:
|
||||
kwargs['private_key'] = kwargs['public_key']
|
||||
log.warning("OpenSSL no longer allows working with non-signed CSRs. A private_key must be specified. Attempting to use public_key as private_key")
|
||||
log.warning(
|
||||
"OpenSSL no longer allows working with non-signed CSRs. A private_key must be specified. Attempting to use public_key as private_key")
|
||||
|
||||
if 'private_key' not in kwargs not in kwargs:
|
||||
raise salt.exceptions.SaltInvocationError('private_key is required')
|
||||
|
@ -1527,7 +1581,10 @@ def create_csr(path=None, text=False, **kwargs):
|
|||
if 'public_key' not in kwargs:
|
||||
kwargs['public_key'] = kwargs['private_key']
|
||||
|
||||
csr.set_pubkey(get_public_key(kwargs['public_key'], asObj=True))
|
||||
if 'public_key_passphrase' not in kwargs:
|
||||
kwargs['public_key_passphrase'] = None
|
||||
csr.set_pubkey(get_public_key(kwargs['public_key'],
|
||||
passphrase=kwargs['public_key_passphrase'], asObj=True))
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
for entry, num in six.iteritems(subject.nid):
|
||||
|
@ -1565,7 +1622,8 @@ def create_csr(path=None, text=False, **kwargs):
|
|||
|
||||
csr.add_extensions(extstack)
|
||||
|
||||
csr.sign(_get_private_key_obj(kwargs['private_key']), kwargs['algorithm'])
|
||||
csr.sign(_get_private_key_obj(kwargs['private_key'],
|
||||
passphrase=kwargs['public_key_passphrase']), kwargs['algorithm'])
|
||||
|
||||
if path:
|
||||
return write_pem(
|
||||
|
@ -1577,7 +1635,7 @@ def create_csr(path=None, text=False, **kwargs):
|
|||
return csr.as_pem()
|
||||
|
||||
|
||||
def verify_private_key(private_key, public_key):
|
||||
def verify_private_key(private_key, public_key, passphrase=None):
|
||||
'''
|
||||
Verify that 'private_key' matches 'public_key'
|
||||
|
||||
|
@ -1589,6 +1647,9 @@ def verify_private_key(private_key, public_key):
|
|||
The public key to verify, can be a string or path to a PEM formatted
|
||||
certificate, csr, or another private key.
|
||||
|
||||
passphrase:
|
||||
Passphrase to decrypt the private key.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
@ -1596,10 +1657,12 @@ def verify_private_key(private_key, public_key):
|
|||
salt '*' x509.verify_private_key private_key=/etc/pki/myca.key \\
|
||||
public_key=/etc/pki/myca.crt
|
||||
'''
|
||||
return bool(get_public_key(private_key) == get_public_key(public_key))
|
||||
return bool(get_public_key(private_key, passphrase)
|
||||
== get_public_key(public_key))
|
||||
|
||||
|
||||
def verify_signature(certificate, signing_pub_key=None):
|
||||
def verify_signature(certificate, signing_pub_key=None,
|
||||
signing_pub_key_passphrase=None):
|
||||
'''
|
||||
Verify that ``certificate`` has been signed by ``signing_pub_key``
|
||||
|
||||
|
@ -1611,6 +1674,9 @@ def verify_signature(certificate, signing_pub_key=None):
|
|||
The public key to verify, can be a string or path to a PEM formatted
|
||||
certificate, csr, or private key.
|
||||
|
||||
signing_pub_key_passphrase:
|
||||
Passphrase to the signing_pub_key if it is an encrypted private key.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
@ -1621,7 +1687,8 @@ def verify_signature(certificate, signing_pub_key=None):
|
|||
cert = _get_certificate_obj(certificate)
|
||||
|
||||
if signing_pub_key:
|
||||
signing_pub_key = get_public_key(signing_pub_key, asObj=True)
|
||||
signing_pub_key = get_public_key(signing_pub_key,
|
||||
passphrase=signing_pub_key_passphrase, asObj=True)
|
||||
|
||||
return bool(cert.verify(pkey=signing_pub_key) == 1)
|
||||
|
||||
|
|
|
@ -63,13 +63,6 @@ the mine where it can be easily retrieved by other minions.
|
|||
/etc/pki/issued_certs:
|
||||
file.directory: []
|
||||
|
||||
/etc/pki/ca.key:
|
||||
x509.private_key_managed:
|
||||
- bits: 4096
|
||||
- backup: True
|
||||
- require:
|
||||
- file: /etc/pki
|
||||
|
||||
/etc/pki/ca.crt:
|
||||
x509.certificate_managed:
|
||||
- signing_private_key: /etc/pki/ca.key
|
||||
|
@ -84,8 +77,12 @@ the mine where it can be easily retrieved by other minions.
|
|||
- days_valid: 3650
|
||||
- days_remaining: 0
|
||||
- backup: True
|
||||
- managed_private_key:
|
||||
name: /etc/pki/ca.key
|
||||
bits: 4096
|
||||
backup: True
|
||||
- require:
|
||||
- x509: /etc/pki/ca.key
|
||||
- file: /etc/pki
|
||||
|
||||
mine.send:
|
||||
module.run:
|
||||
|
@ -142,10 +139,6 @@ This state creates a private key then requests a certificate signed by ca accord
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
/etc/pki/www.key:
|
||||
x509.private_key_managed:
|
||||
- bits: 4096
|
||||
|
||||
/etc/pki/www.crt:
|
||||
x509.certificate_managed:
|
||||
- ca_server: ca
|
||||
|
@ -154,6 +147,10 @@ This state creates a private key then requests a certificate signed by ca accord
|
|||
- CN: www.example.com
|
||||
- days_remaining: 30
|
||||
- backup: True
|
||||
- managed_private_key:
|
||||
name: /etc/pki/www.key
|
||||
bits: 4096
|
||||
backup: True
|
||||
|
||||
'''
|
||||
|
||||
|
@ -190,7 +187,8 @@ def _revoked_to_list(revs):
|
|||
list_ = []
|
||||
|
||||
for rev in revs:
|
||||
for rev_name, props in six.iteritems(rev): # pylint: disable=unused-variable
|
||||
for rev_name, props in six.iteritems(
|
||||
rev): # pylint: disable=unused-variable
|
||||
dict_ = {}
|
||||
for prop in props:
|
||||
for propname, val in six.iteritems(prop):
|
||||
|
@ -202,11 +200,46 @@ def _revoked_to_list(revs):
|
|||
return list_
|
||||
|
||||
|
||||
def _get_file_args(name, **kwargs):
|
||||
valid_file_args = ['user',
|
||||
'group',
|
||||
'mode',
|
||||
'makedirs',
|
||||
'dir_mode',
|
||||
'backup',
|
||||
'create',
|
||||
'follow_symlinks',
|
||||
'check_cmd']
|
||||
file_args = {}
|
||||
extra_args = {}
|
||||
for k, v in kwargs.items():
|
||||
if k in valid_file_args:
|
||||
file_args[k] = v
|
||||
else:
|
||||
extra_args[k] = v
|
||||
file_args['name'] = name
|
||||
return file_args, extra_args
|
||||
|
||||
|
||||
def _check_private_key(name, bits=2048, passphrase=None, new=False):
|
||||
current_bits = 0
|
||||
if os.path.isfile(name):
|
||||
try:
|
||||
current_bits = __salt__['x509.get_private_key_size'](
|
||||
private_key=name, passphrase=passphrase)
|
||||
except salt.exceptions.SaltInvocationError:
|
||||
pass
|
||||
|
||||
return current_bits == bits and not new
|
||||
|
||||
|
||||
def private_key_managed(name,
|
||||
bits=2048,
|
||||
passphrase=None,
|
||||
cipher='aes_128_cbc',
|
||||
new=False,
|
||||
backup=False,
|
||||
verbose=True,):
|
||||
verbose=True,
|
||||
**kwargs):
|
||||
'''
|
||||
Manage a private key's existence.
|
||||
|
||||
|
@ -216,14 +249,15 @@ def private_key_managed(name,
|
|||
bits:
|
||||
Key length in bits. Default 2048.
|
||||
|
||||
passphrase:
|
||||
Passphrase for encrypting the private key.
|
||||
|
||||
cipher:
|
||||
Cipher for encrypting the private key.
|
||||
|
||||
new:
|
||||
Always create a new key. Defaults to False.
|
||||
Combining new with :mod:`prereq <salt.states.requsities.preqreq>` can allow key rotation
|
||||
whenever a new certificiate is generated.
|
||||
|
||||
backup:
|
||||
When replacing an existing file, backup the old file on the minion.
|
||||
Default is False.
|
||||
Combining new with :mod:`prereq <salt.states.requsities.preqreq>`, or when used as part of a `managed_private_key` can allow key rotation whenever a new certificiate is generated.
|
||||
|
||||
verbose:
|
||||
Provide visual feedback on stdout, dots while key is generated.
|
||||
|
@ -231,6 +265,9 @@ def private_key_managed(name,
|
|||
|
||||
.. versionadded:: 2016.11.0
|
||||
|
||||
kwargs:
|
||||
Any kwargs supported by file.managed are supported.
|
||||
|
||||
Example:
|
||||
|
||||
The jinja templating in this example ensures a private key is generated if the file doesn't exist
|
||||
|
@ -247,45 +284,24 @@ def private_key_managed(name,
|
|||
- x509: /etc/pki/www.crt
|
||||
{%- endif %}
|
||||
'''
|
||||
ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
|
||||
|
||||
current_bits = 0
|
||||
if os.path.isfile(name):
|
||||
try:
|
||||
current_bits = __salt__['x509.get_private_key_size'](private_key=name)
|
||||
current = "{0} bit private key".format(current_bits)
|
||||
except salt.exceptions.SaltInvocationError:
|
||||
current = '{0} is not a valid Private Key.'.format(name)
|
||||
file_args, kwargs = _get_file_args(name, **kwargs)
|
||||
new_key = False
|
||||
if _check_private_key(name, bits, passphrase, new):
|
||||
file_args['contents'] = __salt__['x509.get_pem_entry'](
|
||||
name, pem_type='RSA PRIVATE KEY')
|
||||
else:
|
||||
current = '{0} does not exist.'.format(name)
|
||||
new_key = True
|
||||
file_args['contents'] = __salt__['x509.create_private_key'](
|
||||
text=True, bits=bits, passphrase=passphrase, cipher=cipher, verbose=verbose)
|
||||
|
||||
if current_bits == bits and not new:
|
||||
ret['result'] = True
|
||||
ret['comment'] = 'The Private key is already in the correct state'
|
||||
return ret
|
||||
|
||||
ret['changes'] = {
|
||||
'old': current,
|
||||
'new': "{0} bit private key".format(bits)}
|
||||
|
||||
if __opts__['test'] is True:
|
||||
ret['result'] = None
|
||||
ret['comment'] = 'The Private Key "{0}" will be updated.'.format(name)
|
||||
return ret
|
||||
|
||||
if os.path.isfile(name) and backup:
|
||||
bkroot = os.path.join(__opts__['cachedir'], 'file_backup')
|
||||
salt.utils.backup_minion(name, bkroot)
|
||||
|
||||
ret['comment'] = __salt__['x509.create_private_key'](
|
||||
path=name, bits=bits, verbose=verbose)
|
||||
ret['result'] = True
|
||||
ret = __states__['file.managed'](**file_args)
|
||||
if ret['changes'] and new_key:
|
||||
ret['changes'] = 'New private key generated'
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def csr_managed(name,
|
||||
backup=False,
|
||||
**kwargs):
|
||||
'''
|
||||
Manage a Certificate Signing Request
|
||||
|
@ -297,6 +313,9 @@ def csr_managed(name,
|
|||
The properties to be added to the certificate request, including items like subject, extensions
|
||||
and public key. See above for valid properties.
|
||||
|
||||
kwargs:
|
||||
Any arguments supported by :state:`file.managed <salt.states.file.managed>` are supported.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
@ -310,45 +329,23 @@ def csr_managed(name,
|
|||
- L: Salt Lake City
|
||||
- keyUsage: 'critical dataEncipherment'
|
||||
'''
|
||||
ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
|
||||
old = __salt__['x509.read_csr'](name)
|
||||
file_args, kwargs = _get_file_args(name, **kwargs)
|
||||
file_args['contents'] = __salt__['x509.create_csr'](text=True, **kwargs)
|
||||
|
||||
if os.path.isfile(name):
|
||||
try:
|
||||
current = __salt__['x509.read_csr'](csr=name)
|
||||
except salt.exceptions.SaltInvocationError:
|
||||
current = '{0} is not a valid CSR.'.format(name)
|
||||
else:
|
||||
current = '{0} does not exist.'.format(name)
|
||||
|
||||
new_csr = __salt__['x509.create_csr'](text=True, **kwargs)
|
||||
new = __salt__['x509.read_csr'](csr=new_csr)
|
||||
|
||||
if current == new:
|
||||
ret['result'] = True
|
||||
ret['comment'] = 'The CSR is already in the correct state'
|
||||
return ret
|
||||
|
||||
ret['changes'] = {
|
||||
'old': current,
|
||||
'new': new, }
|
||||
|
||||
if __opts__['test'] is True:
|
||||
ret['result'] = None
|
||||
ret['comment'] = 'The CSR {0} will be updated.'.format(name)
|
||||
|
||||
if os.path.isfile(name) and backup:
|
||||
bkroot = os.path.join(__opts__['cachedir'], 'file_backup')
|
||||
salt.utils.backup_minion(name, bkroot)
|
||||
|
||||
ret['comment'] = __salt__['x509.write_pem'](text=new_csr, path=name, pem_type="CERTIFICATE REQUEST")
|
||||
ret['result'] = True
|
||||
ret = __states__['file.managed'](**file_args)
|
||||
if ret['changes']:
|
||||
new = __salt__['x509.read_csr'](file_args['contents'])
|
||||
if old != new:
|
||||
ret['changes'] = {"Old": old, "New": new}
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def certificate_managed(name,
|
||||
days_remaining=90,
|
||||
backup=False,
|
||||
managed_private_key=None,
|
||||
append_certs=None,
|
||||
**kwargs):
|
||||
'''
|
||||
Manage a Certificate
|
||||
|
@ -360,12 +357,14 @@ def certificate_managed(name,
|
|||
The minimum number of days remaining when the certificate should be recreated. Default is 90. A
|
||||
value of 0 disables automatic renewal.
|
||||
|
||||
backup:
|
||||
When replacing an existing file, backup the old file on the minion. Default is False.
|
||||
managed_private_key:
|
||||
Manages the private key corresponding to the certificate. All of the arguments supported by :state:`x509.private_key_managed <salt.states.x509.private_key_managed>` are supported. If `name` is not speicified or is the same as the name of the certificate, the private key and certificate will be written together in the same file.
|
||||
|
||||
append_certs:
|
||||
A list of certificates to be appended to the managed file.
|
||||
|
||||
kwargs:
|
||||
Any arguments supported by :mod:`x509.create_certificate <salt.modules.x509.create_certificate>`
|
||||
are supported.
|
||||
Any arguments supported by :mod:`x509.create_certificate <salt.modules.x509.create_certificate>` or :state:`file.managed <salt.states.file.managed>` are supported.
|
||||
|
||||
Examples:
|
||||
|
||||
|
@ -400,11 +399,42 @@ def certificate_managed(name,
|
|||
- backup: True
|
||||
|
||||
'''
|
||||
ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
|
||||
|
||||
if 'path' in kwargs:
|
||||
name = kwargs.pop('path')
|
||||
|
||||
file_args, kwargs = _get_file_args(name, **kwargs)
|
||||
|
||||
rotate_private_key = False
|
||||
new_private_key = False
|
||||
if managed_private_key:
|
||||
private_key_args = {
|
||||
'name': name,
|
||||
'new': False,
|
||||
'bits': 2048,
|
||||
'passphrase': None,
|
||||
'cipher': 'aes_128_cbc',
|
||||
'verbose': True
|
||||
}
|
||||
private_key_args.update(managed_private_key)
|
||||
kwargs['public_key_passphrase'] = private_key_args['passphrase']
|
||||
|
||||
if private_key_args['new']:
|
||||
rotate_private_key = True
|
||||
private_key_args['new'] = False
|
||||
|
||||
if _check_private_key(private_key_args['name'],
|
||||
private_key_args['bits'],
|
||||
private_key_args['passphrase'],
|
||||
private_key_args['new']):
|
||||
private_key = __salt__['x509.get_pem_entry'](
|
||||
private_key_args['name'], pem_type='RSA PRIVATE KEY')
|
||||
else:
|
||||
new_private_key = True
|
||||
private_key = __salt__['x509.create_private_key'](text=True, bits=private_key_args['bits'], passphrase=private_key_args[
|
||||
'passphrase'], cipher=private_key_args['cipher'], verbose=private_key_args['verbose'])
|
||||
|
||||
kwargs['public_key'] = private_key
|
||||
|
||||
current_days_remaining = 0
|
||||
current_comp = {}
|
||||
|
||||
|
@ -418,7 +448,7 @@ def certificate_managed(name,
|
|||
try:
|
||||
current_comp['X509v3 Extensions']['authorityKeyIdentifier'] = (
|
||||
re.sub(r'serial:([0-9A-F]{2}:)*[0-9A-F]{2}', 'serial:--',
|
||||
current_comp['X509v3 Extensions']['authorityKeyIdentifier']))
|
||||
current_comp['X509v3 Extensions']['authorityKeyIdentifier']))
|
||||
except KeyError:
|
||||
pass
|
||||
current_comp.pop('Not Before')
|
||||
|
@ -427,8 +457,8 @@ def certificate_managed(name,
|
|||
current_comp.pop('SHA-256 Finger Print')
|
||||
current_notafter = current_comp.pop('Not After')
|
||||
current_days_remaining = (
|
||||
datetime.datetime.strptime(current_notafter, '%Y-%m-%d %H:%M:%S') -
|
||||
datetime.datetime.now()).days
|
||||
datetime.datetime.strptime(current_notafter, '%Y-%m-%d %H:%M:%S') -
|
||||
datetime.datetime.now()).days
|
||||
if days_remaining == 0:
|
||||
days_remaining = current_days_remaining - 1
|
||||
except salt.exceptions.SaltInvocationError:
|
||||
|
@ -437,7 +467,8 @@ def certificate_managed(name,
|
|||
current = '{0} does not exist.'.format(name)
|
||||
|
||||
if 'ca_server' in kwargs and 'signing_policy' not in kwargs:
|
||||
raise salt.exceptions.SaltInvocationError('signing_policy must be specified if ca_server is.')
|
||||
raise salt.exceptions.SaltInvocationError(
|
||||
'signing_policy must be specified if ca_server is.')
|
||||
|
||||
new = __salt__['x509.create_certificate'](testrun=True, **kwargs)
|
||||
|
||||
|
@ -450,7 +481,7 @@ def certificate_managed(name,
|
|||
try:
|
||||
new_comp['X509v3 Extensions']['authorityKeyIdentifier'] = (
|
||||
re.sub(r'serial:([0-9A-F]{2}:)*[0-9A-F]{2}', 'serial:--',
|
||||
new_comp['X509v3 Extensions']['authorityKeyIdentifier']))
|
||||
new_comp['X509v3 Extensions']['authorityKeyIdentifier']))
|
||||
except KeyError:
|
||||
pass
|
||||
new_comp.pop('Not Before')
|
||||
|
@ -462,28 +493,58 @@ def certificate_managed(name,
|
|||
else:
|
||||
new_comp = new
|
||||
|
||||
new_certificate = False
|
||||
if (current_comp == new_comp and
|
||||
current_days_remaining > days_remaining and
|
||||
__salt__['x509.verify_signature'](name, new_issuer_public_key)):
|
||||
ret['result'] = True
|
||||
ret['comment'] = 'The certificate is already in the correct state'
|
||||
return ret
|
||||
certificate = __salt__['x509.get_pem_entry'](
|
||||
name, pem_type='CERTIFICATE')
|
||||
else:
|
||||
if rotate_private_key and not new_private_key:
|
||||
new_private_key = True
|
||||
private_key = __salt__['x509.create_private_key'](
|
||||
text=True, bits=private_key_args['bits'], verbose=private_key_args['verbose'])
|
||||
kwargs['public_key'] = private_key
|
||||
new_certificate = True
|
||||
certificate = __salt__['x509.create_certificate'](text=True, **kwargs)
|
||||
|
||||
ret['changes'] = {
|
||||
'old': current,
|
||||
'new': new, }
|
||||
file_args['contents'] = ''
|
||||
private_ret = {}
|
||||
if managed_private_key:
|
||||
if private_key_args['name'] == name:
|
||||
file_args['contents'] = private_key
|
||||
else:
|
||||
private_file_args = copy.deepcopy(file_args)
|
||||
unique_private_file_args, _ = _get_file_args(**private_key_args)
|
||||
private_file_args.update(unique_private_file_args)
|
||||
private_file_args['contents'] = private_key
|
||||
private_ret = __states__['file.managed'](**private_file_args)
|
||||
if not private_ret['result']:
|
||||
return private_ret
|
||||
|
||||
if __opts__['test'] is True:
|
||||
ret['result'] = None
|
||||
ret['comment'] = 'The certificate {0} will be updated.'.format(name)
|
||||
return ret
|
||||
file_args['contents'] += certificate
|
||||
|
||||
if os.path.isfile(name) and backup:
|
||||
bkroot = os.path.join(__opts__['cachedir'], 'file_backup')
|
||||
salt.utils.backup_minion(name, bkroot)
|
||||
if not append_certs:
|
||||
append_certs = []
|
||||
for append_cert in append_certs:
|
||||
file_args[
|
||||
'contents'] += __salt__['x509.get_pem_entry'](append_cert, pem_type='CERTIFICATE')
|
||||
|
||||
ret['comment'] = __salt__['x509.create_certificate'](path=name, **kwargs)
|
||||
ret['result'] = True
|
||||
file_args['show_changes'] = False
|
||||
ret = __states__['file.managed'](**file_args)
|
||||
|
||||
if ret['changes']:
|
||||
ret['changes'] = {'Certificate': ret['changes']}
|
||||
else:
|
||||
ret['changes'] = {}
|
||||
if private_ret and private_ret['changes']:
|
||||
ret['changes']['Private Key'] = private_ret['changes']
|
||||
if new_private_key:
|
||||
ret['changes']['Private Key'] = 'New private key generated'
|
||||
if new_certificate:
|
||||
ret['changes']['Certificate'] = {
|
||||
'Old': current,
|
||||
'New': __salt__['x509.read_certificate'](certificate=certificate)}
|
||||
|
||||
return ret
|
||||
|
||||
|
@ -496,7 +557,7 @@ def crl_managed(name,
|
|||
digest="",
|
||||
days_remaining=30,
|
||||
include_expired=False,
|
||||
backup=False,):
|
||||
**kwargs):
|
||||
'''
|
||||
Manage a Certificate Revocation List
|
||||
|
||||
|
@ -530,8 +591,8 @@ def crl_managed(name,
|
|||
include_expired:
|
||||
Include expired certificates in the CRL. Default is ``False``.
|
||||
|
||||
backup:
|
||||
When replacing an existing file, backup the old file on the minion. Default is False.
|
||||
kwargs:
|
||||
Any arguments supported by :state:`file.managed <salt.states.file.managed>` are supported.
|
||||
|
||||
Example:
|
||||
|
||||
|
@ -552,8 +613,6 @@ def crl_managed(name,
|
|||
- revocation_date: 2015-02-25 00:00:00
|
||||
- reason: cessationOfOperation
|
||||
'''
|
||||
ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
|
||||
|
||||
if revoked is None:
|
||||
revoked = []
|
||||
|
||||
|
@ -569,8 +628,8 @@ def crl_managed(name,
|
|||
current_comp.pop('Last Update')
|
||||
current_notafter = current_comp.pop('Next Update')
|
||||
current_days_remaining = (
|
||||
datetime.datetime.strptime(current_notafter, '%Y-%m-%d %H:%M:%S') -
|
||||
datetime.datetime.now()).days
|
||||
datetime.datetime.strptime(current_notafter, '%Y-%m-%d %H:%M:%S') -
|
||||
datetime.datetime.now()).days
|
||||
if days_remaining == 0:
|
||||
days_remaining = current_days_remaining - 1
|
||||
except salt.exceptions.SaltInvocationError:
|
||||
|
@ -579,43 +638,35 @@ def crl_managed(name,
|
|||
current = '{0} does not exist.'.format(name)
|
||||
|
||||
new_crl = __salt__['x509.create_crl'](text=True, signing_private_key=signing_private_key,
|
||||
signing_cert=signing_cert, revoked=revoked, days_valid=days_valid, digest=digest, include_expired=include_expired)
|
||||
signing_cert=signing_cert, revoked=revoked, days_valid=days_valid, digest=digest, include_expired=include_expired)
|
||||
|
||||
new = __salt__['x509.read_crl'](crl=new_crl)
|
||||
new_comp = new.copy()
|
||||
new_comp.pop('Last Update')
|
||||
new_comp.pop('Next Update')
|
||||
|
||||
file_args, kwargs = _get_file_args(name, **kwargs)
|
||||
new_crl = False
|
||||
if (current_comp == new_comp and
|
||||
current_days_remaining > days_remaining and
|
||||
__salt__['x509.verify_crl'](name, signing_cert)):
|
||||
file_args['contents'] = __salt__[
|
||||
'x509.get_pem_entry'](name, pem_type='X509 CRL')
|
||||
else:
|
||||
new_crl = True
|
||||
file_args['contents'] = new_crl
|
||||
|
||||
ret['result'] = True
|
||||
ret['comment'] = 'The crl is already in the correct state'
|
||||
return ret
|
||||
|
||||
ret['changes'] = {
|
||||
'old': current,
|
||||
'new': new, }
|
||||
|
||||
if __opts__['test'] is True:
|
||||
ret['result'] = None
|
||||
ret['comment'] = 'The crl {0} will be updated.'.format(name)
|
||||
return ret
|
||||
|
||||
if os.path.isfile(name) and backup:
|
||||
bkroot = os.path.join(__opts__['cachedir'], 'file_backup')
|
||||
salt.utils.backup_minion(name, bkroot)
|
||||
|
||||
ret['comment'] = __salt__['x509.write_pem'](text=new_crl, path=name, pem_type='X509 CRL')
|
||||
ret['result'] = True
|
||||
|
||||
ret = __states__['file.managed'](**file_args)
|
||||
if new_crl:
|
||||
ret['changes'] = {'Old': current, 'New': __salt__[
|
||||
'x509.read_crl'](crl=new_crl)}
|
||||
return ret
|
||||
|
||||
|
||||
def pem_managed(name,
|
||||
text,
|
||||
backup=False):
|
||||
backup=False,
|
||||
**kwargs):
|
||||
'''
|
||||
Manage the contents of a PEM file directly with the content in text, ensuring correct formatting.
|
||||
|
||||
|
@ -625,37 +676,10 @@ def pem_managed(name,
|
|||
text:
|
||||
The PEM formatted text to write.
|
||||
|
||||
backup:
|
||||
When replacing an existing file, backup the old file on the minion. Default is False.
|
||||
kwargs:
|
||||
Any arguments supported by :state:`file.managed <salt.states.file.managed>` are supported.
|
||||
'''
|
||||
ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
|
||||
file_args, kwargs = _get_file_args(name, **kwargs)
|
||||
file_args['contents'] = __salt__['x509.get_pem_entry'](text=text)
|
||||
|
||||
new = __salt__['x509.get_pem_entry'](text=text)
|
||||
|
||||
try:
|
||||
with salt.utils.fopen(name) as fp_:
|
||||
current = fp_.read()
|
||||
except (OSError, IOError):
|
||||
current = '{0} does not exist or is unreadable'.format(name)
|
||||
|
||||
if new == current:
|
||||
ret['result'] = True
|
||||
ret['comment'] = 'The file is already in the correct state'
|
||||
return ret
|
||||
|
||||
ret['changes']['new'] = new
|
||||
ret['changes']['old'] = current
|
||||
|
||||
if __opts__['test'] is True:
|
||||
ret['result'] = None
|
||||
ret['comment'] = 'The file {0} will be updated.'.format(name)
|
||||
return ret
|
||||
|
||||
if os.path.isfile(name) and backup:
|
||||
bkroot = os.path.join(__opts__['cachedir'], 'file_backup')
|
||||
salt.utils.backup_minion(name, bkroot)
|
||||
|
||||
ret['comment'] = __salt__['x509.write_pem'](text=text, path=name)
|
||||
ret['result'] = True
|
||||
|
||||
return ret
|
||||
return __states__['file.managed'](**file_args)
|
||||
|
|
Loading…
Add table
Reference in a new issue