5

Background / The Problem

Because of security configurations that I absolutely CANNOT change, I must ssh onto all of my boxes a federated user, myuser.

EVERYTHING on my box that I need to deploy to is owned by deployuser and I absolutely CANNOT change this.

myuser can assume user deployuser and has all sudo privileges.

Ultimately, I want my Capistrano deployments to function as if I ssh'd in as deployuser from the get-go, so every single command is run as deployuser and from the /home/deployuser.

I can use ANY version of Capistrano I want, but right now I'm trying to do it with Capistrano 3.

The test setup

I created a simple Capistrano task to test whether I can perform an action on a file I know is owned by deployuser.

desc "Check that we can access everything"
task :check_permissions do
  on roles(:all) do |host|
    execute("whoami")
    execute("cp /tmp/file-owned-by-deploy-user /tmp/test-file")
  end
end

EDIT: The test above actually doesn't work as one might expect. I believe it's because when you manually declare something like execute('command'), it overrides any configuration that says "do this thing as X user." A better test is to just run your cap environment deploy task and see what fails.

What I've tried

sshkit-backends-netssh_global as per their docs

The first thing I tried was sshkit-backends-netssh_global, which in their documentation says they handle exactly this use case.

I added this to my Capfile, then tried adding it to deploy.rb and then even above my task definition just in case:

require 'sshkit/backends/netssh_global'

SSHKit::Backend::NetsshGlobal.configure do |config|
  config.owner        = 'deployuser'
  config.directory    = '/home/deployuser'
end

This has 0 effect whatsoever. No new error messages, just the same Permission Denied, and whoami returns myuser.

Setting :sshkit_backend in Capistrano 3

Second, I tried to set :ssh_backend from Capistrano, which I saw in this issue for sshkit-backends-netssh_global:

require 'sshkit/backends/netssh_global'

set :sshkit_backend, -> {
  SSHKit::Backend::NetsshGlobal.tap do |backend|
    backend.configure do |config|
      config.owner        = 'deployuser'
      config.directory    = '/home/deployuser'
    end
  end
}

This reveals that sshkit-backends-netssh_global is no longer compatible with the latest version of Capistrano. It's using a deprecated method checkout. So damn:

