From 3fff6e5a620427f7ca7323713860b38fa6f81368 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 May 2026 18:12:55 +0200 Subject: [PATCH 1/5] Made cf-remote install --demo generate a random password and salt Co-authored-by: Claude Opus 4.7 (1M context) Signed-off-by: Ole Herman Schumacher Elgesem --- cf_remote/commands.py | 13 ++++++++++-- cf_remote/demo.py | 49 +++++++++++++++++++++++++++++++++++-------- cf_remote/demo.sql | 8 +++---- cf_remote/remote.py | 8 +++++-- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/cf_remote/commands.py b/cf_remote/commands.py index 90c499d..c21dfff 100644 --- a/cf_remote/commands.py +++ b/cf_remote/commands.py @@ -14,6 +14,7 @@ transfer_file, deploy_masterfiles, ) +import cf_remote.demo as demo_lib from cf_remote.packages import Releases from cf_remote.web import download_package from cf_remote.paths import ( @@ -229,12 +230,18 @@ def install( "\n".join(bootstrap + [""]), ) + hub_passwords = {} hub_jobs = [] if hubs: show_host_info = len(hubs) == 1 if type(hubs) is str: hubs = [hubs] for index, hub in enumerate(hubs): + if demo: + password, salt, sha = demo_lib.generate_password() + hub_passwords[hub] = password + else: + salt, sha = None, None hub_jobs.append( HostInstaller( hub, @@ -249,6 +256,8 @@ def install( remote_download=remote_download, trust_keys=trust_keys, insecure=insecure, + demo_salt=salt, + demo_sha=sha, ) ) @@ -293,8 +302,8 @@ def install( if demo and hubs: for hub in hubs: print( - "Your demo hub is ready: https://{}/ (Username: admin, Password: password)".format( - strip_user(hub) + "Your demo hub is ready: https://{}/ (Username: admin, Password: {})".format( + strip_user(hub), hub_passwords[hub] ) ) diff --git a/cf_remote/demo.py b/cf_remote/demo.py index cdfc7c1..00f48ca 100644 --- a/cf_remote/demo.py +++ b/cf_remote/demo.py @@ -1,10 +1,13 @@ import os import json +import hashlib +import secrets +import string from posixpath import dirname, join from cf_remote import log from cf_remote.paths import cf_remote_dir -from cf_remote.utils import save_file +from cf_remote.utils import save_file, strip_user from cf_remote.ssh import scp, ssh_sudo, ssh_cmd, auto_connect @@ -22,18 +25,46 @@ def agent_run(data, *, connection=None): log.debug(output) +def generate_password(): + """Generate credentials for the demo admin user. + + Returns (password, salt, sha) where sha is the hex SHA-256 of + salt + password concatenated with no separator. The password is meant + to be shown to the user; only the salt and sha are sent to the host. + """ + password = "".join(secrets.choice(string.ascii_letters) for _ in range(14)) + salt = "".join(secrets.choice(string.ascii_letters) for _ in range(10)) + sha = hashlib.sha256((salt + password).encode("utf-8")).hexdigest() + return password, salt, sha + + @auto_connect -def disable_password_dialog(host, *, connection=None): +def disable_password_dialog(host, salt, sha, *, connection=None): print("Disabling password change on hub: '{}'".format(host)) - query_path = join(dirname(__file__), "demo.sql") - scp(query_path, host, connection=connection) + template_path = join(dirname(__file__), "demo.sql") + with open(template_path, "r") as f: + sql = f.read() + sql = sql.replace("__CF_REMOTE_SHA__", sha).replace("__CF_REMOTE_SALT__", salt) - query = os.path.basename(query_path) - ssh_sudo( - connection, - '/var/cfengine/bin/psql cfsettings -f "{}"'.format(query), - ) + safe_host = strip_user(host).replace("/", "_").replace(":", "_") + rendered_path = os.path.join(cf_remote_dir(), "demo-{}.sql".format(safe_host)) + save_file(rendered_path, sql) + try: + scp(rendered_path, host, connection=connection) + query = os.path.basename(rendered_path) + try: + ssh_sudo( + connection, + '/var/cfengine/bin/psql cfsettings -f "{}"'.format(query), + ) + finally: + ssh_cmd(connection, 'rm -f "{}"'.format(query)) + finally: + try: + os.remove(rendered_path) + except OSError as e: + log.warning("Could not remove local '{}': {}".format(rendered_path, e)) def def_json(call_collect=False): diff --git a/cf_remote/demo.sql b/cf_remote/demo.sql index ed6a087..edfd215 100644 --- a/cf_remote/demo.sql +++ b/cf_remote/demo.sql @@ -13,8 +13,8 @@ INSERT INTO "users" ("username", "roles", "changetimestamp") SELECT 'admin', - 'SHA=7f062dc2ef82d2b87f012fc17d70c372aa4e2883d9b6c5c1cc7382a5c868b724', - 'eWAbKQmxNP', + 'SHA=__CF_REMOTE_SHA__', + '__CF_REMOTE_SALT__', 'admin', 'admin@organisation.com', FALSE, @@ -23,5 +23,5 @@ SELECT 'admin', now() ON CONFLICT (username, EXTERNAL) DO UPDATE -SET password = 'SHA=7f062dc2ef82d2b87f012fc17d70c372aa4e2883d9b6c5c1cc7382a5c868b724', - salt = 'eWAbKQmxNP'; +SET password = 'SHA=__CF_REMOTE_SHA__', + salt = '__CF_REMOTE_SALT__'; diff --git a/cf_remote/remote.py b/cf_remote/remote.py index b8aee65..e945e14 100644 --- a/cf_remote/remote.py +++ b/cf_remote/remote.py @@ -661,7 +661,9 @@ def install_host( show_info=True, remote_download=False, trust_keys=None, - insecure=False + insecure=False, + demo_salt=None, + demo_sha=None, ): data = get_info(host, connection=connection) if show_info: @@ -758,7 +760,9 @@ def install_host( host, connection=connection, call_collect=call_collect ) demo_lib.agent_run(data, connection=connection) - demo_lib.disable_password_dialog(host, connection=connection) + demo_lib.disable_password_dialog( + host, demo_salt, demo_sha, connection=connection + ) demo_lib.agent_run(data, connection=connection) return 0 From 7f0215b30c1fd3a168c015d997ebdbf051f91c78 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 May 2026 18:16:05 +0200 Subject: [PATCH 2/5] Adjusted --demo message to be more accurate Signed-off-by: Ole Herman Schumacher Elgesem --- cf_remote/demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf_remote/demo.py b/cf_remote/demo.py index 00f48ca..9c5973f 100644 --- a/cf_remote/demo.py +++ b/cf_remote/demo.py @@ -40,7 +40,7 @@ def generate_password(): @auto_connect def disable_password_dialog(host, salt, sha, *, connection=None): - print("Disabling password change on hub: '{}'".format(host)) + print("Setting up demo admin user on hub: '{}'".format(host)) template_path = join(dirname(__file__), "demo.sql") with open(template_path, "r") as f: From 3b30af4e10fbee79d83895dc9c1e9f6965aebcac Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Thu, 7 May 2026 18:16:42 +0200 Subject: [PATCH 3/5] Renamed function for more up to date name Signed-off-by: Ole Herman Schumacher Elgesem --- cf_remote/demo.py | 2 +- cf_remote/remote.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cf_remote/demo.py b/cf_remote/demo.py index 9c5973f..a3b43fb 100644 --- a/cf_remote/demo.py +++ b/cf_remote/demo.py @@ -39,7 +39,7 @@ def generate_password(): @auto_connect -def disable_password_dialog(host, salt, sha, *, connection=None): +def setup_demo_admin_user(host, salt, sha, *, connection=None): print("Setting up demo admin user on hub: '{}'".format(host)) template_path = join(dirname(__file__), "demo.sql") diff --git a/cf_remote/remote.py b/cf_remote/remote.py index e945e14..1ca89bd 100644 --- a/cf_remote/remote.py +++ b/cf_remote/remote.py @@ -760,7 +760,7 @@ def install_host( host, connection=connection, call_collect=call_collect ) demo_lib.agent_run(data, connection=connection) - demo_lib.disable_password_dialog( + demo_lib.setup_demo_admin_user( host, demo_salt, demo_sha, connection=connection ) demo_lib.agent_run(data, connection=connection) From f17e45cbcec91ab5e9591ed77bcd184024006ac7 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Fri, 8 May 2026 14:55:39 +0200 Subject: [PATCH 4/5] Ensured demo SQL files are created with 0600 permissions Co-authored-by: Claude Opus 4.7 (1M context) Signed-off-by: Ole Herman Schumacher Elgesem --- cf_remote/demo.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cf_remote/demo.py b/cf_remote/demo.py index a3b43fb..b867522 100644 --- a/cf_remote/demo.py +++ b/cf_remote/demo.py @@ -7,7 +7,7 @@ from cf_remote import log from cf_remote.paths import cf_remote_dir -from cf_remote.utils import save_file, strip_user +from cf_remote.utils import mkdir, save_file, strip_user from cf_remote.ssh import scp, ssh_sudo, ssh_cmd, auto_connect @@ -48,8 +48,16 @@ def setup_demo_admin_user(host, salt, sha, *, connection=None): sql = sql.replace("__CF_REMOTE_SHA__", sha).replace("__CF_REMOTE_SALT__", salt) safe_host = strip_user(host).replace("/", "_").replace(":", "_") + mkdir(cf_remote_dir()) rendered_path = os.path.join(cf_remote_dir(), "demo-{}.sql".format(safe_host)) - save_file(rendered_path, sql) + # The SQL file contains the password salt and SHA, so create it with + # 0600 perms from the start (O_EXCL ensures we don't reuse a pre-existing + # file that might have looser permissions). + if os.path.exists(rendered_path): + os.remove(rendered_path) + fd = os.open(rendered_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, "w") as f: + f.write(sql) try: scp(rendered_path, host, connection=connection) query = os.path.basename(rendered_path) From fea4551d55dbd249e0cf11272997ce9436092b7c Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Fri, 8 May 2026 14:58:58 +0200 Subject: [PATCH 5/5] Changed --demo functionality to use tempfile.mkdtemp Co-authored-by: Claude Opus 4.7 (1M context) Signed-off-by: Ole Herman Schumacher Elgesem --- cf_remote/demo.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/cf_remote/demo.py b/cf_remote/demo.py index b867522..5a7fc70 100644 --- a/cf_remote/demo.py +++ b/cf_remote/demo.py @@ -2,12 +2,14 @@ import json import hashlib import secrets +import shutil import string +import tempfile from posixpath import dirname, join from cf_remote import log from cf_remote.paths import cf_remote_dir -from cf_remote.utils import mkdir, save_file, strip_user +from cf_remote.utils import save_file from cf_remote.ssh import scp, ssh_sudo, ssh_cmd, auto_connect @@ -47,18 +49,14 @@ def setup_demo_admin_user(host, salt, sha, *, connection=None): sql = f.read() sql = sql.replace("__CF_REMOTE_SHA__", sha).replace("__CF_REMOTE_SALT__", salt) - safe_host = strip_user(host).replace("/", "_").replace(":", "_") - mkdir(cf_remote_dir()) - rendered_path = os.path.join(cf_remote_dir(), "demo-{}.sql".format(safe_host)) - # The SQL file contains the password salt and SHA, so create it with - # 0600 perms from the start (O_EXCL ensures we don't reuse a pre-existing - # file that might have looser permissions). - if os.path.exists(rendered_path): - os.remove(rendered_path) - fd = os.open(rendered_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) - with os.fdopen(fd, "w") as f: - f.write(sql) + # The SQL file contains the password salt and SHA. mkdtemp creates the + # directory with 0700 perms, so anything inside is protected from other + # local users. + tmp_dir = tempfile.mkdtemp(prefix="cf-remote-demo-") try: + rendered_path = os.path.join(tmp_dir, "demo.sql") + with open(rendered_path, "w") as f: + f.write(sql) scp(rendered_path, host, connection=connection) query = os.path.basename(rendered_path) try: @@ -69,10 +67,7 @@ def setup_demo_admin_user(host, salt, sha, *, connection=None): finally: ssh_cmd(connection, 'rm -f "{}"'.format(query)) finally: - try: - os.remove(rendered_path) - except OSError as e: - log.warning("Could not remove local '{}': {}".format(rendered_path, e)) + shutil.rmtree(tmp_dir, ignore_errors=True) def def_json(call_collect=False):