From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from bombadil.infradead.org (bombadil.infradead.org [198.137.202.133]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DAA201C3C18 for ; Wed, 17 Sep 2025 00:34:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=198.137.202.133 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1758069294; cv=none; b=bKMbuZ3mkhESniGyB6PLcjdl+TUNaZQliEhg1seF1M6mnmHE0DHH195SjlCeLzEjHmlYd5nB8OlUDRfKhI1VpiAA97XEK1h12tGhSQJ4LzbHvUd8EgBghERjNCIme2XPkpT1PpRCv5AcYpp4jOPcEA8lvKph4CgSWz1Vh8nAvmg= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1758069294; c=relaxed/simple; bh=KZ8GW2fioMVvuuu5PMHz6c6b9kgN0KrIyOyDBntVtrc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Qmi0vvmCbIKI7JoU5TB1Z0SkD5B7dASm4IHZbnPRyGiNA6y1nZEV3zdbVPnw9NZtcoBbeoM0FGoRydUzc9midpdA+/Wnmvhhm9hUubwQoauO9LzzrDLocKs90nIDl3vOpPeOJM/qDY6OwzW9g/hqBVGvZ0dh5CoxJ+4jMvqDlb4= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org; spf=none smtp.mailfrom=infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=4YB4UJJP; arc=none smtp.client-ip=198.137.202.133 Authentication-Results: smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="4YB4UJJP" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=bombadil.20210309; h=Sender:Content-Transfer-Encoding: MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From: Reply-To:Content-Type:Content-ID:Content-Description; bh=0jaGGk4FtASjBrqwNhy+l/MI6Y73WkirckfS66LUV5g=; b=4YB4UJJPorSswgnDaAo42fwQPS IL3r/0ejSaOCdUWcbaqXj055DiGAwwaF9F5W7/6m2FlmLD37sLFpwKPMCL5QWkAksBN0YhdOpxrNM vBu8GySFZWisxjAkewaLlT2KB+Q+kJxKYw8A+1NTO4yTkYa7RC0dJ5Mi4yWgV0e/KGclmYJbn17fu S+NYWbAwYigzcJjyV2zK4OqseRSPO5tR6Mg+aopcE3Oaxlp2VOaNEKApoyn/bAYufegtzzO72fmbQ iRyktmJ/mNShOt4QkxN2WpfNHenixG5Mxp4QK4//+hhKG2rqsDGWDiDxz3KA1wIze+28mKmDUCAz4 4gNtPbTQ==; Received: from mcgrof by bombadil.infradead.org with local (Exim 4.98.2 #2 (Red Hat Linux)) id 1uyg8J-00000009j5G-3sBc; Wed, 17 Sep 2025 00:34:51 +0000 From: Luis Chamberlain To: Chuck Lever , Daniel Gomez , kdevops@lists.linux.dev Cc: Luis Chamberlain Subject: [PATCH v4 1/8] aws: prevent SSH key conflicts across multiple kdevops directories Date: Tue, 16 Sep 2025 17:34:42 -0700 Message-ID: <20250917003451.2318229-2-mcgrof@kernel.org> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20250917003451.2318229-1-mcgrof@kernel.org> References: <20250917003451.2318229-1-mcgrof@kernel.org> Precedence: bulk X-Mailing-List: kdevops@lists.linux.dev List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Sender: Luis Chamberlain Problem: When using kdevops in multiple directories for AWS deployments, all instances share the same hardcoded SSH key name "kdevops_aws_key" in the AWS backend. This creates a critical issue: if different directories use different SSH keys but the same key name, terraform will silently replace the existing key in AWS, immediately locking users out of any running instances that were using the previous key. Root Cause: The AWS terraform configuration uses a fixed ssh_keyname variable that defaults to "kdevops_aws_key". When terraform runs with a different public key content but the same key name, it updates the aws_key_pair resource, replacing the public key in AWS without warning. Solution: Implement directory-based unique SSH key naming for AWS, following the pattern already established for Lambda Labs. Each kdevops directory now generates a unique SSH key name based on its absolute path using SHA256 hashing. Implementation Details: 1. Created scripts/aws_ssh_key_name.py: - Generates unique key names like "kdevops-aws-projectname-a8999659" - Uses SHA256 hash of the directory path (8 chars) for uniqueness - Includes project name for human readability - Provides options for key name, private key path, or public key path 2. Added terraform/aws/kconfigs/Kconfig.ssh: - New configuration choice: unique keys (default) vs shared keys - Unique mode: Each directory gets its own SSH key name automatically - Shared mode: Legacy behavior with explicit warnings about risks - Dynamic key name generation using the Python script 3. Updated terraform integration: - Modified terraform/aws/Kconfig to include new SSH configuration - Updated playbooks/roles/gen_tfvars/templates/aws/terraform.tfvars.j2 to use dynamic ssh_keyname variable - Added defaults in playbooks/roles/gen_tfvars/defaults/main.yml - Integrated with global SSH config in terraform/Kconfig.ssh Benefits: - Prevents accidental key overwrites in AWS backend - No more lockouts from existing instances when using multiple directories - Backward compatible - users can still use shared keys if needed - Consistent with Lambda Labs implementation pattern - Each directory's instances are isolated with their own SSH keys Example: Directory: /home/user/projects/kdevops AWS Key Name: kdevops-aws-projects-kdevops-a8999659 Local Keys: ~/.ssh/kdevops-aws-projects-kdevops-a8999659[.pub] This ensures that running kdevops from different directories will never conflict, as each gets its own unique key name in both AWS and locally. Generated-by: Claude AI Signed-off-by: Luis Chamberlain --- playbooks/roles/gen_tfvars/defaults/main.yml | 2 + .../templates/aws/terraform.tfvars.j2 | 3 +- scripts/aws_ssh_key_name.py | 165 ++++++++++++++++++ terraform/Kconfig.ssh | 14 +- terraform/aws/Kconfig | 1 + terraform/aws/kconfigs/Kconfig.ssh | 79 +++++++++ 6 files changed, 257 insertions(+), 7 deletions(-) create mode 100755 scripts/aws_ssh_key_name.py create mode 100644 terraform/aws/kconfigs/Kconfig.ssh diff --git a/playbooks/roles/gen_tfvars/defaults/main.yml b/playbooks/roles/gen_tfvars/defaults/main.yml index c9e531bc..9957a078 100644 --- a/playbooks/roles/gen_tfvars/defaults/main.yml +++ b/playbooks/roles/gen_tfvars/defaults/main.yml @@ -27,6 +27,8 @@ terraform_aws_instance_type: "t2.micro" terraform_aws_ebs_volumes_per_instance: "0" terraform_aws_ebs_volume_size: 0 terraform_aws_ebs_volume_type: "gp3" +terraform_aws_ssh_key_name: "kdevops-aws-fallback" +terraform_aws_ssh_pubkey_file: "~/.ssh/kdevops_terraform.pub" terraform_oci_assign_public_ip: false terraform_oci_use_existing_vcn: false diff --git a/playbooks/roles/gen_tfvars/templates/aws/terraform.tfvars.j2 b/playbooks/roles/gen_tfvars/templates/aws/terraform.tfvars.j2 index d880254b..4c976f13 100644 --- a/playbooks/roles/gen_tfvars/templates/aws/terraform.tfvars.j2 +++ b/playbooks/roles/gen_tfvars/templates/aws/terraform.tfvars.j2 @@ -14,7 +14,8 @@ aws_ebs_volume_iops = {{ terraform_aws_ebs_volume_iops }} aws_ebs_volume_throughput = {{ terraform_aws_ebs_volume_throughput }} {% endif %} -ssh_config_pubkey_file = "{{ kdevops_terraform_ssh_config_pubkey_file }}" +ssh_keyname = "{{ terraform_aws_ssh_key_name }}" +ssh_config_pubkey_file = "{{ terraform_aws_ssh_pubkey_file | default(kdevops_terraform_ssh_config_pubkey_file) }}" ssh_config_user = "{{ kdevops_terraform_ssh_config_user }}" ssh_config = "{{ sshconfig }}" diff --git a/scripts/aws_ssh_key_name.py b/scripts/aws_ssh_key_name.py new file mode 100755 index 00000000..ff4bc967 --- /dev/null +++ b/scripts/aws_ssh_key_name.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: copyleft-next-0.3.1 + +""" +Generate a unique SSH key name for AWS based on the current directory. +This ensures each kdevops instance uses its own SSH key to prevent conflicts. +""" + +import hashlib +import os +import sys + + +def get_directory_hash(path: str, length: int = 8) -> str: + """ + Generate a short hash of the directory path. + + Args: + path: Directory path to hash + length: Number of hex characters to use (default 8) + + Returns: + Hex string of specified length + """ + # Get the absolute path to ensure consistency + abs_path = os.path.abspath(path) + + # Create SHA256 hash of the path + hash_obj = hashlib.sha256(abs_path.encode("utf-8")) + + # Return first N characters of the hex digest + return hash_obj.hexdigest()[:length] + + +def get_project_name(path: str) -> str: + """ + Extract a meaningful project name from the path. + + Args: + path: Directory path + + Returns: + Project name derived from directory + """ + abs_path = os.path.abspath(path) + + # Get the last two directory components for context + # e.g., /home/user/projects/kdevops -> projects-kdevops + parts = abs_path.rstrip("/").split("/") + + if len(parts) >= 2: + # Use last two directories + project_parts = parts[-2:] + # Filter out generic names + filtered = [ + p + for p in project_parts + if p not in ["data", "home", "root", "usr", "var", "tmp", "cloud", "opt"] + ] + if filtered: + return "-".join(filtered) + + # Fallback to just the last directory + return parts[-1] if parts else "kdevops" + + +def generate_ssh_key_name( + prefix: str = "kdevops-aws", include_project: bool = True +) -> str: + """ + Generate a unique SSH key name for the current directory. + + Args: + prefix: Prefix for the key name (default "kdevops-aws") + include_project: Include project name in the key (default True) + + Returns: + Unique SSH key name like "kdevops-aws-projectname-a1b2c3d4" + """ + # Find the kdevops root directory + script_dir = os.path.dirname(os.path.abspath(__file__)) + kdevops_root = os.path.dirname(script_dir) + + # Use the kdevops root directory for consistent key naming + dir_hash = get_directory_hash(kdevops_root) + + parts = [prefix] + + if include_project: + project = get_project_name(kdevops_root) + # Limit project name length and sanitize + project = project.replace("_", "-").replace(".", "-")[:20] + parts.append(project) + + parts.append(dir_hash) + + # Create the key name + key_name = "-".join(parts) + + # Ensure it's a valid AWS key name (alphanumeric, hyphens, underscores) + key_name = "".join(c if c.isalnum() or c in "-_" else "-" for c in key_name) + + # Remove multiple consecutive hyphens + while "--" in key_name: + key_name = key_name.replace("--", "-") + + # AWS key pair names have a 255 character limit, but let's be reasonable + if len(key_name) > 60: + # Keep prefix and full hash for uniqueness + key_name = f"{prefix}-{dir_hash}" + + return key_name.strip("-") + + +def get_ssh_key_path(key_type: str = "") -> str: + """ + Generate the local SSH key file path based on directory. + + Args: + key_type: Either "" for private key, ".pub" for public key + + Returns: + Path to SSH key file + """ + key_name = generate_ssh_key_name() + return os.path.expanduser(f"~/.ssh/{key_name}{key_type}") + + +def main(): + """Main entry point.""" + if len(sys.argv) > 1: + if sys.argv[1] == "--help" or sys.argv[1] == "-h": + print("Usage: aws_ssh_key_name.py [--simple|--path|--pubkey-path]") + print() + print("Generate a unique SSH key name based on current directory.") + print() + print("Options:") + print(" --simple Generate simple name without project context") + print(" --path Output full path to private key file") + print(" --pubkey-path Output full path to public key file") + print(" --help Show this help message") + print() + print("Examples:") + print(" Default: kdevops-aws-projectname-a1b2c3d4") + print(" Simple: kdevops-aws-a1b2c3d4") + print(" Path: /home/user/.ssh/kdevops-aws-projectname-a1b2c3d4") + print( + " Pubkey Path: /home/user/.ssh/kdevops-aws-projectname-a1b2c3d4.pub" + ) + sys.exit(0) + elif sys.argv[1] == "--simple": + print(generate_ssh_key_name(include_project=False)) + elif sys.argv[1] == "--path": + print(get_ssh_key_path()) + elif sys.argv[1] == "--pubkey-path": + print(get_ssh_key_path(".pub")) + else: + print(f"Unknown option: {sys.argv[1]}", file=sys.stderr) + sys.exit(1) + else: + print(generate_ssh_key_name()) + + +if __name__ == "__main__": + main() diff --git a/terraform/Kconfig.ssh b/terraform/Kconfig.ssh index 8a19d7c5..8f762cde 100644 --- a/terraform/Kconfig.ssh +++ b/terraform/Kconfig.ssh @@ -27,26 +27,28 @@ config TERRAFORM_SSH_CONFIG_USER config TERRAFORM_SSH_CONFIG_PUBKEY_FILE string "The ssh public key to use to log in" default "~/.ssh/kdevops_terraform_$(shell, echo $(TOPDIR_PATH) | sha256sum | cut -c1-8).pub" if TERRAFORM_LAMBDALABS + default TERRAFORM_AWS_SSH_PUBKEY_FILE if TERRAFORM_AWS && TERRAFORM_AWS_SSH_KEY_UNIQUE default "~/.ssh/kdevops_terraform.pub" help The ssh public key which will be pegged onto the systems's ~/.ssh/authorized_keys file so you can log in. - For Lambda Labs, the key path is made unique per directory by appending - the directory checksum to avoid conflicts when running multiple kdevops - instances. + For Lambda Labs and AWS (when using unique keys), the key path is made + unique per directory by appending the directory checksum to avoid + conflicts when running multiple kdevops instances. config TERRAFORM_SSH_CONFIG_PRIVKEY_FILE string "The ssh private key file for authentication" default "~/.ssh/kdevops_terraform_$(shell, echo $(TOPDIR_PATH) | sha256sum | cut -c1-8)" if TERRAFORM_LAMBDALABS + default TERRAFORM_AWS_SSH_PRIVKEY_FILE if TERRAFORM_AWS && TERRAFORM_AWS_SSH_KEY_UNIQUE default "~/.ssh/kdevops_terraform" help The ssh private key file used for authenticating to the systems. This should correspond to the public key specified above. - For Lambda Labs, the key path is made unique per directory by appending - the directory checksum to avoid conflicts when running multiple kdevops - instances. + For Lambda Labs and AWS (when using unique keys), the key path is made + unique per directory by appending the directory checksum to avoid + conflicts when running multiple kdevops instances. config TERRAFORM_SSH_CONFIG_GENKEY bool "Should we create a new random key for you?" diff --git a/terraform/aws/Kconfig b/terraform/aws/Kconfig index 557f3a08..d4015cda 100644 --- a/terraform/aws/Kconfig +++ b/terraform/aws/Kconfig @@ -11,6 +11,7 @@ source "terraform/aws/kconfigs/Kconfig.storage" endmenu menu "Identity & Access" source "terraform/aws/kconfigs/Kconfig.identity" +source "terraform/aws/kconfigs/Kconfig.ssh" endmenu endif # TERRAFORM_AWS diff --git a/terraform/aws/kconfigs/Kconfig.ssh b/terraform/aws/kconfigs/Kconfig.ssh new file mode 100644 index 00000000..3b62be48 --- /dev/null +++ b/terraform/aws/kconfigs/Kconfig.ssh @@ -0,0 +1,79 @@ +# AWS SSH Key Management Configuration + +choice + prompt "AWS SSH key management strategy" + default TERRAFORM_AWS_SSH_KEY_UNIQUE + help + Choose how SSH keys are managed for AWS instances. + + Unique key (recommended): Each kdevops project directory gets its own + SSH key, preventing conflicts when using multiple kdevops instances. + + Shared key: Use the same key name across all projects (less secure, + risk of overwriting keys and locking out existing instances). + +config TERRAFORM_AWS_SSH_KEY_UNIQUE + bool "Use unique SSH key per project directory (recommended)" + help + Generate a unique SSH key name for each kdevops project directory. + The key name will be based on the directory path, ensuring no conflicts + between different kdevops deployments. + + Example: kdevops-aws-projectname-a1b2c3d4 + + This prevents accidentally overwriting SSH keys in AWS when using + kdevops in multiple directories, which would lock you out of + existing instances. + +config TERRAFORM_AWS_SSH_KEY_SHARED + bool "Use shared SSH key name (legacy/risky)" + help + Use a fixed SSH key name that you specify. This is less secure + and can lead to accidental key overwrites if you use kdevops in + multiple directories with different keys. + + WARNING: Using the same key name with different public keys will + replace the key in AWS and lock you out of existing instances! + +endchoice + +config TERRAFORM_AWS_SSH_KEY_NAME_CUSTOM + string "Custom SSH key name (only for shared mode)" + default "kdevops_aws_key" + depends on TERRAFORM_AWS_SSH_KEY_SHARED + help + Specify the custom SSH key name to use when in shared mode. + + WARNING: If this key already exists in AWS with a different + public key, it will be replaced, potentially locking you out + of existing instances! + +config TERRAFORM_AWS_SSH_KEY_NAME + string + output yaml + default $(shell, python3 scripts/aws_ssh_key_name.py 2>/dev/null || echo "kdevops-aws-fallback") if TERRAFORM_AWS_SSH_KEY_UNIQUE + default TERRAFORM_AWS_SSH_KEY_NAME_CUSTOM if TERRAFORM_AWS_SSH_KEY_SHARED + help + The actual SSH key name that will be used in AWS. + When using unique mode, this is automatically generated based on + the current directory path. + +config TERRAFORM_AWS_SSH_PUBKEY_FILE + string + output yaml + default $(shell, python3 scripts/aws_ssh_key_name.py --pubkey-path 2>/dev/null || echo "~/.ssh/kdevops_terraform.pub") if TERRAFORM_AWS_SSH_KEY_UNIQUE + default "~/.ssh/kdevops_terraform.pub" if TERRAFORM_AWS_SSH_KEY_SHARED + help + Path to the SSH public key file. + When using unique mode, this path includes the directory-specific + key name to avoid conflicts. + +config TERRAFORM_AWS_SSH_PRIVKEY_FILE + string + output yaml + default $(shell, python3 scripts/aws_ssh_key_name.py --path 2>/dev/null || echo "~/.ssh/kdevops_terraform") if TERRAFORM_AWS_SSH_KEY_UNIQUE + default "~/.ssh/kdevops_terraform" if TERRAFORM_AWS_SSH_KEY_SHARED + help + Path to the SSH private key file. + When using unique mode, this path includes the directory-specific + key name to avoid conflicts. -- 2.51.0