Merge pull request 'truenas-12' (#1) from truenas-12 into master
Reviewed-on: #1
This commit is contained in:
10
README.md
10
README.md
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 =
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user