NoMethodError: undefined method `checkout' for #<SSHKit::Backend::ConnectionPool:0x007fae8340e3c0>

I tried downgrading to a lower version of Capistrano, and while that fixed the deprecation issue, it did not magically make these configuration settings take effect. I didn't dig in too deep though.

Using as 'deployuser' from SSHKit to test

I kinda feel like I'm going crazy here though because, when I try to wrap my task in as 'user' as per the SSHKit documentation, whoami gets run as deployuser while the cp command still returns Permission Denied.

So maybe my test script isn't doing what I think it's doing. Still, this command can be copy/pasted onto the server and run by appending sudo -u deployuser.

In my edit above, I say it's because I think doing an execute('command') is a little different and doesn't take your config into account - it just executes the thing. I think. So this may still work for other types of tasks (though not internal Capistrano tasks of course).

desc "Check that we can access everything"
task :check_permissions do
  on roles(:all) do
    as 'deployuser' do
      # this returns deployuser
      execute("whoami")

      # this throws permission denied
      execute("cp /tmp/file-owned-by-deploy-user /tmp/test-file")
    end
  end
end

Using SSHKit.config.command_map

Another tactic I tried, which results in the same as the above example is:

SSHKit.config.command_map = Hash.new do |hash, command|
  hash[command] = "sudo -u deployuser #{command}"
end

whoami returns deployuser, but the cp command still returns Permission Denied. Again, this might be because execute is not actually what I need to be testing here.

Related but unsolved Stackoverflow questions

Some related issues

Has anyone ever gotten this to work? Any ideas?

EDIT: Things that are kind of working (ish)

I've been working on this all day, and here's what I've got so far...and it's gross, but I got a basic deploy working.

First, while you for the most part want to append sudo -u deployuser to commands, that's not true for ALL commands. I had a couple others in here originally, but I got it down to just chmod that can't be run as my deployuser

SSHKit.config.command_map = Hash.new do |hash, command|
  skipped_commands = [:chmod]

  if skipped_commands.include?(command)
    hash[command] = command
  else
    hash[command] = "sudo -u deployuser #{command}"
  end
end

Ok so THEN I realized that this doesn't work for all Capistrano cases, because of these >> being inserted in a couple of rake tasks on the Capistrano side.

So I wrote a monkey patch for the two tasks:

namespace :deploy do

  # MONKEY PATCH OH NO
  # =======================

  # you actually have to clear the old rake task 
  # for yours to overwrite Capistrano's
  Rake::Task['deploy:set_current_revision'].clear
  desc "IT'S MINE"
  task :set_current_revision  do
    on release_roles(:all) do
      within release_path do
        execute :echo, "\"$(sudo -u deployuser git rev-parse HEAD)\" | sudo -u deployuser tee REVISION"
      end
    end
  end

  Rake::Task['deploy:log_revision'].clear
  desc "Log details of the depl
  task :log_revision do
    on release_roles(:all) do
      within releases_path do
        execute :echo, %Q{"#{revision_log_message}" | sudo -u deployuser tee #{revision_log}}
      end
    end
  end
end

Investigation is ongoing (I still have to implement the rest of the deploy), but a simple cap test_environment deploy is finishing now after these changes.

Also, here's my bundle for those who find this in the future:

Gems included by the bundle:
  * activesupport (4.2.10)
  * airbrussh (1.3.0)
  * aws-partitions (1.56.0)
  * aws-sdk-core (3.14.0)
  * aws-sdk-ec2 (1.25.0)
  * aws-sigv4 (1.0.2)
  * bundler (1.16.0)
  * capistrano (3.10.1)
  * capistrano-bundler (1.3.0)
  * capistrano-rails (1.3.1)
  * coderay (1.1.2)
  * concurrent-ruby (1.0.5)
  * dogapi (1.28.0)
  * faraday (0.14.0)
  * i18n (0.9.3)
  * jmespath (1.3.1)
  * json (1.8.6)
  * method_source (0.9.0)
  * minitest (5.11.1)
  * multi_json (1.13.1)
  * multipart-post (2.0.0)
  * net-scp (1.2.1)
  * net-ssh (4.2.0)
  * pry (0.11.3)
  * rake (12.3.0)
  * slackbot (0.0.2)
  * sshkit (1.15.1)
  * sshkit-backends-netssh_global (0.1.1)
  * thread_safe (0.3.6)
  * tzinfo (1.2.4)

Edit Again!

The author of the original plugin posted this patch yesterday which works for me in Capistrano 3.10 without any monkey patches.

CryptoFool
  • 21,719
  • 5
  • 26
  • 44

1 Answers1

1

I'm not a capistrano user, but there's an OpenSSH feature that you might be able to use for this purpose. Assumptions:

  1. Capistrano is connecting to an OpenSSH ssh server on the remote systems.
  2. The ssh sessions are being authenticated using an ssh key
  3. You can alter the remote ssh configurations (notably the myuser authorized_keys file)

If the assumptions are true, then myuser on each remote system has a .ssh/authorized_keys file containing the public key which capistrano uses to authenticate. You can add a directive to that line which forces the ssh server to run a hardcoded command when that key is used, instead of the command requested by the remote client. This allows you to insert your own handler for the requested command.

The handler can be a simple shell script. This is untested but you should get the idea:

#!/bin/sh
sudo -u deployuser $SSH_ORIGINAL_COMMAND

Store this in a file on the remote system, make it executable, and set it as the command to run in the authorized_keys file. When the ssh server invokes this command on behalf of a remote client, it will set the SSH_ORIGINAL_COMMAND environment variable to the command string requested by the remote client.

The authorized_keys file is described here. Basically, each line of the file specifies one key that can be used to authenticate. A line looks like this:

ssh-rsa AAAAB3NzaC1yc2EAA...

You can prepend a command= directive to the line:

command=/home/myuser/run-as-deployuser ssh-rsa AAAAB3NzaC1yc2EAA...

When this key is used to authenticate, the ssh server will invoke the requested command instead of invoking whatever command the client requested. The SSH_ORIGINAL_COMMAND environment variable will be set to the command requested by the client, as I said earlier.

Kenster
  • 23,465
  • 21
  • 80
  • 106
  • This is really awesome and a great idea actually. Unfortunately it won't work in the Capistrano case, because Capistrano has some built-in rake tasks that do things such as `execute :echo, "\"#{fetch(:current_revision)}\" >> REVISION"` (so the >> command won't get appended with sudo -u deployuser) But in general, this is a super sweet solution to needing to run things generically as another user. – Liz Hubertz Jan 24 '18 at 01:56