Skip to content

Commit 16fd9e9

Browse files
committed
updates
1 parent 17ad075 commit 16fd9e9

File tree

3 files changed

+56
-6
lines changed

3 files changed

+56
-6
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ This version of this script is a work in progress, and has had minimal testing.
1010
# Known issues
1111
Connections to the Websocket API will fail if you have a HTTP -> HTTPS redirect enabled, either in TrueNAS itself or in some other system (e.g., Traefik) in front of TrueNAS. This results from an [issue](https://github.com/truenas/api_client/issues/13) in the upstream API client.
1212

13+
# Status
14+
* TrueNAS SCALE 25.04-BETA1 - Works locally (running on the TrueNAS host) and remotely (so long as all dependencies are installed), but see notes below.
15+
* TrueNAS SCALE 24.10 - Works locally and remotely.
16+
* TrueNAS SCALE 24.04 - Works remotely only--the TrueNAS API client isn't installed in this version of TrueNAS. Will not update certificates for apps on this or earlier versions of TrueNAS SCALE.
17+
* TrueNAS SCALE 23.10 - Same as 24.04.
18+
19+
## Notes for 25.04
20+
Security measures in TrueNAS SCALE 25.04 require that the API keys be passed over a secure channel. In order to use this script with 25.04:
21+
* You must have already deployed a trusted cert to your NAS
22+
* That cert must not be expired
23+
* You must have configured the UI to use that cert
24+
* The address you're using to connect to the NAS (`connect_host` in `deploy_config`) must be named in that cert
25+
* You must set `protocol = wss` in `deploy_config`.
26+
1327
# Installation
1428
This script can run on any machine running Python 3 that has network access to your TrueNAS server, but in most cases it's best to run it directly on the TrueNAS box. Change to a convenient directory and run `git clone https://github.com/danb35/deploy-freenas`. If you're installing this on your TrueNAS server, it cannot be in your home directory; place it in a convenient place on a storage pool instead.
1529

@@ -30,6 +44,7 @@ ui_certificate_enabled = true
3044
ftp_enabled = false
3145
apps_enabled = false
3246
cert_base_name = letsencrypt
47+
protocol = ws
3348
```
3449

3550
Everything but `api_key` and paths to the cert and key are optional, and the defaults are documented in `deploy_config.example`.

deploy_config.example

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ api_key = YourNewlyGeneratedAPIKey#@#$*
1717
# Default is localhost (assuming the script is running on your FreeNAS box)
1818
# connect_host = baz.bar.foo
1919

20-
# verify sets whether the script will attempt to verify the server's certificate with a HTTPS
21-
# connection. Set to true if you're using a HTTPS connection to a remote host. If connect_host
22-
# is set to localhost (or is unset), set to false. Default is false.
23-
# verify = false
20+
# protocol specifies the protocol used to connect to the API. Default is ws.
21+
# Set to wss for TrueNAS 25.04 and later
22+
# protocol = wss
2423

2524
# set ui_certificate_enabled to false if you want to skip using the new cerificate for the UI. Default is true.
2625
# ui_certificate_enabled = false

deploy_freenas.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import socket
2626
from datetime import datetime, timedelta
2727
from truenas_api_client import Client
28+
from OpenSSL import crypto
29+
import re
2830

2931
parser = argparse.ArgumentParser(description='Import and activate a SSL/TLS certificate into TrueNAS.')
3032
parser.add_argument('-c', '--config', default=(os.path.join(os.path.dirname(os.path.realpath(__file__)),
@@ -40,8 +42,9 @@
4042
exit(1)
4143

4244
API_KEY = deploy.get('api_key')
45+
PROTOCOL = deploy.get('protocol', "ws")
4346
CONNECT_HOST = deploy.get('connect_host',"localhost")
44-
CONNECT_URI = "ws://" + CONNECT_HOST + "/websocket"
47+
CONNECT_URI = PROTOCOL + "://" + CONNECT_HOST + "/websocket"
4548

4649
PRIVATEKEY_PATH = deploy.get('privkey_path')
4750
if os.path.isfile(PRIVATEKEY_PATH)==False:
@@ -67,8 +70,41 @@
6770
with open(FULLCHAIN_PATH, 'r') as file:
6871
full_chain = file.read()
6972

73+
def extract_leaf_certificate(fullchain_pem):
74+
"""Extract the first certificate (leaf) from a full chain PEM file."""
75+
certs = re.findall(r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----",
76+
fullchain_pem, re.DOTALL)
77+
if not certs:
78+
raise ValueError("No valid certificate found in the provided PEM data.")
79+
return certs[0] # Return the first certificate (leaf)
80+
81+
def validate_cert_key_pair(cert_pem, key_pem):
82+
"""Validate that the certificate's public key matches the private key."""
83+
leaf_cert_pem = extract_leaf_certificate(cert_pem)
84+
85+
# Load the extracted leaf certificate and key
86+
cert_obj = crypto.load_certificate(crypto.FILETYPE_PEM, leaf_cert_pem)
87+
key_obj = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
88+
89+
# Verify that the certificate's public key matches the private key
90+
try:
91+
return cert_obj.get_pubkey().to_cryptography_key().public_numbers() == \
92+
key_obj.to_cryptography_key().public_key().public_numbers()
93+
except Exception as e:
94+
print(f"Validation error: {e}")
95+
return False
96+
97+
if validate_cert_key_pair(full_chain, priv_key):
98+
print("✅ Certificate and private key match.")
99+
else:
100+
print("❌ Certificate and private key do not match.")
101+
exit(1)
102+
70103
with Client(CONNECT_URI) as c:
71-
c.call("auth.login_with_api_key", API_KEY)
104+
result=c.call("auth.login_with_api_key", API_KEY)
105+
if result==False:
106+
print("Failed to authenticate!")
107+
exit(1)
72108
# Import the certificate
73109
args = {"name": cert_name, "certificate": full_chain, "privatekey": priv_key, "create_type": "CERTIFICATE_CREATE_IMPORTED"}
74110
cert = c.call("certificate.create", args, job=True)

0 commit comments

Comments
 (0)