Uploading files to root-owned directories with Capistrano

Capistrano can't write to root-owned directories by default. There's a way around it.

Jamhur Mustafayev - • Updated

Sidenote: This was one of those unfindable issues that I spent more time on that I’m comfortable admitting. I decided to document and put it on the Internets for future lurkers.

As a part of provisioning new servers, I upload configuration files to system directories such as /lib or /etc. These directories are universally owned by the root user.

I provision servers with a bash script that runs over SSH. Since I couldn’t figure out how to upload files as a part of the script (in one swoop), I decided to transfer them to remote hosts using Capistrano. Capistrano is a fairly popular tool in Ruby land for server automation and deployment (if you’re coming from the Python land, Capistrano is the equivalent of Fabric, although more featureful).

Since Capistrano’s process runs as a non-privileged user, it can’t write to the root-owned directories. Sometimes, you can sidestep that limitation by uploading a file to your home directory and then creating a symlink (for example, with systemd unit files).

That worked fine until I wanted to upload Sidekiq’s logrotate configuration to the remote host. The challenge I encountered was two-fold. First, the configuration file has to live under the /etc/logrotate.d directory, which is only writable by the root user (a symlink hack won’t fly here). On top of that, the logrotate process has to run with root privileges, given that it writes to the /var/log directory and needs to restart the process so that it could write to the newly created log file.

Capistrano (Net::SCP, to be more precise) will aptly yell at you if you try to upload your file to the system directory.

Net::SCP::Error: scp: /etc/logrotate.d/sidekiq: Permission denied

Slash tmp to the rescue

To work around this limitation, I made use of the fact that the /tmp folder is world-writable on Unix-based operating systems.

$ stat -c '%U:%G %a' /tmp
root:root 1777

Trivia: 1 (or t if you’re using a human-readable form) represents a sticky bit, which is a flag that allows only the owner to move or delete files in a given directory. It’s especially useful in world-writable directories such as /tmp.

All I had to do was to upload the file to the /tmp directory and then copy it over to the destination directory. Note that I still had to use sudo to copy files over.

While we’re at it, make sure a deploy user for Capistrano doesn’t have a password. Otherwise, you’re setting yourself up for a world of pain.

require 'securerandom'
require 'stringio'

def sudo_upload(file_path, remote_path, mode: '644', owner: 'root:root')
  tmp_path = "/tmp/#{SecureRandom.uuid}"

  upload!(file_path, tmp_path)

  execute(:sudo, :mkdir, '-p', File.dirname(remote_path))
  execute(:sudo, :mv, '-f', tmp_path, remote_path)
  execute(:sudo, :chmod, mode, remote_path)
  execute(:sudo, :chown, owner, remote_path)
end

Whenever I need to upload the file, I call this method from within my rake task by passing in a file path on my local machine and a destination path on the remote host. Note that remote_path must be an absolute path, not relative.

task :upload_config_files do
  on roles(:worker) do
    sudo_upload('sidekiq.logrotate', '/etc/logrotate.d/sidekiq')
  end
end

Voilà!

Interested in behind-the-scenes of a one-person software company?

No spyware, no promotional emails, or other nonsense. I will only send you a single email when I've got something interesting to say. Unsubscribe anytime.

You can also subscribe to the Atom feed (it's like RSS, but better).