36 Commits

Author SHA1 Message Date
a24c81160f Merge pull request 'updated for new api' (#2) from Scale-24 into master
Reviewed-on: #2

Updated to new api
2025-10-24 01:43:20 +00:00
eb226df901 updated for new api 2025-10-24 01:41:46 +00:00
b6aa8121b8 Add try catch for services to restart to work on core and scale for dataset unlock 2024-08-31 15:40:10 +00:00
92c693abc4 commented out jail starts since they do work with scale, will need to fix this for backwards comp 2024-08-31 15:21:22 +00:00
d73c245475 Updated to test for encrypt key in pool to keep backwards compatibility while working with scale 2024-08-31 15:17:08 +00:00
7b08763c64 Updated as simplejson is not available on truenas scale 2024-08-31 15:06:50 +00:00
c541a93994 Increased sleep to 120 seconds 2024-07-07 22:24:24 +00:00
c609216830 update help toc correctly show program name 2023-11-03 15:46:14 -04:00
cce20aa267 upped sleep to 60 seconds 2023-07-26 17:07:09 -04:00
f868336a8b upped sleep to 30 seconds 2023-07-26 16:58:05 -04:00
7a2b4d52c9 add a delay after activating pool for jail storage 2023-07-26 16:46:27 -04:00
cac20e14b6 Commented out restart of jail service 2023-07-26 15:25:21 -04:00
e7db02dce3 added logic to remove jails service restart before we have selected the jail storage pool 2023-07-26 15:23:00 -04:00
dac79ffa7c removed extra / 2023-07-26 15:16:26 -04:00
bb1a0616ed updated to use pool id instead of dataset id which is not int 2023-07-26 15:16:08 -04:00
4aeec47d1b added missing end brackets 2023-07-23 23:19:32 -04:00
acb4ac3921 fixed spacing 2023-07-23 23:18:28 -04:00
aa8574da13 fixed spacing 2023-07-23 23:17:57 -04:00
d55a5fff83 fixed spacing 2023-07-23 23:17:27 -04:00
e01b3d11ec Added lookup for services before unlocking so we can restart the correct services enabled 2023-07-23 23:16:45 -04:00
f8009f9ad3 Added empty line before jail storage pool output 2023-07-23 15:06:28 -04:00
13abab0b67 Updated to work with new config process 2023-07-23 15:05:52 -04:00
fe752ff3fb added force restart of jails 2023-07-21 23:49:34 -04:00
4025bca3cb numbered array didn't work :( 2023-07-21 23:44:14 -04:00
ff5b2e2d74 No point in asking for jail storage pool when creating a pool since there is only one in the config 2023-07-21 23:42:36 -04:00
72048eee94 mixed up e and j code 2023-07-21 23:40:29 -04:00
4f55ac6cce Updated to have jail storage pool and activate pool after unlock 2023-07-21 23:20:06 -04:00
a7d84132d6 I forgot the pool unlock already restarts the services 2023-04-02 17:46:23 -04:00
46e2edca74 switch to hmac compare digest for password confirmation verify 2023-04-02 14:53:31 -04:00
4c94efef26 spelling mistakes 2023-04-02 13:45:35 -04:00
d93ef77fd7 space 2023-04-02 13:45:08 -04:00
5f7e60e220 removed api key from debug log as it is easy to create a new one 2023-04-02 13:43:54 -04:00
20d6b44f6c updated wording 2023-04-02 13:43:14 -04:00
87a021529f moved config menu into for loop for better visuals 2023-04-02 13:03:01 -04:00
91908ae777 updated missed pool variable to config 2023-04-02 13:01:31 -04:00
1b9329d471 update git ignore to include secrets.config 2023-04-01 23:28:21 -04:00
4 changed files with 97 additions and 139 deletions

1
.gitignore vendored
View File

@@ -124,4 +124,5 @@ dmypy.json
*.key
*.crt
*secrets.ini
*secrets.config
*client.conf

View File

@@ -1,5 +1,5 @@
# truenas-kmip-unlocker
Encrytped secrets are stored within the [secrets.ini](secrets.ini.sample) file.
Encrytped secrets are stored within the secrets.config file.
**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.**
@@ -26,11 +26,12 @@ suppress_ragged_eofs=True
#### Encrypt your secrets
* Encrypt your api key for truenas
* Encrypt the passphrase for your pool/dataset
* Select jail storage pool
* Encrypt your remaining pool/dataset passphrases as needed
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.
Run the following command to configure your API, jail storage pool, and pool names and passphrases.
```shell
python truenas-kmip-unlock.py --encrypt
python truenas-kmip-unlock.py --config
```
#### Create Task
@@ -40,7 +41,7 @@ python /root/truenas-kmip-unlocker/truenas-kmip-unlock.py
```
#### Debugging
Nothing is logged to a file for this. Everything is output to the console. To enabled debug mode, pass the **[-v|--verbose]** argument when running the command. If the verbose argument is passed in, all passphrases will be outputted in plain text to the console. This is to ensure the decryption is working correctly.
Nothing is logged to a file for this. Everything is output to the console. To enabled debug mode, pass the **[-v|--verbose]** argument when running the command.
```python
python /root/truenas-kmip-unlocker/truenas-kmip-unlock.py --verbose
```

View File

@@ -1,14 +0,0 @@
# 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 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_api_key = encrypted_api_key goes here
#[media]
#encrypted_key = some encrypted key goes here
#
[DEFAULT]
encrypted_api_key =

View File

@@ -9,46 +9,22 @@ import base64
import argparse
import configparser
import getpass
import requests
from truenas_api_client import Client
import platform
import subprocess
import time
import simplejson as json
try:
import simplejson as json
except ImportError:
import json
import hmac as pyhmac
import datetime
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
from kmip.core import enums
from kmip.demos import utils
from kmip.pie import client
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/v2.0/{}'.format(resource)
logger.debug('Request URL: {}'.format(url))
logger.debug('Request Data: {}'.format(data))
r = requests.request(
method,
url,
data=data,
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer {}'.format(api_key)},
verify=False
)
logger.debug('Request Status Code: {}'.format(r.status_code))
if r.ok:
try:
logger.debug('Request Returned JSON: {}'.format(r.json()))
return {'ok': r.ok, 'status_code': r.status_code, 'response': r.json()}
except:
logger.debug('Request Returned Text: {}'.format(r.text))
return {'ok': r.ok, 'status_code': r.status_code, 'response': r.text}
raise ValueError(r)
def build_logger(level):
logger = logging.getLogger()
logger.setLevel(level)
@@ -262,10 +238,11 @@ def new_pool_details():
while True:
pool_passphrase = getpass.getpass("Please enter pool encryption passphrase: ")
pool_passphrase_confirm = getpass.getpass("Please confirm pool encryption passphrase: ")
if pool_passphrase == pool_passphrase_confirm:
if pyhmac.compare_digest(pool_passphrase, pool_passphrase_confirm):
break
else:
print("The pool encryption passphrases do not match, please try again.")
array = dict()
array[pool_name] = dict()
array[pool_name]["pool_passphrase"] = pool_passphrase
@@ -281,7 +258,7 @@ def edit_pool_details(pools, pool_to_edit):
while True:
pool_passphrase = getpass.getpass("Please enter pool encryption passphrase: ")
pool_passphrase_confirm = getpass.getpass("Please confirm pool encryption passphrase: ")
if pool_passphrase == pool_passphrase_confirm:
if pyhmac.compare_digest(pool_passphrase, pool_passphrase_confirm):
break
else:
print("The pool encryption passphrases do not match, please try again.")
@@ -311,8 +288,8 @@ def select_pool(pools, wording = "edit"):
if __name__ == '__main__':
# Build and parse arguments
parser = argparse.ArgumentParser(
usage="%prog [options]",
description="Run Truenas Pool Unlcok opteration. This will unlock encrypted pools on truenas.")
usage="{} [options]".format(os.path.basename(__file__)),
description="Run Truenas pool unlock operation. This will unlock encrypted pools on truenas.")
parser.add_argument(
"-c",
"--config",
@@ -320,6 +297,13 @@ if __name__ == '__main__':
dest="config",
help="Edit pools configuration."
)
#parser.add_argument(
# "-r",
# "--restartAppsVMs",
# action="store_true",
# dest="restartAppsVMs",
# help="Restarts apps and VMs if running"
#)
parser.add_argument (
"-v",
"--verbose",
@@ -356,8 +340,12 @@ if __name__ == '__main__':
user_input = input("n/q> ")
if user_input.casefold() == "n":
config = dict()
api_key = input("Please enter your API key: ")
config['API Key'] = api_key
config['URI'] = input("Please enter your hostname or IP address (default: localhost): ") or "localhost"
if ask_for_confirmation("Would you like to verify the TLS cert?"):
config['Verify TLS'] = True
else:
config['Verify TLS'] = False
config['API Key'] = input("Please enter your API key: ")
config['Pools'] = new_pool_details()
write_config_file(config, secrets_config_file)
break
@@ -365,25 +353,35 @@ if __name__ == '__main__':
sys.exit(0)
else:
print("This value must be one of the following characters: n, q.")
while True:
config = read_config_file(secrets_config_file)
print("Current pools:")
print(" ")
for pool in config['Pools']:
print(pool)
print(" ")
print("a) Edit API Key")
print("URI: {}".format(config['URI']))
print("Verify TLS Cert: {}".format(config['Verify TLS']))
print(" ")
print("h) Edit Host, TLS, and API Key")
print("e) Edit pool")
print("n) New pool")
print("d) Delete pool")
print("q) Quit config")
while True:
user_input = input("a/e/n/d/q> ")
# Edit API Key
if user_input.casefold() == "a":
api_key = input("Please enter your API key: ")
config['API Key'] = api_key
write_config_file(pools, secrets_config_file)
# Editing an account
user_input = input("h/e/n/d/q> ")
# Edit Host, TLS, and API Key
if user_input.casefold() == "h":
if ask_for_confirmation("Would you like to edit the hostname or IP address?\nCurrent Value: {}".format(config['URI'])):
config['URI'] = input("Please enter your hostname or IP address: ")
if ask_for_confirmation("Would you like to edit verify TLS cert?\nCurrent Value: {}".format(config['Verify TLS'])):
if ask_for_confirmation("Would you like to verify the TLS cert?"):
config['Verify TLS'] = True
else:
config['Verify TLS'] = False
if ask_for_confirmation("Would you like to edit the API key?"):
config['API Key'] = input("Please enter your API key: ")
write_config_file(config, secrets_config_file)
# Editing a pool
elif user_input.casefold() == "e":
pool_to_edit = select_pool(config['Pools'])
pool_details = edit_pool_details(config['Pools'], pool_to_edit)
@@ -391,21 +389,21 @@ if __name__ == '__main__':
config['Pools'].update(pool_details)
write_config_file(config, secrets_config_file)
break
# Createing a new account
# Createing a new pool
elif user_input.casefold() == "n":
pool_details = new_pool_details()
config['Pools'].update(pool_details)
write_config_file(config, secrets_config_file)
break
# Deleting an account
# Deleting a pool
elif user_input.casefold() == "d":
pool_to_delete = select_pool(config['Pools'], "delete")
if not ask_for_confirmation("Are you sure you wish to delete {} pool? ".format(pool_to_delete)):
break
del config['Pools'][pool_to_delete]
if len(config['Pools']) == 0:
# no more accounts, remove secrets file
if ask_for_confirmation("There are no pools left in the config, do you want to remove the secrets file?"):
# no more accounts, remove secrets file if requests
os.remove(secrets_config_file)
else:
write_config_file(config, secrets_config_file)
@@ -417,74 +415,46 @@ if __name__ == '__main__':
# Catch all for non-valid characters
else:
print("This value must be one of the following characters: a, e, n, d, q.")
print("This value must be one of the following characters: h, e, n, d, q")
if not os.path.exists(secrets_config_file):
print("No configuration file found. Please run {} -c to configure your accounts.".format(script_name))
print("No configuration file found. Please run {} -c/--config for configuration".format(script_name))
sys.exit(-1)
# run the decryption of the keys and unlock the pool
config = read_config_file(secrets_config_file)
api_key = config['API Key']
logger.debug("API Key: {}".format(api_key))
API_POOLS = request('pool', api_key)['response']
API_DATASETS = request('pool/dataset', api_key)['response']
for pool_dataset_name in config['Pools']:
if pool_dataset_name != 'DEFAULT':
name = pool_dataset_name
pool_passphrase = config['Pools'][name]['pool_passphrase']
logger.info("Attempting to unlock {} pool.".format(name))
for pool in API_POOLS:
if pool['name'] == 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(pool_passphrase), 'services_restart': ['cifs', 'nfs']})
if response['ok']:
logger.info('Pool {} was unlocked successfully'.format(pool['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'})
URI = config['URI']
VERIFY_TLS = config['Verify TLS']
API_KEY = config['API Key']
with Client(uri="wss://{}/api/current".format(URI), verify_ssl=VERIFY_TLS) as c:
if c.call("auth.login_with_api_key", API_KEY):
logger.debug('Successfully logged into {}'.format(URI))
else:
logger.error('Pool {} was NOT unlocked successfully'.format(pool['name']))
else:
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 API_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(pool_passphrase)}]}})
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']))
logger.error('Unable to login to {}'.format(URI))
SYSTEM_INFORMATION = c.call("system.info")
logger.debug('System Info: {}'.format(SYSTEM_INFORMATION))
POOL_DATASETS = c.call("pool.dataset.details")
logger.debug('Pool Datasets: {}'.format(POOL_DATASETS))
for config_pool in config['Pools']:
config_pool_name = config_pool
pool_passphrase = config['Pools'][config_pool_name]['pool_passphrase']
logger.info("Attempting to unlock {} pool".format(config_pool_name))
for pool_dataset in POOL_DATASETS:
pool_name = pool_dataset['name']
if pool_name == config_pool_name:
pool_encrypted = pool_dataset['encrypted']
pool_unlocked = pool_dataset['key_loaded']
if pool_encrypted == True and pool_unlocked == False:
logger.info('Dataset {} is locked'.format(pool_name))
unlock_args = {"datasets": [{"name": pool_name, "passphrase": pool_passphrase}]}
# start any jails that are set to start on boot but are most likely on an encrypted pool
API_JAILS = request('jail', api_key)['response']
for jail in API_JAILS:
if jail['boot'] == 1:
logger.info('Jail {} is set to start on boot'.format(jail['id']))
if jail['state'] == 'down':
logger.info('Jail {} is stopped'.format(jail['id']))
logger.info('Starting jail {}'.format(jail['id']))
response = request('jail/start', api_key, 'POST', '{}'.format(jail['id']))
if response['ok']:
logger.info('Started jail {} successfully'.format(jail['id']))
unlock_status = c.call("pool.dataset.unlock", pool_name, unlock_args, job=True)
# return will look like this: {'unlocked': ['test'], 'failed': {}}
if pool_name in unlock_status.get('unlocked', []):
logger.info('Dataset {} was unlocked successfully'.format(pool_name))
else:
logger.error('Jail {} was NOT started successfully'.format(jail['id']))
logger.error('Dataset {} was NOT unlocked'.format(pool_name))
else:
logger.info('Jail {} is already started'.format(jail['id']))
logger.info('Dataset {} is already unlocked'.format(pool_name))
client.close()
sys.exit(0)