From 61928aaf316f9c9bf12018f188c05d34843e786e Mon Sep 17 00:00:00 2001 From: Max Makarov Date: Tue, 31 Mar 2026 15:02:07 +0300 Subject: [PATCH] Fix SSH key path for Administrator accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Write SSH public keys for the built-in Administrator account to C:\ProgramData\ssh\administrators_authorized_keys instead of C:\Users\Administrator\.ssh\authorized_keys. The default Windows OpenSSH sshd_config uses a Match Group directive that reads admin keys from the ProgramData path: Match Group administrators AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys The previous behavior wrote keys to ~/.ssh/authorized_keys which: 1. Requires the user profile to exist (fails after sysprep before first login — the ProfileList registry entry is missing) 2. Is ignored by sshd for admin users due to the Match Group override The ProgramData path is a system directory that always exists, does not depend on user profiles, and is where all major cloud providers (AWS EC2Launch v2, Azure) write admin SSH keys. Also set proper ACL on administrators_authorized_keys per Microsoft docs: only BUILTIN\Administrators and NT AUTHORITY\SYSTEM should have access. For non-admin users, the behavior is unchanged (~/.ssh/authorized_keys). Closes: https://github.com/cloudbase/cloudbase-init/issues/162 Signed-off-by: Max Makarov --- cloudbaseinit/plugins/common/sshpublickeys.py | 62 ++++++++++++++----- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/cloudbaseinit/plugins/common/sshpublickeys.py b/cloudbaseinit/plugins/common/sshpublickeys.py index 1c533bdec..6f9230814 100644 --- a/cloudbaseinit/plugins/common/sshpublickeys.py +++ b/cloudbaseinit/plugins/common/sshpublickeys.py @@ -13,11 +13,11 @@ # under the License. import os +import subprocess from oslo_log import log as oslo_logging from cloudbaseinit import conf as cloudbaseinit_conf -from cloudbaseinit import exception from cloudbaseinit.osutils import factory as osutils_factory from cloudbaseinit.plugins.common import base @@ -28,6 +28,34 @@ class SetUserSSHPublicKeysPlugin(base.BasePlugin): + @staticmethod + def _write_authorized_keys(authorized_keys_path, public_keys): + authorized_keys_dir = os.path.dirname(authorized_keys_path) + if not os.path.exists(authorized_keys_dir): + os.makedirs(authorized_keys_dir) + + LOG.info("Writing SSH public keys in: %s" % authorized_keys_path) + with open(authorized_keys_path, 'w') as f: + for public_key in public_keys: + # All public keys are space-stripped. + f.write(public_key + "\n") + + @staticmethod + def _set_admin_authorized_keys_acl(authorized_keys_path): + """Set ACL on administrators_authorized_keys per Microsoft docs. + + Only SYSTEM and Administrators should have access. + """ + try: + subprocess.check_call([ + "icacls.exe", authorized_keys_path, + "/inheritance:r", + "/grant", "Administrators:F", + "/grant", "SYSTEM:F", + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception: + LOG.exception("Failed to set ACL on %s" % authorized_keys_path) + def execute(self, service, shared_data): public_keys = service.get_public_keys() if not public_keys: @@ -37,22 +65,28 @@ def execute(self, service, shared_data): username = service.get_admin_username() or CONF.username osutils = osutils_factory.get_os_utils() - user_home = osutils.get_user_home(username) + # For users in the Administrators group, write keys to + # C:\ProgramData\ssh\administrators_authorized_keys as per + # the default OpenSSH sshd_config Match Group directive. + # This path does not require the user profile to exist. + if osutils.is_builtin_admin(username): + programdata = os.environ.get("ProgramData", r"C:\ProgramData") + admin_keys_path = os.path.join( + programdata, "ssh", "administrators_authorized_keys") + self._write_authorized_keys(admin_keys_path, public_keys) + self._set_admin_authorized_keys_acl(admin_keys_path) + return base.PLUGIN_EXECUTION_DONE, False + + user_home = osutils.get_user_home(username) if not user_home: - raise exception.CloudbaseInitException("User profile not found!") + LOG.warning("User profile not found for %r, " + "cannot write SSH public keys", username) + return base.PLUGIN_EXECUTION_DONE, False LOG.debug("User home: %s" % user_home) - - user_ssh_dir = os.path.join(user_home, '.ssh') - if not os.path.exists(user_ssh_dir): - os.makedirs(user_ssh_dir) - - authorized_keys_path = os.path.join(user_ssh_dir, "authorized_keys") - LOG.info("Writing SSH public keys in: %s" % authorized_keys_path) - with open(authorized_keys_path, 'w') as f: - for public_key in public_keys: - # All public keys are space-stripped. - f.write(public_key + "\n") + authorized_keys_path = os.path.join( + user_home, '.ssh', 'authorized_keys') + self._write_authorized_keys(authorized_keys_path, public_keys) return base.PLUGIN_EXECUTION_DONE, False