Merge pull request 'truenas-12' (#1) from truenas-12 into master

Reviewed-on: #1
This commit is contained in:
2020-10-20 22:40:00 -04:00
3 changed files with 81 additions and 64 deletions

View File

@@ -1,7 +1,7 @@
# truenas-kmip-unlocker
Encrytped secrets are stored within the [secrets.ini](secrets.ini.sample) file.
##### I reverted the script to make it work with 11.3. I am going to wait until 12 is either RC or released as I had trouble with the new api commands.
**This will work with native zfs encryption but it will only unlock the top encrypted dataset and the children must be encrypted with the same passphrase. This continues to support GELI encrpyted pools.**
#### Install pyKMIP on the computer
```shell
@@ -24,11 +24,11 @@ suppress_ragged_eofs=True
```
#### Encrypt your secrets
* Encrypt your root password for Freenas
* Encrypt the passphrase for your pool
* Encrypt your remaining pool passphrases as needed
* Encrypt your api key for truenas
* Encrypt the passphrase for your pool/dataset
* Encrypt your remaining pool/dataset passphrases as needed
Run the following command to encrypt your secrets, it will ask for your pool passphrase/password that you want to encrypt and to confirm it before outputting the encrypted passphrase. Take the encrypted secret and create a new section in the config ini file for the pool; your encrypted root password goes into the DEFAULT section. The section name will be the pool name and the only key in that section is the encrypted_key which will be this value.
Run the following command to encrypt your secrets, it will ask for your pool/dataset passphrase that you want to encrypt and to confirm it before outputting the encrypted passphrase. Take the encrypted secret and create a new section in the config ini file for the pool/dataset; your encrypted api key goes into the DEFAULT section. The section name will be the pool/dataset name and the only key in that section is the encrypted_key which will be this value.
```shell
python truenas-kmip-unlock.py --encrypt
```

View File

@@ -1,14 +1,14 @@
# THIS IS A SAMPLE FILE, PLEASE COPY IT AND EDIT THE COPY
# INI secrets file holds the encrypted secrest for the script
# the default section holds the encrypted root password and then the other sections are the vm pool names and encrypted keys
# the section name is the name of the pool
# the default section holds the encrypted api key and then the other sections are the pool/datasets names and encrypted keys to unlock the pools/datasets
# the section name is the name of the pool/dataset
# only one variable should be in each section called encrypted_key
# example
#[DEFAULT]
#encrypted_root_password = encrypted_root_password goes here
#[vms]
#encrypted_api_key = encrypted_api_key goes here
#[media]
#encrypted_key = some encrypted key goes here
#
[DEFAULT]
encrypted_root_password =
encrypted_api_key =

View File

@@ -21,21 +21,19 @@ from kmip.demos import utils
from kmip.pie import client
def request(resource, root_password, method='GET', data=None):
def request(resource, api_key, method='GET', data=None):
if data is None:
data = ''
#else:
#data = json.dumps(data)
url = 'https://127.0.0.1/api/v1.0/{}'.format(resource)
else:
data = json.dumps(data)
url = 'https://127.0.0.1/api/v2.0/{}'.format(resource)
logger.debug('Request URL: {}'.format(url))
logger.debug('Request Data: {}'.format(data))
r = requests.request(
method,
url,
#data=data,
data = json.dumps(data),
headers={'Content-Type': "application/json"},
auth=('root', '{0}'.format(root_password)),
data=data,
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer {}'.format(api_key)},
verify=False
)
logger.debug('Request Status Code: {}'.format(r.status_code))
@@ -61,7 +59,7 @@ def create_key(client):
]
)
logger.debug("Successfully created a new encryption key.")
logger.debug("Secret ID: {0}".format(key_id))
logger.debug("Secret ID: {}".format(key_id))
except Exception as e:
logger.error(e)
sys.exit(-1)
@@ -93,17 +91,19 @@ def encrypt(client, data):
iv
)
)
logger.debug("Successfully encrypted the data: {0}".format(data))
logger.debug("Successfully encrypted the data: {}".format(data))
cipher_text_base64_bytes = base64.b64encode(cipher_text)
cipher_text_base64 = cipher_text_base64_bytes.decode('ascii')
logger.debug("Cipher text (raw): {0}".format(cipher_text))
logger.debug("Cipher txt (encoded): {0}".format(cipher_text_base64))
logger.debug("IV (raw): {0}".format(iv))
logger.debug("Cipher text (raw): {}".format(cipher_text))
logger.debug("Cipher txt (encoded): {}".format(cipher_text_base64))
logger.debug("IV (raw): {}".format(iv))
iv_base64_bytes = base64.b64encode(iv)
iv_base64 = iv_base64_bytes.decode('ascii')
logger.debug("IV (encoded): {0}".format(iv_base64))
cipher_data = base64.b64encode(key_id.encode('UTF-8') + iv + cipher_text).decode()
logger.debug("key_id + iv + cipher_text (encoded): {0}".format(cipher_data))
logger.debug("IV (encoded): {}".format(iv_base64))
padded_key_id = str(key_id).zfill(9)
logger.debug("Padding Key ID {} with zeros: {}".format(key_id,padded_key_id))
cipher_data = base64.b64encode(padded_key_id.encode('UTF-8') + iv + cipher_text).decode()
logger.debug("padded_key_id + iv + cipher_text (encoded): {}".format(cipher_data))
return cipher_data
except Exception as e:
logger.error(e)
@@ -111,12 +111,14 @@ def encrypt(client, data):
def decrypt(client, data):
try:
cipher_data = base64.b64decode(data)
key_id = cipher_data[:2].decode('UTF-8')
iv = cipher_data[2:18]
cipher_text = cipher_data[18:]
logger.debug("Decrypting with Key ID: {0}".format(key_id))
logger.debug("Decrypting with IV (raw): {0}".format(iv))
logger.debug("Decrypting cipher text (raw): {0}".format(cipher_text))
padded_key_id = cipher_data[:9].decode('UTF-8')
iv = cipher_data[9:25]
cipher_text = cipher_data[25:]
logger.debug("Removing padding from Key ID: {}".format(padded_key_id))
key_id = padded_key_id.lstrip('0')
logger.debug("Decrypting with Key ID: {}".format(key_id))
logger.debug("Decrypting with IV (raw): {}".format(iv))
logger.debug("Decrypting cipher text (raw): {}".format(cipher_text))
plain_text = client.decrypt(
cipher_text,
uid=key_id,
@@ -132,7 +134,7 @@ def decrypt(client, data):
)
logger.debug("Successfully decrypted the data.")
plain_text = plain_text.decode('utf-8')
logger.debug("Plain text: '{0}'".format(plain_text))
logger.debug("Plain text: '{}'".format(plain_text))
return plain_text
except Exception as e:
logger.error(e)
@@ -174,59 +176,74 @@ if __name__ == '__main__':
config = opts.config
passphrase = opts.message
# Build the client and connect to the server
#with client.ProxyKmipClient(
# config=config,
# config_file=opts.config_file
#) as client:
# encrypt(client, passphrase)
client = client.ProxyKmipClient(config=config, config_file=cwd + '/pykmip/client.conf')
client.open()
if opts.encrypt:
if not passphrase:
while True:
passphrase = getpass("Please enter the passphrase/password to encrypt: ")
passphrase2 = getpass("Please enter the passphrase/password again: ")
passphrase = getpass("Please enter your pool/dataset unlock passphrase to encrypt: ")
passphrase2 = getpass("Please enter the passphrase again: ")
if passphrase == passphrase2:
break
else:
print("The passwords do not match, please try again.")
print("The passphrases do not match, please try again.")
encrypted_passphrase = encrypt(client, passphrase)
print("Your encrypted passphrase: {0}".format(encrypted_passphrase))
print("Your encrypted passphrase: {}".format(encrypted_passphrase))
sys.exit(0)
elif opts.decrypt:
if not passphrase:
passphrase = input("Please enter the passphrase/password to decrypt: ")
passphrase = input("Please enter the encryted passphrase to decrypt: ")
decrypted_passphrase = decrypt(client, passphrase)
print("Your decrypted passphrase: {0}".format(decrypted_passphrase))
print("Your decrypted passphrase: {}".format(decrypted_passphrase))
sys.exit(0)
else:
# run the decryption of the keys and unlock the pool
parser = configparser.ConfigParser()
parser.read(cwd + '/secrets.ini')
encrypted_root_password = parser.get('DEFAULT', 'encrypted_root_password')
root_password = decrypt(client, encrypted_root_password)
logger.debug("Root password: {0}".format(root_password))
#POOLS = request('pool', root_password, 'GET')
POOLS = request('storage/volume/', root_password, 'GET')
for pool_name in parser.sections():
if pool_name != 'DEFAULT':
name = pool_name
encrypted_api_key = parser.get('DEFAULT', 'encrypted_api_key')
api_key = decrypt(client, encrypted_api_key)
logger.debug("API Key: {}".format(api_key))
POOLS = request('pool', api_key)['response']
DATASETS = request('pool/dataset', api_key)['response']
for pool_dataset_name in parser.sections():
if pool_dataset_name != 'DEFAULT':
name = pool_dataset_name
encrypted_key = parser.get(name, 'encrypted_key')
logger.debug("Attempting to decrypt {0} pool with encrypted key: {1}".format(name,encrypted_key))
logger.debug("Attempting to decrypt {} pool with encrypted key: {}".format(name,encrypted_key))
decrypted_key = decrypt(client, encrypted_key)
for pool in POOLS['response']:
for pool in POOLS:
if pool['name'] == name:
if pool['is_decrypted'] == False:
logger.info('Pool {0} is locked'.format(pool['name']))
#response = request('pool/id/{0}/unlock'.format(pool['id']), root_password, 'POST', {'passphrase': '{}'.format(decrypted_key), 'services_restart': ['cifs', 'nfs']})
response = request('storage/volume/{}/unlock/'.format(pool['name']), root_password, 'POST', {'passphrase': '{}'.format(decrypted_key)})
if response['ok']:
logger.info('Pool {} was unlocked successfully'.format(pool['name']))
# GELI encrypted pool
if pool['encrypt'] == 2:
if pool['is_decrypted'] == False:
logger.info('Pool {} is locked'.format(pool['name']))
response = request('pool/id/{}/unlock'.format(pool['id']), api_key, 'POST', {'passphrase': '{}'.format(decrypted_key), 'services_restart': ['cifs', 'nfs']})
if response['ok']:
logger.info('Pool {} was unlocked successfully'.format(pool['name']))
else:
logger.error('Pool {} was NOT unlocked successfully'.format(pool['name']))
else:
logger.error('Pool {} was NOT unlocked successfully'.format(pool['name']))
else:
logger.info('Pool {0} is already unlocked'.format(pool['name']))
logger.info('Pool {} is already unlocked'.format(pool['name']))
# start detection of locked zfs dataset
elif pool['encrypt'] == 0:
# loop through the datasets to find the one we are looking for
for dataset in DATASETS:
if dataset['name'] == name:
if dataset['locked'] == True:
logger.info('Dataset {} is locked'.format(dataset['name']))
response = request('pool/dataset/unlock',api_key, 'POST', {'id': '{}'.format(dataset['id']), 'unlock_options': {'recursive': True, 'datasets': [{'name': '{}'.format(dataset['name']), 'passphrase': '{}'.format(decrypted_key)}]}})
if response['ok']:
logger.info('Dataset {} was unlocked successfully'.format(dataset['name']))
# restart services since the dataset was unlocked
logger.info('Restarting the NFS service')
response = request('service/restart', api_key, 'POST', {'service': 'nfs'})
logger.info('Restarting the CIFS/SMB service')
response = request('service/restart', api_key, 'POST', {'service': 'cifs'})
else:
logger.error('Dataset {} was NOT unlocked successfully'.format(dataset['name']))
else:
logger.info('Dataset {} is already unlocked'.format(dataset['name']))
#print('pool {0} is decrypted: {1}'.format(pool['name'],pool['is_decrypted']))
# this line should be after the actual unlock on the disk
#logger.debug("Decrypted {0} pool key.".format(name))