nacl updates

This commit is contained in:
s8weber 2017-07-24 10:11:32 -04:00
parent fa8d672f9a
commit 2b79ba2f32
3 changed files with 725 additions and 166 deletions

View file

@ -5,3 +5,4 @@ yappi>=0.8.2
--allow-unverified python-neutronclient>2.3.6 --allow-unverified python-neutronclient>2.3.6
python-gnupg python-gnupg
cherrypy>=3.2.2 cherrypy>=3.2.2
libnacl

View file

@ -11,112 +11,157 @@ regardless if they are encrypted or not.
When generating keys and encrypting passwords use --local when using salt-call for extra When generating keys and encrypting passwords use --local when using salt-call for extra
security. Also consider using just the salt runner nacl when encrypting pillar passwords. security. Also consider using just the salt runner nacl when encrypting pillar passwords.
:configuration: The following configuration defaults can be
define (pillar or config files) Avoid storing private keys in pillars! Ensure master does not have `pillar_opts=True`:
.. code-block:: python
# cat /etc/salt/master.d/nacl.conf
nacl.config:
# NOTE: `key` and `key_file` have been renamed to `sk`, `sk_file`
# also `box_type` default changed from secretbox to sealedbox.
box_type: sealedbox (default)
sk_file: /etc/salt/pki/master/nacl (default)
pk_file: /etc/salt/pki/master/nacl.pub (default)
sk: None
pk: None
Usage can override the config defaults:
.. code-block:: bash
salt-call nacl.enc sk_file=/etc/salt/pki/master/nacl pk_file=/etc/salt/pki/master/nacl.pub
The nacl lib uses 32byte keys, these keys are base64 encoded to make your life more simple. The nacl lib uses 32byte keys, these keys are base64 encoded to make your life more simple.
To generate your `key` or `keyfile` you can use: To generate your `sk_file` and `pk_file` use:
.. code-block:: bash .. code-block:: bash
salt-call --local nacl.keygen keyfile=/root/.nacl salt-call --local nacl.keygen sk_file=/etc/salt/pki/master/nacl
# or if you want to work without files.
salt-call --local nacl.keygen
local:
----------
pk:
/kfGX7PbWeu099702PBbKWLpG/9p06IQRswkdWHCDk0=
sk:
SVWut5SqNpuPeNzb1b9y6b2eXg2PLIog43GBzp48Sow=
Now with your key, you can encrypt some data: Now with your keypair, you can encrypt data:
You have two option, `sealedbox` or `secretbox`.
SecretBox is data encrypted using private key `pk`. Sealedbox is encrypted using public key `pk`.
Recommend using Sealedbox because the one way encryption permits developers to encrypt data for source control but not decrypt.
Sealedbox only has one key that is for both encryption and decryption.
.. code-block:: bash .. code-block:: bash
salt-call --local nacl.enc mypass keyfile=/root/.nacl salt-call --local nacl.enc asecretpass pk=/kfGX7PbWeu099702PBbKWLpG/9p06IQRswkdWHCDk0=
DRB7Q6/X5gGSRCTpZyxS6hXO5LnlJIIJ4ivbmUlbWj0llUA+uaVyvou3vJ4= tqXzeIJnTAM9Xf0mdLcpEdklMbfBGPj2oTKmlgrm3S1DTVVHNnh9h8mU1GKllGq/+cYsk6m5WhGdk58=
To decrypt the data: To decrypt the data:
.. code-block:: bash .. code-block:: bash
salt-call --local nacl.dec data='DRB7Q6/X5gGSRCTpZyxS6hXO5LnlJIIJ4ivbmUlbWj0llUA+uaVyvou3vJ4=' keyfile=/root/.nacl salt-call --local nacl.dec data='tqXzeIJnTAM9Xf0mdLcpEdklMbfBGPj2oTKmlgrm3S1DTVVHNnh9h8mU1GKllGq/+cYsk6m5WhGdk58=' \
mypass sk='SVWut5SqNpuPeNzb1b9y6b2eXg2PLIog43GBzp48Sow='
The following optional configurations can be defined in the When the keys are defined in the master config you can use them from the nacl runner
minion or master config. Avoid storing the config in pillars! without extra parameters:
.. code-block:: yaml .. code-block:: python
cat /etc/salt/master.d/nacl.conf # cat /etc/salt/master.d/nacl.conf
nacl.config: nacl.config:
key: 'cKEzd4kXsbeCE7/nLTIqXwnUiD1ulg4NoeeYcCFpd9k=' sk_file: /etc/salt/pki/master/nacl
keyfile: /root/.nacl pk: 'cTIqXwnUiD1ulg4kXsbeCE7/NoeKEzd4nLeYcCFpd9k='
When the key is defined in the master config you can use it from the nacl runner:
.. code-block:: bash .. code-block:: bash
salt-run nacl.enc 'myotherpass' salt-run nacl.enc 'asecretpass'
salt-run nacl.dec 'tqXzeIJnTAM9Xf0mdLcpEdklMbfBGPj2oTKmlgrm3S1DTVVHNnh9h8mU1GKllGq/+cYsk6m5WhGdk58='
Now you can create a pillar with protected data like: .. code-block:: yam
# a salt developers minion could have pillar data that includes a nacl public key
nacl.config:
pk: '/kfGX7PbWeu099702PBbKWLpG/9p06IQRswkdWHCDk0='
The developer can then use a less secure system to encrypt data.
.. code-block:: bash
salt-call --local nacl.enc apassword
Pillar files can include protected data that the salt master decrypts:
.. code-block:: jinja .. code-block:: jinja
pillarexample: pillarexample:
user: root user: root
password: {{ salt.nacl.dec('DRB7Q6/X5gGSRCTpZyxS6hXO5LnlJIIJ4ivbmUlbWj0llUA+uaVyvou3vJ4=') }} password1: {{salt.nacl.dec('DRB7Q6/X5gGSRCTpZyxS6hlbWj0llUA+uaVyvou3vJ4=')|json}}
cert_key: {{salt.nacl.dec_file('/srv/salt/certs/example.com/key.nacl')|json}}
cert_key2: {{salt.nacl.dec_file('salt:///certs/example.com/key.nacl')|json}}
Or do something interesting with grains like: Larger files like certificates can be encrypted with:
.. code-block:: jinja
salt-call nacl.enc minionname:dbrole
AL24Z2C5OlkReer3DuQTFdrNLchLuz3NGIhGjZkLtKRYry/b/CksWM8O9yskLwH2AGVLoEXI5jAa
salt minionname grains.setval role 'AL24Z2C5OlkReer3DuQTFdrNLchLuz3NGIhGjZkLtKRYry/b/CksWM8O9yskLwH2AGVLoEXI5jAa'
{%- set r = grains.get('role') %}
{%- set role = None %}
{%- if r and 'nacl.dec' in salt %}
{%- set r = salt['nacl.dec'](r,keyfile='/root/.nacl').split(':') %}
{%- if opts['id'] == r[0] %}
{%- set role = r[1] %}
{%- endif %}
{%- endif %}
base:
{%- if role %}
'{{ opts['id'] }}':
- {{ role }}
{%- endif %}
Multi-line text items like certificates require a bit of extra work. You have to strip the new lines
and replace them with '/n' characters. Certificates specifically require some leading white space when
calling nacl.enc so that the '--' in the first line (commonly -----BEGIN CERTIFICATE-----) doesn't get
interpreted as an argument to nacl.enc. For instance if you have a certificate file that lives in cert.crt:
.. code-block:: bash .. code-block:: bash
cert=$(cat cert.crt |awk '{printf "%s\\n",$0} END {print ""}'); salt-run nacl.enc " $cert" salt-call nacl.enc_file /tmp/cert.crt out=/tmp/cert.nacl
# or more advanced
cert=$(cat /tmp/cert.crt)
salt-call --out=newline_values_only nacl.enc_pub data="$cert" > /tmp/cert.nacl
Pillar data should look the same, even though the secret will be quite long. However, when calling In Pillars rended with jinja be sure to include `|json` so line breaks are encoded:
multiline encrypted secrets from pillar in a state, use the following format to avoid issues with /n
creating extra whitespace at the beginning of each line in the cert file:
.. code-block:: jinja .. code-block:: jinja
secret.txt: cert: "{{salt.nacl.dec('S2uogToXkgENz9...085KYt')|json}}"
file.managed:
- template: jinja In States rendered with jinja it is also good pratice to include `|json`:
- user: user
- group: group .. code-block:: jinja
- mode: 700
- contents: "{{- salt['pillar.get']('secret') }}" {{sls}} private key:
file.managed:
- name: /etc/ssl/private/cert.key
- mode: 700
- contents: "{{pillar['pillarexample']['cert_key']|json}}"
Optional small program to encrypt data without needing salt modules.
.. code-block:: python
#!/bin/python3
import sys, base64, libnacl.sealed
pk = base64.b64decode('YOURPUBKEY')
b = libnacl.sealed.SealedBox(pk)
data = sys.stdin.buffer.read()
print(base64.b64encode(b.encrypt(data)).decode())
.. code-block:: bash
echo 'apassword' | nacl_enc.py
The '{{-' will tell jinja to strip the whitespace from the beginning of each of the new lines.
''' '''
from __future__ import absolute_import from __future__ import absolute_import
import base64 import base64
import os import os
import salt.utils.files import salt.utils
import salt.syspaths import salt.syspaths
REQ_ERROR = None REQ_ERROR = None
try: try:
import libnacl.secret import libnacl.secret
import libnacl.sealed
except (ImportError, OSError) as e: except (ImportError, OSError) as e:
REQ_ERROR = 'libnacl import error, perhaps missing python libnacl package' REQ_ERROR = 'libnacl import error, perhaps missing python libnacl package or should update.'
__virtualname__ = 'nacl' __virtualname__ = 'nacl'
@ -130,91 +175,284 @@ def _get_config(**kwargs):
Return configuration Return configuration
''' '''
config = { config = {
'key': None, 'box_type': 'sealedbox',
'keyfile': None, 'sk': None,
'sk_file': '/etc/salt/pki/master/nacl',
'pk': None,
'pk_file': '/etc/salt/pki/master/nacl.pub',
} }
config_key = '{0}.config'.format(__virtualname__) config_key = '{0}.config'.format(__virtualname__)
config.update(__salt__['config.get'](config_key, {})) try:
for k in set(config) & set(kwargs): config.update(__salt__['config.get'](config_key, {}))
except (NameError, KeyError) as e:
# likly using salt-run so fallback to __opts__
config.update(__opts__.get(config_key, {}))
# pylint: disable=C0201
for k in set(config.keys()) & set(kwargs.keys()):
config[k] = kwargs[k] config[k] = kwargs[k]
return config return config
def _get_key(rstrip_newline=True, **kwargs): def _get_sk(**kwargs):
''' '''
Return key Return sk
''' '''
config = _get_config(**kwargs) config = _get_config(**kwargs)
key = config['key'] key = config['sk']
keyfile = config['keyfile'] sk_file = config['sk_file']
if not key and keyfile: if not key and sk_file:
if not os.path.isfile(keyfile): with salt.utils.fopen(sk_file, 'rb') as keyf:
raise Exception('file not found: {0}'.format(keyfile)) key = str(keyf.read()).rstrip('\n')
with salt.utils.files.fopen(keyfile, 'rb') as keyf:
key = keyf.read()
if key is None: if key is None:
raise Exception('no key found') raise Exception('no key or sk_file found')
key = str(key) return base64.b64decode(key)
if rstrip_newline:
key = key.rstrip('\n')
return key
def keygen(keyfile=None): def _get_pk(**kwargs):
''' '''
Use libnacl to generate a private key Return pk
'''
config = _get_config(**kwargs)
pubkey = config['pk']
pk_file = config['pk_file']
if not pubkey and pk_file:
with salt.utils.fopen(pk_file, 'rb') as keyf:
pubkey = str(keyf.read()).rstrip('\n')
if pubkey is None:
raise Exception('no pubkey or pk_file found')
pubkey = str(pubkey)
return base64.b64decode(pubkey)
def keygen(sk_file=None, pk_file=None):
'''
Use libnacl to generate a keypair.
If no `sk_file` is defined return a keypair.
If only the `sk_file` is defined `pk_file` will use the same name with a postfix `.pub`.
When the `sk_file` is already existing, but `pk_file` is not. The `pk_file` will be generated
using the `sk_file`.
CLI Examples: CLI Examples:
.. code-block:: bash .. code-block:: bash
salt-call nacl.keygen
salt-call nacl.keygen sk_file=/etc/salt/pki/master/nacl
salt-call nacl.keygen sk_file=/etc/salt/pki/master/nacl pk_file=/etc/salt/pki/master/nacl.pub
salt-call --local nacl.keygen salt-call --local nacl.keygen
salt-call --local nacl.keygen keyfile=/root/.nacl
salt-call --local --out=newline_values_only nacl.keygen > /root/.nacl
''' '''
b = libnacl.secret.SecretBox() if sk_file is None:
key = b.sk kp = libnacl.public.SecretKey()
key = base64.b64encode(key) return {'sk': base64.b64encode(kp.sk), 'pk': base64.b64encode(kp.pk)}
if keyfile:
if os.path.isfile(keyfile): if pk_file is None:
raise Exception('file already found: {0}'.format(keyfile)) pk_file = '{0}.pub'.format(sk_file)
with salt.utils.files.fopen(keyfile, 'w') as keyf:
keyf.write(key) if sk_file and pk_file is None:
return 'saved: {0}'.format(keyfile) if not os.path.isfile(sk_file):
return key kp = libnacl.public.SecretKey()
with salt.utils.fopen(sk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.sk))
if not salt.utils.is_windows():
os.chmod(sk_file, 1536) # 0600
return 'saved sk_file: {0}'.format(sk_file)
else:
raise Exception('sk_file:{0} already exist.'.format(sk_file))
if sk_file is None and pk_file:
raise Exception('sk_file: Must be set inorder to generate a public key.')
if os.path.isfile(sk_file) and os.path.isfile(pk_file):
raise Exception('sk_file:{0} and pk_file:{1} already exist.'.format(sk_file, pk_file))
if os.path.isfile(sk_file) and not os.path.isfile(pk_file):
# generate pk using the sk
with salt.utils.fopen(sk_file, 'rb') as keyf:
sk = str(keyf.read()).rstrip('\n')
sk = base64.b64decode(sk)
kp = libnacl.public.SecretKey(sk)
with salt.utils.fopen(pk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.pk))
return 'saved pk_file: {0}'.format(pk_file)
kp = libnacl.public.SecretKey()
with salt.utils.fopen(sk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.sk))
if not salt.utils.is_windows():
os.chmod(sk_file, 1536) # 0600
with salt.utils.fopen(pk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.pk))
return 'saved sk_file:{0} pk_file: {1}'.format(sk_file, pk_file)
def enc(data, **kwargs): def enc(data, **kwargs):
''' '''
Takes a key generated from `nacl.keygen` and encrypt some data. Alias to `{box_type}_encrypt`
box_type: secretbox, sealedbox(default)
'''
box_type = _get_config(**kwargs)['box_type']
if box_type == 'sealedbox':
return sealedbox_encrypt(data, **kwargs)
if box_type == 'secretbox':
return secretbox_encrypt(data, **kwargs)
return sealedbox_encrypt(data, **kwargs)
def enc_file(name, out=None, **kwargs):
'''
This is a helper function to encrypt a file and return its contents.
You can provide an optional output file using `out`
`name` can be a local file or when not using `salt-run` can be a url like `salt://`, `https://` etc.
CLI Examples: CLI Examples:
.. code-block:: bash .. code-block:: bash
salt-call --local nacl.enc datatoenc salt-run nacl.enc_file name=/tmp/id_rsa
salt-call --local nacl.enc datatoenc keyfile=/root/.nacl salt-call nacl.enc_file name=salt://crt/mycert out=/tmp/cert
salt-call --local nacl.enc datatoenc key='cKEzd4kXsbeCE7/nLTIqXwnUiD1ulg4NoeeYcCFpd9k=' salt-run nacl.enc_file name=/tmp/id_rsa box_type=secretbox \
sk_file=/etc/salt/pki/master/nacl.pub
''' '''
key = _get_key(**kwargs) try:
sk = base64.b64decode(key) data = __salt__['cp.get_file_str'](name)
b = libnacl.secret.SecretBox(sk) except Exception as e:
return base64.b64encode(b.encrypt(data)) # likly using salt-run so fallback to local filesystem
with salt.utils.fopen(name, 'rb') as f:
data = f.read()
d = enc(data, **kwargs)
if out:
if os.path.isfile(out):
raise Exception('file:{0} already exist.'.format(out))
with salt.utils.fopen(out, 'wb') as f:
f.write(d)
return 'Wrote: {0}'.format(out)
return d
def dec(data, **kwargs): def dec(data, **kwargs):
''' '''
Takes a key generated from `nacl.keygen` and decrypt some data. Alias to `{box_type}_decrypt`
box_type: secretbox, sealedbox(default)
'''
box_type = _get_config(**kwargs)['box_type']
if box_type == 'sealedbox':
return sealedbox_decrypt(data, **kwargs)
if box_type == 'secretbox':
return secretbox_decrypt(data, **kwargs)
return sealedbox_decrypt(data, **kwargs)
def dec_file(name, out=None, **kwargs):
'''
This is a helper function to decrypt a file and return its contents.
You can provide an optional output file using `out`
`name` can be a local file or when not using `salt-run` can be a url like `salt://`, `https://` etc.
CLI Examples: CLI Examples:
.. code-block:: bash .. code-block:: bash
salt-call --local nacl.dec pEXHQM6cuaF7A= salt-run nacl.dec_file name=/tmp/id_rsa.nacl
salt-call --local nacl.dec data='pEXHQM6cuaF7A=' keyfile=/root/.nacl salt-call nacl.dec_file name=salt://crt/mycert.nacl out=/tmp/id_rsa
salt-call --local nacl.dec data='pEXHQM6cuaF7A=' key='cKEzd4kXsbeCE7/nLTIqXwnUiD1ulg4NoeeYcCFpd9k=' salt-run nacl.dec_file name=/tmp/id_rsa.nacl box_type=secretbox \
sk_file=/etc/salt/pki/master/nacl.pub
''' '''
key = _get_key(**kwargs) try:
sk = base64.b64decode(key) data = __salt__['cp.get_file_str'](name)
b = libnacl.secret.SecretBox(key=sk) except Exception as e:
# likly using salt-run so fallback to local filesystem
with salt.utils.fopen(name, 'rb') as f:
data = f.read()
d = dec(data, **kwargs)
if out:
if os.path.isfile(out):
raise Exception('file:{0} already exist.'.format(out))
with salt.utils.fopen(out, 'wb') as f:
f.write(d)
return 'Wrote: {0}'.format(out)
return d
def sealedbox_encrypt(data, **kwargs):
'''
Encrypt data using a public key generated from `nacl.keygen`.
The encryptd data can be decrypted using `nacl.sealedbox_decrypt` only with the secret key.
CLI Examples:
.. code-block:: bash
salt-run nacl.sealedbox_encrypt datatoenc
salt-call --local nacl.sealedbox_encrypt datatoenc pk_file=/etc/salt/pki/master/nacl.pub
salt-call --local nacl.sealedbox_encrypt datatoenc pk='vrwQF7cNiNAVQVAiS3bvcbJUnF0cN6fU9YTZD9mBfzQ='
'''
pk = _get_pk(**kwargs)
b = libnacl.sealed.SealedBox(pk)
return base64.b64encode(b.encrypt(data))
def sealedbox_decrypt(data, **kwargs):
'''
Decrypt data using a secret key that was encrypted using a public key with `nacl.sealedbox_encrypt`.
CLI Examples:
.. code-block:: bash
salt-call nacl.sealedbox_decrypt pEXHQM6cuaF7A=
salt-call --local nacl.sealedbox_decrypt data='pEXHQM6cuaF7A=' sk_file=/etc/salt/pki/master/nacl
salt-call --local nacl.sealedbox_decrypt data='pEXHQM6cuaF7A=' sk='YmFkcGFzcwo='
'''
if data is None:
return None
sk = _get_sk(**kwargs)
keypair = libnacl.public.SecretKey(sk)
b = libnacl.sealed.SealedBox(keypair)
return b.decrypt(base64.b64decode(data))
def secretbox_encrypt(data, **kwargs):
'''
Encrypt data using a secret key generated from `nacl.keygen`.
The same secret key can be used to decrypt the data using `nacl.secretbox_decrypt`.
CLI Examples:
.. code-block:: bash
salt-run nacl.secretbox_encrypt datatoenc
salt-call --local nacl.secretbox_encrypt datatoenc sk_file=/etc/salt/pki/master/nacl
salt-call --local nacl.secretbox_encrypt datatoenc sk='YmFkcGFzcwo='
'''
sk = _get_sk(**kwargs)
b = libnacl.secret.SecretBox(sk)
return base64.b64encode(b.encrypt(data))
def secretbox_decrypt(data, **kwargs):
'''
Decrypt data that was encrypted using `nacl.secretbox_encrypt` using the secret key
that was generated from `nacl.keygen`.
CLI Examples:
.. code-block:: bash
salt-call nacl.secretbox_decrypt pEXHQM6cuaF7A=
salt-call --local nacl.secretbox_decrypt data='pEXHQM6cuaF7A=' sk_file=/etc/salt/pki/master/nacl
salt-call --local nacl.secretbox_decrypt data='pEXHQM6cuaF7A=' sk='YmFkcGFzcwo='
'''
if data is None:
return None
key = _get_sk(**kwargs)
b = libnacl.secret.SecretBox(key=key)
return b.decrypt(base64.b64decode(data)) return b.decrypt(base64.b64decode(data))

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
This runner helps create encrypted passwords that can be included in pillars. This module helps include encrypted passwords in pillars, grains and salt state files.
:depends: libnacl, https://github.com/saltstack/libnacl :depends: libnacl, https://github.com/saltstack/libnacl
@ -8,35 +8,160 @@ This is often useful if you wish to store your pillars in source control or
share your pillar data with others that you trust. I don't advise making your pillars public share your pillar data with others that you trust. I don't advise making your pillars public
regardless if they are encrypted or not. regardless if they are encrypted or not.
The following configurations can be defined in the master config When generating keys and encrypting passwords use --local when using salt-call for extra
so your users can create encrypted passwords using the runner nacl: security. Also consider using just the salt runner nacl when encrypting pillar passwords.
:configuration: The following configuration defaults can be
define (pillar or config files) Avoid storing private keys in pillars! Ensure master does not have `pillar_opts=True`:
.. code-block:: python
# cat /etc/salt/master.d/nacl.conf
nacl.config:
# NOTE: `key` and `key_file` have been renamed to `sk`, `sk_file`
# also `box_type` default changed from secretbox to sealedbox.
box_type: sealedbox (default)
sk_file: /etc/salt/pki/master/nacl (default)
pk_file: /etc/salt/pki/master/nacl.pub (default)
sk: None
pk: None
Usage can override the config defaults:
.. code-block:: bash
salt-call nacl.enc sk_file=/etc/salt/pki/master/nacl pk_file=/etc/salt/pki/master/nacl.pub
The nacl lib uses 32byte keys, these keys are base64 encoded to make your life more simple.
To generate your `sk_file` and `pk_file` use:
.. code-block:: bash .. code-block:: bash
cat /etc/salt/master.d/nacl.conf salt-call --local nacl.keygen sk_file=/etc/salt/pki/master/nacl
# or if you want to work without files.
salt-call --local nacl.keygen
local:
----------
pk:
/kfGX7PbWeu099702PBbKWLpG/9p06IQRswkdWHCDk0=
sk:
SVWut5SqNpuPeNzb1b9y6b2eXg2PLIog43GBzp48Sow=
Now with your keypair, you can encrypt data:
You have two option, `sealedbox` or `secretbox`.
SecretBox is data encrypted using private key `pk`. Sealedbox is encrypted using public key `pk`.
Recommend using Sealedbox because the one way encryption permits developers to encrypt data for source control but not decrypt.
Sealedbox only has one key that is for both encryption and decryption.
.. code-block:: bash
salt-call --local nacl.enc asecretpass pk=/kfGX7PbWeu099702PBbKWLpG/9p06IQRswkdWHCDk0=
tqXzeIJnTAM9Xf0mdLcpEdklMbfBGPj2oTKmlgrm3S1DTVVHNnh9h8mU1GKllGq/+cYsk6m5WhGdk58=
To decrypt the data:
.. code-block:: bash
salt-call --local nacl.dec data='tqXzeIJnTAM9Xf0mdLcpEdklMbfBGPj2oTKmlgrm3S1DTVVHNnh9h8mU1GKllGq/+cYsk6m5WhGdk58=' \
sk='SVWut5SqNpuPeNzb1b9y6b2eXg2PLIog43GBzp48Sow='
When the keys are defined in the master config you can use them from the nacl runner
without extra parameters:
.. code-block:: python
# cat /etc/salt/master.d/nacl.conf
nacl.config: nacl.config:
key: 'cKEzd4kXsbeCE7/nLTIqXwnUiD1ulg4NoeeYcCFpd9k=' sk_file: /etc/salt/pki/master/nacl
keyfile: /root/.nacl pk: 'cTIqXwnUiD1ulg4kXsbeCE7/NoeKEzd4nLeYcCFpd9k='
Now with the config in the master you can use the runner nacl like:
.. code-block:: bash .. code-block:: bash
salt-run nacl.enc 'data' salt-run nacl.enc 'asecretpass'
salt-run nacl.dec 'tqXzeIJnTAM9Xf0mdLcpEdklMbfBGPj2oTKmlgrm3S1DTVVHNnh9h8mU1GKllGq/+cYsk6m5WhGdk58='
.. code-block:: yam
# a salt developers minion could have pillar data that includes a nacl public key
nacl.config:
pk: '/kfGX7PbWeu099702PBbKWLpG/9p06IQRswkdWHCDk0='
The developer can then use a less secure system to encrypt data.
.. code-block:: bash
salt-call --local nacl.enc apassword
Pillar files can include protected data that the salt master decrypts:
.. code-block:: jinja
pillarexample:
user: root
password1: {{salt.nacl.dec('DRB7Q6/X5gGSRCTpZyxS6hlbWj0llUA+uaVyvou3vJ4=')|json}}
cert_key: {{salt.nacl.dec_file('/srv/salt/certs/example.com/key.nacl')|json}}
cert_key2: {{salt.nacl.dec_file('salt:///certs/example.com/key.nacl')|json}}
Larger files like certificates can be encrypted with:
.. code-block:: bash
salt-call nacl.enc_file /tmp/cert.crt out=/tmp/cert.nacl
# or more advanced
cert=$(cat /tmp/cert.crt)
salt-call --out=newline_values_only nacl.enc_pub data="$cert" > /tmp/cert.nacl
In Pillars rended with jinja be sure to include `|json` so line breaks are encoded:
.. code-block:: jinja
cert: "{{salt.nacl.dec('S2uogToXkgENz9...085KYt')|json}}"
In States rendered with jinja it is also good pratice to include `|json`:
.. code-block:: jinja
{{sls}} private key:
file.managed:
- name: /etc/ssl/private/cert.key
- mode: 700
- contents: "{{pillar['pillarexample']['cert_key']|json}}"
Optional small program to encrypt data without needing salt modules.
.. code-block:: python
#!/bin/python3
import sys, base64, libnacl.sealed
pk = base64.b64decode('YOURPUBKEY')
b = libnacl.sealed.SealedBox(pk)
data = sys.stdin.buffer.read()
print(base64.b64encode(b.encrypt(data)).decode())
.. code-block:: bash
echo 'apassword' | nacl_enc.py
''' '''
from __future__ import absolute_import from __future__ import absolute_import
import base64 import base64
import os import os
import salt.utils.files import salt.utils
import salt.syspaths import salt.syspaths
REQ_ERROR = None REQ_ERROR = None
try: try:
import libnacl.secret import libnacl.secret
except ImportError as e: import libnacl.sealed
REQ_ERROR = 'libnacl import error, perhaps missing python libnacl package' except (ImportError, OSError) as e:
REQ_ERROR = 'libnacl import error, perhaps missing python libnacl package or should update.'
__virtualname__ = 'nacl' __virtualname__ = 'nacl'
@ -50,91 +175,286 @@ def _get_config(**kwargs):
Return configuration Return configuration
''' '''
config = { config = {
'key': None, 'box_type': 'sealedbox',
'keyfile': None, 'sk': None,
'sk_file': '/etc/salt/pki/master/nacl',
'pk': None,
'pk_file': '/etc/salt/pki/master/nacl.pub',
} }
config_key = '{0}.config'.format(__virtualname__) config_key = '{0}.config'.format(__virtualname__)
config.update(__opts__.get(config_key, {})) try:
for k in set(config) & set(kwargs): config.update(__salt__['config.get'](config_key, {}))
except (NameError, KeyError) as e:
# likly using salt-run so fallback to __opts__
config.update(__opts__.get(config_key, {}))
# pylint: disable=C0201
for k in set(config.keys()) & set(kwargs.keys()):
config[k] = kwargs[k] config[k] = kwargs[k]
return config return config
def _get_key(rstrip_newline=True, **kwargs): def _get_sk(**kwargs):
''' '''
Return key Return sk
''' '''
config = _get_config(**kwargs) config = _get_config(**kwargs)
key = config['key'] key = config['sk']
keyfile = config['keyfile'] sk_file = config['sk_file']
if not key and keyfile: if not key and sk_file:
if not os.path.isfile(keyfile): with salt.utils.fopen(sk_file, 'rb') as keyf:
raise Exception('file not found: {0}'.format(keyfile)) key = str(keyf.read()).rstrip('\n')
with salt.utils.files.fopen(keyfile, 'rb') as keyf:
key = keyf.read()
if key is None: if key is None:
raise Exception('no key found') raise Exception('no key or sk_file found')
key = str(key) return base64.b64decode(key)
if rstrip_newline:
key = key.rstrip('\n')
return key
def keygen(keyfile=None): def _get_pk(**kwargs):
''' '''
Use libnacl to generate a private key Return pk
'''
config = _get_config(**kwargs)
pubkey = config['pk']
pk_file = config['pk_file']
if not pubkey and pk_file:
with salt.utils.fopen(pk_file, 'rb') as keyf:
pubkey = str(keyf.read()).rstrip('\n')
if pubkey is None:
raise Exception('no pubkey or pk_file found')
pubkey = str(pubkey)
return base64.b64decode(pubkey)
def keygen(sk_file=None, pk_file=None):
'''
Use libnacl to generate a keypair.
If no `sk_file` is defined return a keypair.
If only the `sk_file` is defined `pk_file` will use the same name with a postfix `.pub`.
When the `sk_file` is already existing, but `pk_file` is not. The `pk_file` will be generated
using the `sk_file`.
CLI Examples: CLI Examples:
.. code-block:: bash .. code-block:: bash
salt-run nacl.keygen salt-call nacl.keygen
salt-run nacl.keygen keyfile=/root/.nacl salt-call nacl.keygen sk_file=/etc/salt/pki/master/nacl
salt-run --out=newline_values_only nacl.keygen > /root/.nacl salt-call nacl.keygen sk_file=/etc/salt/pki/master/nacl pk_file=/etc/salt/pki/master/nacl.pub
salt-call --local nacl.keygen
''' '''
b = libnacl.secret.SecretBox() if sk_file is None:
key = b.sk kp = libnacl.public.SecretKey()
key = base64.b64encode(key) return {'sk': base64.b64encode(kp.sk), 'pk': base64.b64encode(kp.pk)}
if keyfile:
if os.path.isfile(keyfile): if pk_file is None:
raise Exception('file already found: {0}'.format(keyfile)) pk_file = '{0}.pub'.format(sk_file)
with salt.utils.files.fopen(keyfile, 'w') as keyf:
keyf.write(key) if sk_file and pk_file is None:
return 'saved: {0}'.format(keyfile) if not os.path.isfile(sk_file):
return key kp = libnacl.public.SecretKey()
with salt.utils.fopen(sk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.sk))
if not salt.utils.is_windows():
# chmod 0600 file
os.chmod(sk_file, 1536)
return 'saved sk_file: {0}'.format(sk_file)
else:
raise Exception('sk_file:{0} already exist.'.format(sk_file))
if sk_file is None and pk_file:
raise Exception('sk_file: Must be set inorder to generate a public key.')
if os.path.isfile(sk_file) and os.path.isfile(pk_file):
raise Exception('sk_file:{0} and pk_file:{1} already exist.'.format(sk_file, pk_file))
if os.path.isfile(sk_file) and not os.path.isfile(pk_file):
# generate pk using the sk
with salt.utils.fopen(sk_file, 'rb') as keyf:
sk = str(keyf.read()).rstrip('\n')
sk = base64.b64decode(sk)
kp = libnacl.public.SecretKey(sk)
with salt.utils.fopen(pk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.pk))
return 'saved pk_file: {0}'.format(pk_file)
kp = libnacl.public.SecretKey()
with salt.utils.fopen(sk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.sk))
if not salt.utils.is_windows():
# chmod 0600 file
os.chmod(sk_file, 1536)
with salt.utils.fopen(pk_file, 'w') as keyf:
keyf.write(base64.b64encode(kp.pk))
return 'saved sk_file:{0} pk_file: {1}'.format(sk_file, pk_file)
def enc(data, **kwargs): def enc(data, **kwargs):
''' '''
Takes a key generated from `nacl.keygen` and encrypt some data. Alias to `{box_type}_encrypt`
box_type: secretbox, sealedbox(default)
'''
box_type = _get_config(**kwargs)['box_type']
if box_type == 'sealedbox':
return sealedbox_encrypt(data, **kwargs)
if box_type == 'secretbox':
return secretbox_encrypt(data, **kwargs)
return sealedbox_encrypt(data, **kwargs)
def enc_file(name, out=None, **kwargs):
'''
This is a helper function to encrypt a file and return its contents.
You can provide an optional output file using `out`
`name` can be a local file or when not using `salt-run` can be a url like `salt://`, `https://` etc.
CLI Examples: CLI Examples:
.. code-block:: bash .. code-block:: bash
salt-run nacl.enc datatoenc salt-run nacl.enc_file name=/tmp/id_rsa
salt-run nacl.enc datatoenc keyfile=/root/.nacl salt-call nacl.enc_file name=salt://crt/mycert out=/tmp/cert
salt-run nacl.enc datatoenc key='cKEzd4kXsbeCE7/nLTIqXwnUiD1ulg4NoeeYcCFpd9k=' salt-run nacl.enc_file name=/tmp/id_rsa box_type=secretbox \
sk_file=/etc/salt/pki/master/nacl.pub
''' '''
key = _get_key(**kwargs) try:
sk = base64.b64decode(key) data = __salt__['cp.get_file_str'](name)
b = libnacl.secret.SecretBox(sk) except Exception as e:
return base64.b64encode(b.encrypt(data)) # likly using salt-run so fallback to local filesystem
with salt.utils.fopen(name, 'rb') as f:
data = f.read()
d = enc(data, **kwargs)
if out:
if os.path.isfile(out):
raise Exception('file:{0} already exist.'.format(out))
with salt.utils.fopen(out, 'wb') as f:
f.write(d)
return 'Wrote: {0}'.format(out)
return d
def dec(data, **kwargs): def dec(data, **kwargs):
''' '''
Takes a key generated from `nacl.keygen` and decrypt some data. Alias to `{box_type}_decrypt`
box_type: secretbox, sealedbox(default)
'''
box_type = _get_config(**kwargs)['box_type']
if box_type == 'sealedbox':
return sealedbox_decrypt(data, **kwargs)
if box_type == 'secretbox':
return secretbox_decrypt(data, **kwargs)
return sealedbox_decrypt(data, **kwargs)
def dec_file(name, out=None, **kwargs):
'''
This is a helper function to decrypt a file and return its contents.
You can provide an optional output file using `out`
`name` can be a local file or when not using `salt-run` can be a url like `salt://`, `https://` etc.
CLI Examples: CLI Examples:
.. code-block:: bash .. code-block:: bash
salt-run nacl.dec pEXHQM6cuaF7A= salt-run nacl.dec_file name=/tmp/id_rsa.nacl
salt-run nacl.dec data='pEXHQM6cuaF7A=' keyfile=/root/.nacl salt-call nacl.dec_file name=salt://crt/mycert.nacl out=/tmp/id_rsa
salt-run nacl.dec data='pEXHQM6cuaF7A=' key='cKEzd4kXsbeCE7/nLTIqXwnUiD1ulg4NoeeYcCFpd9k=' salt-run nacl.dec_file name=/tmp/id_rsa.nacl box_type=secretbox \
sk_file=/etc/salt/pki/master/nacl.pub
''' '''
key = _get_key(**kwargs) try:
sk = base64.b64decode(key) data = __salt__['cp.get_file_str'](name)
b = libnacl.secret.SecretBox(key=sk) except Exception as e:
# likly using salt-run so fallback to local filesystem
with salt.utils.fopen(name, 'rb') as f:
data = f.read()
d = dec(data, **kwargs)
if out:
if os.path.isfile(out):
raise Exception('file:{0} already exist.'.format(out))
with salt.utils.fopen(out, 'wb') as f:
f.write(d)
return 'Wrote: {0}'.format(out)
return d
def sealedbox_encrypt(data, **kwargs):
'''
Encrypt data using a public key generated from `nacl.keygen`.
The encryptd data can be decrypted using `nacl.sealedbox_decrypt` only with the secret key.
CLI Examples:
.. code-block:: bash
salt-run nacl.sealedbox_encrypt datatoenc
salt-call --local nacl.sealedbox_encrypt datatoenc pk_file=/etc/salt/pki/master/nacl.pub
salt-call --local nacl.sealedbox_encrypt datatoenc pk='vrwQF7cNiNAVQVAiS3bvcbJUnF0cN6fU9YTZD9mBfzQ='
'''
pk = _get_pk(**kwargs)
b = libnacl.sealed.SealedBox(pk)
return base64.b64encode(b.encrypt(data))
def sealedbox_decrypt(data, **kwargs):
'''
Decrypt data using a secret key that was encrypted using a public key with `nacl.sealedbox_encrypt`.
CLI Examples:
.. code-block:: bash
salt-call nacl.sealedbox_decrypt pEXHQM6cuaF7A=
salt-call --local nacl.sealedbox_decrypt data='pEXHQM6cuaF7A=' sk_file=/etc/salt/pki/master/nacl
salt-call --local nacl.sealedbox_decrypt data='pEXHQM6cuaF7A=' sk='YmFkcGFzcwo='
'''
if data is None:
return None
sk = _get_sk(**kwargs)
keypair = libnacl.public.SecretKey(sk)
b = libnacl.sealed.SealedBox(keypair)
return b.decrypt(base64.b64decode(data))
def secretbox_encrypt(data, **kwargs):
'''
Encrypt data using a secret key generated from `nacl.keygen`.
The same secret key can be used to decrypt the data using `nacl.secretbox_decrypt`.
CLI Examples:
.. code-block:: bash
salt-run nacl.secretbox_encrypt datatoenc
salt-call --local nacl.secretbox_encrypt datatoenc sk_file=/etc/salt/pki/master/nacl
salt-call --local nacl.secretbox_encrypt datatoenc sk='YmFkcGFzcwo='
'''
sk = _get_sk(**kwargs)
b = libnacl.secret.SecretBox(sk)
return base64.b64encode(b.encrypt(data))
def secretbox_decrypt(data, **kwargs):
'''
Decrypt data that was encrypted using `nacl.secretbox_encrypt` using the secret key
that was generated from `nacl.keygen`.
CLI Examples:
.. code-block:: bash
salt-call nacl.secretbox_decrypt pEXHQM6cuaF7A=
salt-call --local nacl.secretbox_decrypt data='pEXHQM6cuaF7A=' sk_file=/etc/salt/pki/master/nacl
salt-call --local nacl.secretbox_decrypt data='pEXHQM6cuaF7A=' sk='YmFkcGFzcwo='
'''
if data is None:
return None
key = _get_sk(**kwargs)
b = libnacl.secret.SecretBox(key=key)
return b.decrypt(base64.b64decode(data)) return b.decrypt(base64.b64decode(data))