Do you work on COVID-19 related projects? Was your business significantly affected by the pandemic?

If the answer to previous questions is yes, Hexadecimal will waive the subscription fee for you until the vaccine is discovered.

Uploading files to root directories with Capistrano

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

Authored by Jamhur Mustafayev on

When provisioning new servers, I usually need to upload configuration files to system directories such as /lib or /etc, which are owned by the root user. Given that Capistrano runs as the non-privileged user, it can’t write to those directories. You can usually sidestep that 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 file 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 the Sidekiq process 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 /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.

So all I had to do was to upload the file to the /tmp directory and then copy it over to the destination directory.

require 'securerandom'
require 'stringio'

def sudo_upload(file_path, remote_path, mode: '644', owner: 'root:root')
  sudo_exec = ->(*command) { execute(*[:sudo] + command) }

  tmp_path = "/tmp/#{SecureRandom.uuid}"

  upload!(file_path, tmp_path)

  sudo_exec.call(:mkdir, '-p', File.dirname(remote_path))
  sudo_exec.call(:mv, '-f', tmp_path, remote_path)
  sudo_exec.call(:chmod, mode, remote_path)
  sudo_exec.call(: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).

sudo_upload('sidekiq.logrotate', '/etc/logrotate.d/sidekiq')

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).