From 97dac33c53583b290e743e0b3d493b3e834ec4ed Mon Sep 17 00:00:00 2001 From: shrikantbhosaleacquia Date: Fri, 27 Feb 2026 13:28:23 +0530 Subject: [PATCH 1/5] Replace SSH with Teleport (tsh ssh) for instance access - Add TeleportConfig value object to compute proxy URL, account ID, hostname, and identity file from stack name and SSH user - Update SSHCommandBuilder to produce tsh ssh commands instead of ssh - Update Controller#ssh to pass TeleportConfig to SSHCommandBuilder - Update rotate_asg_instances SSH class to accept resources and build TeleportConfig from stack name at runtime - Fix rotate_asg_instances.rb to require individual aws-sdk gems - Add rexml to Gemfile (required by newer aws-sdk-core) - Update all affected specs for new command format and SSH constructor --- Gemfile | 1 + lib/moonshot/controller.rb | 7 +- lib/moonshot/ssh_command_builder.rb | 30 ++++---- lib/moonshot/teleport_config.rb | 57 ++++++++++++++ lib/plugins/rotate_asg_instances.rb | 3 +- lib/plugins/rotate_asg_instances/asg.rb | 2 +- lib/plugins/rotate_asg_instances/ssh.rb | 11 ++- .../plugins/rotate_asg_instances/asg_spec.rb | 17 +++-- .../plugins/rotate_asg_instances/ssh_spec.rb | 2 +- spec/moonshot/ssh_spec.rb | 25 ++++--- spec/moonshot/teleport_config_spec.rb | 75 +++++++++++++++++++ 11 files changed, 192 insertions(+), 38 deletions(-) create mode 100644 lib/moonshot/teleport_config.rb create mode 100644 spec/moonshot/teleport_config_spec.rb diff --git a/Gemfile b/Gemfile index 7fb1127a..3c6fe90e 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'rake', require: false group :test do gem 'codeclimate-test-reporter' gem 'pry' + gem 'rexml' gem 'rubocop' end diff --git a/lib/moonshot/controller.rb b/lib/moonshot/controller.rb index cc81250e..070b3edf 100644 --- a/lib/moonshot/controller.rb +++ b/lib/moonshot/controller.rb @@ -186,10 +186,13 @@ def ssh @config.ssh_instance ||= SSHTargetSelector.new( stack, asg_name: @config.ssh_auto_scaling_group_name ).choose! - cb = SSHCommandBuilder.new(@config.ssh_config, @config.ssh_instance) + teleport_config = TeleportConfig.new( + stack.name, @config.ssh_config.ssh_user, ENV['AWS_REGION'] + ) + cb = SSHCommandBuilder.new(@config.ssh_config, @config.ssh_instance, teleport_config) result = cb.build(@config.ssh_command) - warn "Opening SSH connection to #{@config.ssh_instance} (#{result.ip})..." + warn "Opening SSH connection to #{@config.ssh_instance} (#{result.host})..." exec(result.cmd) end diff --git a/lib/moonshot/ssh_command_builder.rb b/lib/moonshot/ssh_command_builder.rb index 40829e6f..1830daea 100644 --- a/lib/moonshot/ssh_command_builder.rb +++ b/lib/moonshot/ssh_command_builder.rb @@ -3,33 +3,31 @@ require 'shellwords' module Moonshot - # Create an ssh command from configuration. + # Create a tsh ssh command from configuration. class SSHCommandBuilder - Result = Struct.new(:cmd, :ip) + Result = Struct.new(:cmd, :host) - def initialize(ssh_config, instance_id) - @config = ssh_config - @instance_id = instance_id + def initialize(ssh_config, instance_id, teleport_config) + @config = ssh_config + @instance_id = instance_id + @teleport_config = teleport_config end def build(command = nil) - cmd = ['ssh', '-t'] + cmd = ['tsh', 'ssh'] cmd << @config.ssh_options if @config.ssh_options - cmd << "-i #{@config.ssh_identity_file}" if @config.ssh_identity_file - cmd << "-l #{@config.ssh_user}" if @config.ssh_user - cmd << instance_ip + cmd << "--proxy=#{@teleport_config.proxy_url}" + cmd << "-ti #{@teleport_config.identity_file}" if @teleport_config.bot_user? + cmd << '-tA' + cmd << "#{@teleport_config.ssh_user}@#{instance_host}" cmd << Shellwords.escape(command) if command - Result.new(cmd.join(' '), instance_ip) + Result.new(cmd.join(' '), instance_host) end private - def instance_ip - @instance_ip ||= Aws::EC2::Client.new - .describe_instances(instance_ids: [@instance_id]) - .reservations.first.instances.first.public_ip_address - rescue StandardError - raise "Failed to determine public IP address for instance #{@instance_id}!" + def instance_host + @instance_host ||= @teleport_config.host_for(@instance_id) end end end diff --git a/lib/moonshot/teleport_config.rb b/lib/moonshot/teleport_config.rb new file mode 100644 index 00000000..894fd90c --- /dev/null +++ b/lib/moonshot/teleport_config.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Moonshot + # Encapsulates Teleport SSH configuration derived from the stack name, + # SSH user, and AWS region. Determines the correct proxy URL, account ID, + # and identity file path for both normal users and bot users. + class TeleportConfig + PROD_ACCOUNT_ID = '546349603759' + DEV_ACCOUNT_ID = '672327909798' + BOT_USER = 'clouddatabot' + + PROD_PROXY_TEMPLATE = '%s.teleport.cloudservices.acquia.io' + DEV_PROXY = 'teleport.dev.cloudservices.acquia.io' + + PROD_IDENTITY_TEMPLATE = 'tbot-auth-%s/identity' + DEV_IDENTITY = 'tbot-auth-clouddata-node/identity' + + attr_reader :proxy_url, :account_id, :region, :ssh_user + + def initialize(stack_name, ssh_user, region) + @stack_name = stack_name.to_s + @ssh_user = ssh_user.to_s + @region = region.to_s + + if prod? + @account_id = PROD_ACCOUNT_ID + @proxy_url = format(PROD_PROXY_TEMPLATE, region: @region) + else + @account_id = DEV_ACCOUNT_ID + @proxy_url = DEV_PROXY + end + end + + def bot_user? + @ssh_user == BOT_USER + end + + # Returns the Teleport identity file path for bot users; nil for normal users. + def identity_file + return nil unless bot_user? + + prod? ? format(PROD_IDENTITY_TEMPLATE, region: @region) : DEV_IDENTITY + end + + # Constructs the Teleport node name used as SSH hostname. + # Format: .. + def host_for(instance_id) + "#{instance_id}.#{@region}.#{@account_id}" + end + + private + + def prod? + @stack_name.include?('prod') + end + end +end diff --git a/lib/plugins/rotate_asg_instances.rb b/lib/plugins/rotate_asg_instances.rb index d8a341c4..50e8b283 100644 --- a/lib/plugins/rotate_asg_instances.rb +++ b/lib/plugins/rotate_asg_instances.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'aws-sdk' +require 'aws-sdk-autoscaling' +require 'aws-sdk-ec2' module Moonshot module Plugins diff --git a/lib/plugins/rotate_asg_instances/asg.rb b/lib/plugins/rotate_asg_instances/asg.rb index 4031c9e2..0e5627ee 100644 --- a/lib/plugins/rotate_asg_instances/asg.rb +++ b/lib/plugins/rotate_asg_instances/asg.rb @@ -7,7 +7,7 @@ class ASG # rubocop:disable Metrics/ClassLength def initialize(resources) @resources = resources - @ssh = Moonshot::RotateAsgInstances::SSH.new + @ssh = Moonshot::RotateAsgInstances::SSH.new(@resources) @ilog = @resources.ilog end diff --git a/lib/plugins/rotate_asg_instances/ssh.rb b/lib/plugins/rotate_asg_instances/ssh.rb index ae29067a..1a41aee1 100644 --- a/lib/plugins/rotate_asg_instances/ssh.rb +++ b/lib/plugins/rotate_asg_instances/ssh.rb @@ -10,6 +10,10 @@ def initialize(response) end class SSH + def initialize(resources) + @resources = resources + end + # As per the standard it is raising correctly but still giving an error. def test_ssh_connection(instance_id) Retriable.retriable(base_interval: 5, tries: 3) do @@ -29,7 +33,12 @@ def exec(command, instance_id) private def build_command(command, instance_id) - cb = SSHCommandBuilder.new(Moonshot.config.ssh_config, instance_id) + teleport_config = Moonshot::TeleportConfig.new( + @resources.controller.stack.name, + Moonshot.config.ssh_config.ssh_user, + ENV['AWS_REGION'] + ) + cb = SSHCommandBuilder.new(Moonshot.config.ssh_config, instance_id, teleport_config) cb.build(command).cmd end end diff --git a/spec/moonshot/plugins/rotate_asg_instances/asg_spec.rb b/spec/moonshot/plugins/rotate_asg_instances/asg_spec.rb index 76e54af5..c00b44b5 100644 --- a/spec/moonshot/plugins/rotate_asg_instances/asg_spec.rb +++ b/spec/moonshot/plugins/rotate_asg_instances/asg_spec.rb @@ -113,26 +113,32 @@ def stub_cf_client end describe '#shutdown_instance' do - let(:public_ip_address) { '10.234.32.21' } let(:instance) { instance_double(Aws::EC2::Instance) } let(:command_builder) { Moonshot::SSHCommandBuilder } subject { super().send(:shutdown_instance, instance_id) } before(:each) do + ENV['AWS_REGION'] = 'us-east-1' moonshot_config.ssh_config.ssh_user = 'ci_user' moonshot_config.ssh_config.ssh_options = ssh_options allow(Aws::EC2::Instance).to receive(:new).and_return(instance) - allow_any_instance_of(command_builder).to receive(:instance_ip).and_return(public_ip_address) + allow(instance).to receive(:exists?).and_return(true) + allow(instance).to receive(:state).and_return({ name: 'running' }) allow(instance).to receive(:wait_until_stopped) + allow(ilog).to receive(:info) end + after(:each) do + ENV.delete('AWS_REGION') + end context 'when ssh_options are not defined' do let(:ssh_options) { nil } it 'issues a shutdown without options to the instance' do expect_any_instance_of(ssh_executor).to receive(:run).with( - "ssh -t -l #{moonshot_config.ssh_config.ssh_user} #{public_ip_address} sudo\\ shutdown\\ -h\\ now" + 'tsh ssh --proxy=teleport.dev.cloudservices.acquia.io -tA ' \ + "ci_user@#{instance_id}.us-east-1.672327909798 sudo\\ shutdown\\ -h\\ now" ) subject end @@ -143,8 +149,9 @@ def stub_cf_client it 'issues a shutdown with options to the instance' do expect_any_instance_of(ssh_executor).to receive(:run).with( - 'ssh -t -v -o UserKnownHostsFile=/dev/null ' \ - "-l ci_user #{public_ip_address} sudo\\ shutdown\\ -h\\ now" + 'tsh ssh -v -o UserKnownHostsFile=/dev/null ' \ + '--proxy=teleport.dev.cloudservices.acquia.io -tA ' \ + "ci_user@#{instance_id}.us-east-1.672327909798 sudo\\ shutdown\\ -h\\ now" ) subject end diff --git a/spec/moonshot/plugins/rotate_asg_instances/ssh_spec.rb b/spec/moonshot/plugins/rotate_asg_instances/ssh_spec.rb index f6f897ee..dd3c7eb6 100644 --- a/spec/moonshot/plugins/rotate_asg_instances/ssh_spec.rb +++ b/spec/moonshot/plugins/rotate_asg_instances/ssh_spec.rb @@ -29,7 +29,7 @@ let(:config) { resources.controller.config } - subject { described_class.new } + subject { described_class.new(resources) } describe '#test_ssh_connection' do it 'raise error if #test_ssh_connection fails' do diff --git a/spec/moonshot/ssh_spec.rb b/spec/moonshot/ssh_spec.rb index 56423c3d..4ace7bec 100644 --- a/spec/moonshot/ssh_spec.rb +++ b/spec/moonshot/ssh_spec.rb @@ -4,28 +4,33 @@ c.app_name = 'MyApp' c.environment_name = 'prod' c.ssh_config.ssh_user = 'joeuser' - c.ssh_config.ssh_identity_file = '/Users/joeuser/.ssh/thegoods.key' c.ssh_command = 'cat /etc/passwd' Moonshot::Controller.new(c) end + let(:stack_double) { instance_double(Moonshot::Stack, name: 'MyApp-prod') } + describe 'Moonshot::Controller#ssh' do before(:each) do ENV.delete('MOONSHOT_SSH_OPTIONS') + ENV['AWS_REGION'] = 'us-east-1' + allow(subject).to receive(:stack).and_return(stack_double) + end + + after(:each) do + ENV.delete('AWS_REGION') end context 'normally' do - it 'should execute an ssh command with proper parameters' do + it 'should execute a tsh ssh command with proper parameters' do ts = instance_double(Moonshot::SSHTargetSelector) expect(Moonshot::SSHTargetSelector).to receive(:new).and_return(ts) expect(ts).to receive(:choose!).and_return('i-04683a82f2dddcc04') - expect_any_instance_of(Moonshot::SSHCommandBuilder).to receive(:instance_ip).exactly(2) - .and_return('123.123.123.123') expect(subject).to receive(:exec) - .with('ssh -t -i /Users/joeuser/.ssh/thegoods.key -l joeuser 123.123.123.123 cat\ /etc/passwd') # rubocop:disable LineLength + .with('tsh ssh --proxy=us-east-1.teleport.cloudservices.acquia.io -tA joeuser@i-04683a82f2dddcc04.us-east-1.546349603759 cat\ /etc/passwd') # rubocop:disable LineLength expect { subject.ssh } - .to output("Opening SSH connection to i-04683a82f2dddcc04 (123.123.123.123)...\n") + .to output("Opening SSH connection to i-04683a82f2dddcc04 (i-04683a82f2dddcc04.us-east-1.546349603759)...\n") .to_stderr end end @@ -37,13 +42,11 @@ c end - it 'should execute an ssh command with proper parameters' do - expect_any_instance_of(Moonshot::SSHCommandBuilder).to receive(:instance_ip).exactly(2) - .and_return('123.123.123.123') + it 'should execute a tsh ssh command with proper parameters' do expect(subject).to receive(:exec) - .with('ssh -t -i /Users/joeuser/.ssh/thegoods.key -l joeuser 123.123.123.123 cat\ /etc/passwd') # rubocop:disable LineLength + .with('tsh ssh --proxy=us-east-1.teleport.cloudservices.acquia.io -tA joeuser@i-012012012012012.us-east-1.546349603759 cat\ /etc/passwd') # rubocop:disable LineLength expect { subject.ssh } - .to output("Opening SSH connection to i-012012012012012 (123.123.123.123)...\n").to_stderr + .to output("Opening SSH connection to i-012012012012012 (i-012012012012012.us-east-1.546349603759)...\n").to_stderr end end end diff --git a/spec/moonshot/teleport_config_spec.rb b/spec/moonshot/teleport_config_spec.rb new file mode 100644 index 00000000..6b8ca35e --- /dev/null +++ b/spec/moonshot/teleport_config_spec.rb @@ -0,0 +1,75 @@ +describe Moonshot::TeleportConfig do + let(:instance_id) { 'i-0036e48e43b79740f' } + + describe 'prod environment (stack name contains "prod")' do + subject { described_class.new('myapp-prod-us-east-1', 'joeuser', 'us-east-1') } + + it 'uses region-based proxy URL' do + expect(subject.proxy_url).to eq('us-east-1.teleport.cloudservices.acquia.io') + end + + it 'uses the prod account ID' do + expect(subject.account_id).to eq('546349603759') + end + + it 'builds the Teleport hostname correctly' do + expect(subject.host_for(instance_id)).to eq( + 'i-0036e48e43b79740f.us-east-1.546349603759' + ) + end + + it 'is not a bot user for a normal user' do + expect(subject.bot_user?).to be false + end + + it 'returns nil identity_file for normal user' do + expect(subject.identity_file).to be_nil + end + + context 'with bot user (clouddatabot)' do + subject { described_class.new('myapp-prod-us-east-1', 'clouddatabot', 'us-east-1') } + + it 'is identified as a bot user' do + expect(subject.bot_user?).to be true + end + + it 'uses the region-based identity file path' do + expect(subject.identity_file).to eq('tbot-auth-us-east-1/identity') + end + end + end + + describe 'dev environment (stack name does not contain "prod")' do + subject { described_class.new('myapp-dev-jsmith', 'joeuser', 'us-east-1') } + + it 'uses the shared dev proxy URL' do + expect(subject.proxy_url).to eq('teleport.dev.cloudservices.acquia.io') + end + + it 'uses the dev account ID' do + expect(subject.account_id).to eq('672327909798') + end + + it 'builds the Teleport hostname correctly' do + expect(subject.host_for(instance_id)).to eq( + 'i-0036e48e43b79740f.us-east-1.672327909798' + ) + end + + it 'is not a bot user for a normal user' do + expect(subject.bot_user?).to be false + end + + context 'with bot user (clouddatabot)' do + subject { described_class.new('myapp-dev-jsmith', 'clouddatabot', 'us-east-1') } + + it 'is identified as a bot user' do + expect(subject.bot_user?).to be true + end + + it 'uses the fixed clouddata-node identity file path' do + expect(subject.identity_file).to eq('tbot-auth-clouddata-node/identity') + end + end + end +end From 2107b767a2881a9ba93491e904cab67350a0a7b6 Mon Sep 17 00:00:00 2001 From: shrikantbhosaleacquia Date: Fri, 6 Mar 2026 16:17:02 +0530 Subject: [PATCH 2/5] CPD-11979: use correct dev identity. Change: minor Purpose: feature --- lib/moonshot/teleport_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/moonshot/teleport_config.rb b/lib/moonshot/teleport_config.rb index 894fd90c..f4158e44 100644 --- a/lib/moonshot/teleport_config.rb +++ b/lib/moonshot/teleport_config.rb @@ -13,7 +13,7 @@ class TeleportConfig DEV_PROXY = 'teleport.dev.cloudservices.acquia.io' PROD_IDENTITY_TEMPLATE = 'tbot-auth-%s/identity' - DEV_IDENTITY = 'tbot-auth-clouddata-node/identity' + DEV_IDENTITY = '/opt/machine-id/identity' attr_reader :proxy_url, :account_id, :region, :ssh_user From 818342b1738c2cf760f330e1d1cc8c102139bd79 Mon Sep 17 00:00:00 2001 From: shrikantbhosaleacquia Date: Mon, 9 Mar 2026 09:47:43 +0530 Subject: [PATCH 3/5] CPD-11979: Teleport bot ssh. Change: minor Purpose: feature --- lib/moonshot/teleport_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/moonshot/teleport_config.rb b/lib/moonshot/teleport_config.rb index f4158e44..8e4e71d2 100644 --- a/lib/moonshot/teleport_config.rb +++ b/lib/moonshot/teleport_config.rb @@ -7,7 +7,7 @@ module Moonshot class TeleportConfig PROD_ACCOUNT_ID = '546349603759' DEV_ACCOUNT_ID = '672327909798' - BOT_USER = 'clouddatabot' + BOT_USER = 'ci_user' PROD_PROXY_TEMPLATE = '%s.teleport.cloudservices.acquia.io' DEV_PROXY = 'teleport.dev.cloudservices.acquia.io' From 0ed6f094fd34747b76499776c6b7bccb955af650 Mon Sep 17 00:00:00 2001 From: shrikantbhosaleacquia Date: Mon, 9 Mar 2026 16:32:02 +0530 Subject: [PATCH 4/5] CPD-11979: ssh keys. Change: minor Purpose: feature --- lib/moonshot/ssh_command_builder.rb | 3 +-- lib/moonshot/teleport_config.rb | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/moonshot/ssh_command_builder.rb b/lib/moonshot/ssh_command_builder.rb index 1830daea..9004f2bd 100644 --- a/lib/moonshot/ssh_command_builder.rb +++ b/lib/moonshot/ssh_command_builder.rb @@ -17,8 +17,7 @@ def build(command = nil) cmd = ['tsh', 'ssh'] cmd << @config.ssh_options if @config.ssh_options cmd << "--proxy=#{@teleport_config.proxy_url}" - cmd << "-ti #{@teleport_config.identity_file}" if @teleport_config.bot_user? - cmd << '-tA' + cmd << "-i #{@teleport_config.identity_file}" if @teleport_config.bot_user? cmd << "#{@teleport_config.ssh_user}@#{instance_host}" cmd << Shellwords.escape(command) if command Result.new(cmd.join(' '), instance_host) diff --git a/lib/moonshot/teleport_config.rb b/lib/moonshot/teleport_config.rb index 8e4e71d2..b1a9015c 100644 --- a/lib/moonshot/teleport_config.rb +++ b/lib/moonshot/teleport_config.rb @@ -7,13 +7,13 @@ module Moonshot class TeleportConfig PROD_ACCOUNT_ID = '546349603759' DEV_ACCOUNT_ID = '672327909798' - BOT_USER = 'ci_user' + BOT_USER = 'clouddatabot' PROD_PROXY_TEMPLATE = '%s.teleport.cloudservices.acquia.io' DEV_PROXY = 'teleport.dev.cloudservices.acquia.io' - PROD_IDENTITY_TEMPLATE = 'tbot-auth-%s/identity' - DEV_IDENTITY = '/opt/machine-id/identity' + PROD_IDENTITY_TEMPLATE = '/opt/machine-id/%s/identity' + DEV_IDENTITY = '/opt/machine-id/dev-us-east-1/identity' attr_reader :proxy_url, :account_id, :region, :ssh_user From f5c1fa075f93b860a4863a693e67bbd4e906a417 Mon Sep 17 00:00:00 2001 From: shrikantbhosaleacquia Date: Wed, 11 Mar 2026 17:51:25 +0530 Subject: [PATCH 5/5] CPD-11979: teleport ssh. Change: minor Purpose: feature --- spec/moonshot/teleport_config_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/moonshot/teleport_config_spec.rb b/spec/moonshot/teleport_config_spec.rb index 6b8ca35e..ec538761 100644 --- a/spec/moonshot/teleport_config_spec.rb +++ b/spec/moonshot/teleport_config_spec.rb @@ -34,7 +34,7 @@ end it 'uses the region-based identity file path' do - expect(subject.identity_file).to eq('tbot-auth-us-east-1/identity') + expect(subject.identity_file).to eq('/opt/machine-id/us-east-1/identity') end end end @@ -67,8 +67,8 @@ expect(subject.bot_user?).to be true end - it 'uses the fixed clouddata-node identity file path' do - expect(subject.identity_file).to eq('tbot-auth-clouddata-node/identity') + it 'uses the dev identity file path' do + expect(subject.identity_file).to eq('/opt/machine-id/dev-us-east-1/identity') end end end