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 often need to 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 those files as a part of the script, I decided to transfer them to remote hosts with 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).

Since Capistrano runs as the non-privileged user, it can’t write to the root-owned directories. Sometimes, you can sidestep that limitation by uploading a file to the user directory, and then creating a symlink to that file in your target directory.

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 (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 Sidekiq process so that it could write to the newly created log file.

Capistrano 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 workaround 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. This is 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. As a general rule, make sure a remote user Capistrano uses doesn’t have a password. Otherwise, you are 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 link tracking, no hidden pixels, no promotional emails, or other nonsense. I will only send you one email when a new article is out. Unsubscribe anytime.

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