{"generator":"Bridgetown","link":[{"@attributes":{"href":"https:\/\/binarysolo.blog\/feed.xml","rel":"self","type":"application\/atom+xml"}},{"@attributes":{"href":"https:\/\/binarysolo.blog\/","rel":"alternate","type":"text\/html"}}],"updated":"2026-08-11T10:31:30+01:00","id":"https:\/\/binarysolo.blog\/feed.xml","title":"Binary Solo","subtitle":"A blog about web development, Ruby, Rails, and some other nonsense ...","entry":[{"title":"Ruby installation and management with rv","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/ruby-installation-and-management-with-rv\/","rel":"alternate","type":"text\/html","title":"Ruby installation and management with rv"}},"published":"2026-08-11T00:00:00+01:00","updated":"2026-08-11T00:00:00+01:00","id":"repo:\/\/posts.collection\/_posts\/2026-08-11-ruby-version-management-with-rv.md","content":"<p>Ruby version management can be quite fiddly. All Ruby developers need multiple Ruby versions installed, as different projects may use different Rubies.<\/p>\n\n<p>For the past few years, I\u2019ve used <a href=\"https:\/\/github.com\/rbenv\/rbenv\/\"><code class=\"highlighter-rouge\">rbenv<\/code><\/a> along with <a href=\"https:\/\/github.com\/rbenv\/ruby-build\"><code class=\"highlighter-rouge\">ruby-build<\/code><\/a> to install and manage Ruby versions. They\u2019re good tools, but I was using them more out of inertia than anything else.<\/p>\n\n<p>When it came to building <a href=\"https:\/\/codeberg.org\/ayushn21\/mimas\">Mimas<\/a>, which is a tool to deploy Ruby apps to a VPS, I started researching alternatives. Initially, I landed on <a href=\"https:\/\/github.com\/postmodern\/chruby\"><code class=\"highlighter-rouge\">chruby<\/code><\/a> and <a href=\"https:\/\/github.com\/postmodern\/ruby-install\"><code class=\"highlighter-rouge\">ruby-install<\/code><\/a>.<\/p>\n\n<p><code class=\"highlighter-rouge\">chruby<\/code> simply manipulates environment variables to ensure the correct Ruby binary is selected, contrasting with <code class=\"highlighter-rouge\">rbenv<\/code> which <em>shims<\/em> the <code class=\"highlighter-rouge\">ruby<\/code> command to intercept invokations and load the appropriate binary.<\/p>\n\n<p><code class=\"highlighter-rouge\">ruby-install<\/code> and <code class=\"highlighter-rouge\">ruby-build<\/code> both make it easier to build Ruby binaries from source, but <code class=\"highlighter-rouge\">ruby-install<\/code> doesn\u2019t need to be updated every time a new Ruby version is released.<\/p>\n\n<p><code class=\"highlighter-rouge\">chruby<\/code>\u2019s cleaner approach of using environment variables rather than a shim was a huge selling point for me, but then I discovered <a href=\"https:\/\/github.com\/spinel-coop\/rv\/\"><code class=\"highlighter-rouge\">rv<\/code><\/a>.<\/p>\n\n<h2 id=\"what-is-rv\">What is <code class=\"highlighter-rouge\">rv<\/code>?<\/h2>\n\n<p><a href=\"https:\/\/github.com\/spinel-coop\/rv\/\"><code class=\"highlighter-rouge\">rv<\/code><\/a> is a relatively new tool which aims to be a lot more than a Ruby version manager. See the project Readme for a detailed roadmap, but for now, I use it only to install and manage Ruby versions.<\/p>\n\n<p>The biggest point of difference is that <code class=\"highlighter-rouge\">rv<\/code> doesn\u2019t build Ruby from source on your machine. The project maintainers <a href=\"https:\/\/github.com\/spinel-coop\/rv-ruby\/releases\">host precompiled binaries<\/a> for all popular operating systems and CPU architectures. This means that Ruby can be installed in under 2 seconds, rather than about 15 minutes.<\/p>\n\n<aside>\n  <p>The binaries are built with YJIT and ZJIT, but not with jemalloc. You\u2019ll need to inject that yourself with the <code class=\"highlighter-rouge\">LD_PRELOAD<\/code> environment variable.<\/p><\/aside>\n\n<h2 id=\"installing-ruby-with-rv\">Installing Ruby with <code class=\"highlighter-rouge\">rv<\/code><\/h2>\n\n<p>Install the latest Ruby version with <code class=\"highlighter-rouge\">rv<\/code> using:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>rv ruby <span class=\"nb\">install <\/span>latest\n<\/code><\/pre><\/div><\/div>\n\n<p>or specify a version:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>rv ruby <span class=\"nb\">install <\/span>4.0.6\n<\/code><\/pre><\/div><\/div>\n\n<p>This will download the correct precompiled binary for your system. This makes Ruby upgrades on servers an order of magnitude easier as you don\u2019t have wait 15 minutes for the build, or worry about the additional CPU usage if it\u2019s a production web server.<\/p>\n\n<h2 id=\"switching-ruby-versions-with-rv\">Switching Ruby versions with <code class=\"highlighter-rouge\">rv<\/code><\/h2>\n\n<p>When it comes to switching between different Ruby versions, <code class=\"highlighter-rouge\">rv<\/code> takes the best bits of both <code class=\"highlighter-rouge\">rbenv<\/code> and <code class=\"highlighter-rouge\">chruby<\/code> and combines them.<\/p>\n\n<p>The <code class=\"highlighter-rouge\">rv shell<\/code> command prints out instructions to integrate <code class=\"highlighter-rouge\">rv<\/code> with your chosen shell. Once integrated, it automatically detects the active Ruby version using the <code class=\"highlighter-rouge\">.ruby-version<\/code> or <code class=\"highlighter-rouge\">.tools-versions<\/code> file in your project, and selects it using environment variables.<\/p>\n\n<p>Additionally, you can use <code class=\"highlighter-rouge\">rv run<\/code> to run commands within a Ruby environment. This can be used to run a command in a different Ruby that what\u2019s currently active, or to setup a Ruby environment where the shell integration doesn\u2019t apply (such as, when a login shell isn\u2019t available).<\/p>\n\n<p>I think this is an elegant solution that allows users to execute Ruby commands seamlessly in an interactive shell, while also providing a way to configure a Ruby environment explicitly, without <em>shimming<\/em> the <code class=\"highlighter-rouge\">ruby<\/code> command itself.<\/p>\n\n<p>See the <a href=\"https:\/\/github.com\/spinel-coop\/rv\">project readme<\/a> for detailed usage docs.<\/p>\n\n<h2 id=\"rv-in-my-workflow\"><code class=\"highlighter-rouge\">rv<\/code> in my workflow<\/h2>\n\n<p>I\u2019ve switched all Ruby management to <code class=\"highlighter-rouge\">rv<\/code> in my deployment tool: <a href=\"https:\/\/codeberg.org\/ayushn21\/mimas\">Mimas<\/a>. It simplified the server provisioning process, and reduced the time required to install a new Ruby to mere seconds.<\/p>\n\n<p>I\u2019ve also started using <code class=\"highlighter-rouge\">rv<\/code> for my local development environment. It took me just a few minutes to uninstall <code class=\"highlighter-rouge\">rbenv<\/code> and get everything working with <code class=\"highlighter-rouge\">rv<\/code>. I really can\u2019t recommend it highly enough. Go and give it a try!<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Zero-downtime Ruby deployment without Docker","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/zero-downtime-ruby-deployment-without-docker\/","rel":"alternate","type":"text\/html","title":"Zero-downtime Ruby deployment without Docker"}},"published":"2026-07-31T00:00:00+01:00","updated":"2026-07-31T00:00:00+01:00","id":"repo:\/\/posts.collection\/_posts\/2026-07-31-deploying-ruby-apps.md","content":"<aside class=\"series_index\">\n  <h2>Deploying Ruby apps to a server<\/h2>\n  <ol>\n    <li>\n      1. &nbsp;<a href=\"\/server-setup-for-ruby-web-applications\">Server setup for Ruby web applications<\/a>\n    <\/li>\n    <li>2a. <a href=\"\/serving-ruby-apps-with-caddy-as-a-reverse-proxy\">Serving Ruby apps with Caddy as a reverse proxy<\/a><\/li>\n    <li>2b. <a href=\"\/falcon-as-a-web-server-for-ruby-applications\">Falcon as a web server for Ruby applications<\/a><\/li>\n    <li>3. &nbsp;Zero-downtime Ruby deployment without Docker<\/li>\n  <\/ol>\n<\/aside>\n\n<p>In the final part of this series, we\u2019ll build upon the setup created in <a href=\"\/serving-ruby-apps-with-caddy-as-a-reverse-proxy\">Part 2a<\/a> and close the loop by deploying code from our machine to the server.<\/p>\n\n<p><a href=\"\/serving-ruby-apps-with-caddy-as-a-reverse-proxy\">Part 2a<\/a> covered the setup to serve Ruby apps, but that app was created on the server itself. To deploy code, we need a way to copy our project files to the server and reload the server process for the changes to take effect. Ideally, we also want to ensure the new code starts up correctly and is healthy, before switching traffic over to it (sometimes known as blue-green deploys).<\/p>\n\n<p>At the end of the post, I\u2019ll introduce <a href=\"https:\/\/codeberg.org\/ayushn21\/mimas\">Mimas<\/a>, which automates everything discussed in this post. Hence, this post is largely theory and doesn\u2019t contain code examples for every step. Although, this knowledge will stand you in good stead if you choose to build your own deployment tooling.<\/p>\n\n<p>Here are the steps required for zero-downtime blue-green deployments:<\/p>\n\n<ol>\n  <li>Copy the code to the server.<\/li>\n  <li>Start a new server process with the deployed code.<\/li>\n  <li>Health-check the new process.<\/li>\n  <li>Switch the reverse proxy to the new server\u2019s Unix socket.<\/li>\n  <li>Gracefully stop the old process without dropping requests.<\/li>\n<\/ol>\n\n<p>Let\u2019s take this step-by-step.<\/p>\n\n<h2 id=\"copying-code-to-the-server\">Copying code to the server<\/h2>\n\n<p>Heroku popularised the <code class=\"highlighter-rouge\">git push<\/code> technique to copy code to a server. Personally I\u2019m not a fan of this method when deploying code to a VPS, because it uses the server as a build server. <code class=\"highlighter-rouge\">git push<\/code> will send your project as-is to the server, and any artifacts that require building, such as JavaScript assets, or an entire static site, will need to be done on the server.<\/p>\n\n<p>This creates more moving parts as build dependencies such as Node need to be installed on the server. I prefer to build the site locally, or on a CI server, and push the built files to the hosting server. The most efficient way to do this is to create a GZIP archive of all required files, and then use SCP to copy that to the server.<\/p>\n\n<h2 id=\"starting-and-health-checking-new-server-process\">Starting and health-checking new server process<\/h2>\n\n<p>After we have the GZIPped code on the server, we can extract it using the <code class=\"highlighter-rouge\">tar -xf<\/code> command. To start the server process, we\u2019ll create a new systemd service for the release. The way we set up the server in <a href=\"\/server-setup-for-ruby-web-applications\">Part 1<\/a> means that this can be done without <code class=\"highlighter-rouge\">sudo<\/code> privileges.<\/p>\n\n<p>When the new systemd service has been created and started, we\u2019ll make a <code class=\"highlighter-rouge\">curl<\/code> request to the new Unix socket to ensure it is running.<\/p>\n\n<h2 id=\"switching-the-reverse-proxy-and-stopping-the-old-service\">Switching the reverse proxy and stopping the old service<\/h2>\n\n<p>When the new service has been health-checked, we can change the Caddyfile to point to the new Unix socket, and reload Caddy. Caddy does this seamlessly without downtime, and gracefully completes any requests in-flight.<\/p>\n\n<p>And finally, we need to stop and remove the old service. Both Puma and Falcon stop gracefully without dropping requests when <code class=\"highlighter-rouge\">SIGTERM<\/code> is sent to the process, so we won\u2019t lose any requests here either.<\/p>\n\n<p>And that\u2019s it, our new code will be up and running!<\/p>\n\n<h2 id=\"automation\">Automation<\/h2>\n\n<p>Doing all the above steps manually is a recipe for disaster. With so many things going on, this needs to be automated for reliability. I\u2019ve created a CLI tool called <a href=\"https:\/\/codeberg.org\/ayushn21\/mimas\">Mimas<\/a> which automates everything I\u2019ve covered in this blog series, and is optimised for single server Ruby deployments.<\/p>\n\n<h2 id=\"introducing-mimas\">Introducing Mimas<\/h2>\n\n<p>My goal with <a href=\"https:\/\/codeberg.org\/ayushn21\/mimas\">Mimas<\/a> was to create a single solution for end-to-end deployments, from securing the server, to configuring log rotation, and everything in between. These were my goals when building Mimas:<\/p>\n\n<ul>\n  <li>Securing the server, including creating the required user acccounts.<\/li>\n  <li>Installing and configuring Caddy.<\/li>\n  <li>Installing prerequisite software including Ruby.<\/li>\n  <li>Deploying multiple sites on a single server.<\/li>\n  <li>Deployment of static sites as well as Ruby apps.<\/li>\n  <li>Creating systemd services to automatically start and restart web services.<\/li>\n  <li>Zero-downtime and blue-green deploys.<\/li>\n  <li>Configure log rotation.<\/li>\n<\/ul>\n\n<p>It\u2019s still early days so there are likely to be issues. All my static sites (including the blog you\u2019re reading) are now hosted on a VPS and deployed using Mimas. I\u2019ve also created an <a href=\"https:\/\/codeberg.org\/ayushn21\/mimas-bridgetown\">official Bridgetown plugin<\/a> for it, so you can deploy your Bridgetown static or hybrid apps to a VPS easily!<\/p>\n\n<p>While it works well with static sites and simple Rack apps, Rails support is currently untested. I\u2019m planning to migrate one of my Rails apps off Render, and onto a VPS, so I will release an official Rails plugin for Mimas in the coming weeks based off that work.<\/p>\n\n<p>Mimas is written in Ruby and works by running Bash commands and scripts over SSH. There\u2019s nothing fancy going on under the hood. The code should be fairly easy to follow, and the <a href=\"https:\/\/codeberg.org\/ayushn21\/mimas#readme\">Readme<\/a> describes how it all works.<\/p>\n\n<p>Docker is far too heavy-handed for most projects, and PaaS providers can be quite expensive for small-scale and side projects. I hope that Mimas helps self-hosting become a bit more accessible as a free all-in-one solution.<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Falcon as a web server for Ruby applications","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/falcon-as-a-web-server-for-ruby-applications\/","rel":"alternate","type":"text\/html","title":"Falcon as a web server for Ruby applications"}},"published":"2026-07-24T00:00:00+01:00","updated":"2026-07-24T00:00:00+01:00","id":"repo:\/\/posts.collection\/_posts\/2026-07-24-2-falcon-as-a-web-server-for-ruby-applications.md","content":"<aside class=\"series_index\">\n  <h2>Deploying Ruby apps to a server<\/h2>\n  <ol>\n    <li>\n      1. &nbsp;<a href=\"\/server-setup-for-ruby-web-applications\">Server setup for Ruby web applications<\/a>\n    <\/li>\n    <li>2a. <a href=\"\/serving-ruby-apps-with-caddy-as-a-reverse-proxy\">Serving Ruby apps with Caddy as a reverse proxy<\/a><\/li>\n    <li>2b. Falcon as a web server for Ruby applications<\/li>\n    <li>3. &nbsp;<a href=\"\/zero-downtime-ruby-deployment-without-docker\">Zero-downtime Ruby deployment without Docker<\/a><\/li>\n  <\/ol>\n<\/aside>\n\n<p>In the <a href=\"\/serving-ruby-apps-with-caddy-as-a-reverse-proxy\">previous post<\/a>, we covered how to install and run Caddy, and then use it as a reverse proxy to serve a Rack app, and discussed why this is the conventional configuration.<\/p>\n\n<p>However, <a href=\"http:\/\/github.com\/socketry\/falcon\">Falcon<\/a> aims to change this. It has a mode called Falcon Virtual designed specifically to face the internet and perform SSL termination and SNI resolution to serve multiple sites. It also supports HTTP\/2 natively. It\u2019s designed for high I\/O throughput using Ruby Fibers making it efficient at serving static assets.<\/p>\n\n<p>That got me thinking that it would be great to serve Ruby apps using Ruby end-to-end. This post describes a strategy to serve a Rack app using Falcon, without an additional reverse proxy. And also, why that\u2019s a really bad idea and I wouldn\u2019t recommend it.<\/p>\n\n<p>To be clear, Falcon\u2019s great, but the approach of serving a Ruby web app without a reverse proxy is wrong in my opinion, and I\u2019ll explain why at the end.<\/p>\n\n<p>You\u2019ll need a domain name that you own, and can point at your server to complete this guide.<\/p>\n\n<h2 id=\"initial-setup\">Initial setup<\/h2>\n\n<p>If you worked through the previous post, and have Caddy installed on your server, stop the Caddy service as it will clash with Falcon. Falcon will need to bind to ports 80 and 443 to serve internet HTTP and HTTPS traffic.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c\"># Run as the `admin` user<\/span>\n<span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>systemctl stop caddy\n<\/code><\/pre><\/div><\/div>\n\n<p>Apart from that, this guide builds on the server setup described in <a href=\"\/server-setup-for-ruby-web-applications\">Part 1<\/a>.<\/p>\n\n<p>Ports lower than 1024 are privileged and can only be bound to by processes with elevated privileges. However, I\u2019d prefer to run Falcon as the <code class=\"highlighter-rouge\">deploy<\/code> user. That way, automated tools can manage it without needing to connect as <code class=\"highlighter-rouge\">admin<\/code>.<\/p>\n\n<h3 id=\"authbind\">Authbind<\/h3>\n\n<p>There\u2019s a utility called <code class=\"highlighter-rouge\">authbind<\/code> which enables processes run by users without elevated privileges to bind to privileged ports.<\/p>\n\n<aside>\n  <p>We didn\u2019t use <code class=\"highlighter-rouge\">authbind<\/code> to run Caddy in the previous post because it isn\u2019t compatible with it. It\u2019s got something to do with the way the Go language handles networking. If you have a good explanation for why it is incompatible, <a href=\"mailto:ayush@radioactivetoy.tech\">email me<\/a>! I\u2019d love to understand it better.<\/p><\/aside>\n\n<p>SSH to your server as <code class=\"highlighter-rouge\">admin<\/code> and install <code class=\"highlighter-rouge\">authbind<\/code>:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ssh admin@&lt;ip-address&gt;\n<span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>apt <span class=\"nb\">install <\/span>authbind\n<\/code><\/pre><\/div><\/div>\n\n<p>Then, we need to create files representing each port we\u2019d like to allow a user to bind to, and give them the <em>execute<\/em> permission on that file:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">sudo touch<\/span> \/etc\/authbind\/byport\/80\n<span class=\"nv\">$ <\/span><span class=\"nb\">sudo touch<\/span> \/etc\/authbind\/byport\/443\n<span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>setfacl <span class=\"nt\">-m<\/span> u:deploy:rx \/etc\/authbind\/byport\/80\n<span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>setfacl <span class=\"nt\">-m<\/span> u:deploy:rx \/etc\/authbind\/byport\/443\n<\/code><\/pre><\/div><\/div>\n\n<p>We create two files for the two HTTP ports and give the <code class=\"highlighter-rouge\">deploy<\/code> user the necessary permissions. Using <code class=\"highlighter-rouge\">authbind<\/code>, we can now run a process under the <code class=\"highlighter-rouge\">deploy<\/code> user that can bind to these ports.<\/p>\n\n<h3 id=\"certbot\">Certbot<\/h3>\n\n<p>While you\u2019re connected as <code class=\"highlighter-rouge\">admin<\/code>, install <code class=\"highlighter-rouge\">certbot<\/code> to issue SSL certificates, as Falcon doesn\u2019t do this automatically.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>snap <span class=\"nb\">install<\/span> <span class=\"nt\">--classic<\/span> certbot\n<\/code><\/pre><\/div><\/div>\n\n<p>Exit the SSH session and reconnect as <code class=\"highlighter-rouge\">deploy<\/code>:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ssh deploy@&lt;ip-address&gt;\n<\/code><\/pre><\/div><\/div>\n\n<h2 id=\"a-basic-rack-app\">A basic Rack app<\/h2>\n\n<aside>\n  <p>If you followed the previous post, and already have a Rack app, you can re-use that. There are some minor differences in setup, so ensure you still follow along closely.<\/p>\n<\/aside>\n\n<p>Create a new Rack app within a folder named after your domain name:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> ~\/http\/example.com\n<\/code><\/pre><\/div><\/div>\n\n<aside>\n  <p>This is also a good time to ensure you've created a DNS A record to point your domain to the server's IP address.<\/p>\n<\/aside>\n\n<p>Scaffold an empty Ruby project:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">cd<\/span> ~\/http\/example.com\n<span class=\"nv\">$ <\/span>bundle init\n<span class=\"nv\">$ <\/span><span class=\"nb\">touch <\/span>config.ru\n<span class=\"nv\">$ <\/span>bundle add rack falcon\n<\/code><\/pre><\/div><\/div>\n\n<p>Create a basic Rack app as shown below:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># config.ru<\/span>\n\n<span class=\"k\">class<\/span> <span class=\"nc\">App<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nf\">call<\/span><span class=\"p\">(<\/span><span class=\"n\">env<\/span><span class=\"p\">)<\/span>\n    <span class=\"p\">[<\/span><span class=\"mi\">200<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span> <span class=\"s2\">\"content-type\"<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"s2\">\"text\/plain\"<\/span> <span class=\"p\">},<\/span> <span class=\"p\">[<\/span><span class=\"s2\">\"It is now <\/span><span class=\"si\">#{<\/span><span class=\"no\">Time<\/span><span class=\"p\">.<\/span><span class=\"nf\">now<\/span><span class=\"p\">.<\/span><span class=\"nf\">utc<\/span><span class=\"si\">}<\/span><span class=\"s2\">\"<\/span><span class=\"p\">]]<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n\n<span class=\"n\">run<\/span> <span class=\"no\">App<\/span><span class=\"p\">.<\/span><span class=\"nf\">new<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>You\u2019ll also need to create a Falcon config file that is executable:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">touch <\/span>falcon.rb\n<span class=\"nv\">$ <\/span><span class=\"nb\">chmod<\/span> +x falcon.rb\n<\/code><\/pre><\/div><\/div>\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># falcon.rb<\/span>\n\n<span class=\"c1\">#!\/usr\/bin\/env falcon-host<\/span>\n\n<span class=\"nb\">require<\/span> <span class=\"s2\">\"falcon\/environment\/rack\"<\/span>\n\n<span class=\"n\">service<\/span> <span class=\"s2\">\"example.com\"<\/span> <span class=\"k\">do<\/span>\n  <span class=\"kp\">include<\/span> <span class=\"no\">Falcon<\/span><span class=\"o\">::<\/span><span class=\"no\">Environment<\/span><span class=\"o\">::<\/span><span class=\"no\">Rack<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<h2 id=\"bootstrapping-an-ssl-certificate\">Bootstrapping an SSL certificate<\/h2>\n\n<p>This is where things start to get tricky because Falcon doesn\u2019t automatically issue SSL certificates. We\u2019ll use a utility called <code class=\"highlighter-rouge\">certbot<\/code> to issue certificates. Certbot makes a request to <a href=\"https:\/\/letsencrypt.org\">Let\u2019s Encrypt<\/a>, which issues something called an <em>ACME challenge<\/em>. This is an HTTP request to your server for a file certbot places at a conventional location. It\u2019s used to validate that you own the domain and server.<\/p>\n\n<p>This leads to a catch-22 problem where we need an SSL certificate (even an invalid one), to be able to start Falcon so it can serve the ACME challenge used to issue the SSL certificate.<\/p>\n\n<p>We\u2019ll solve this problem by:<\/p>\n\n<ol>\n  <li>Creating a self-signed SSL certificate and point Falcon at that in the absence of a valid certificate.<\/li>\n  <li>Handle the ACME challenge HTTP request using the self-signed certificate.<\/li>\n<\/ol>\n\n<h3 id=\"issuing-a-self-signed-certificate\">Issuing a self-signed certificate<\/h3>\n\n<p>First, update the Falcon config so it looks for a live SSL certificate, and falls back to a self-signed certificate if one doesn\u2019t exist:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># falcon.rb<\/span>\n\n<span class=\"c1\">#!\/usr\/bin\/env falcon-host<\/span>\n<span class=\"c1\"># frozen_string_literal: true<\/span>\n\n<span class=\"nb\">require<\/span> <span class=\"s2\">\"falcon\/environment\/rack\"<\/span>\n<span class=\"nb\">require<\/span> <span class=\"s2\">\"falcon\/environment\/tls\"<\/span>\n\n<span class=\"n\">service<\/span> <span class=\"s2\">\"rack.forkhandles.cc\"<\/span> <span class=\"k\">do<\/span>\n  <span class=\"kp\">include<\/span> <span class=\"no\">Falcon<\/span><span class=\"o\">::<\/span><span class=\"no\">Environment<\/span><span class=\"o\">::<\/span><span class=\"no\">Rack<\/span>\n  <span class=\"kp\">include<\/span> <span class=\"no\">Falcon<\/span><span class=\"o\">::<\/span><span class=\"no\">Environment<\/span><span class=\"o\">::<\/span><span class=\"no\">TLS<\/span>\n\n  <span class=\"c1\"># Required due to https:\/\/github.com\/socketry\/falcon\/pull\/355<\/span>\n  <span class=\"n\">ssl_private_key<\/span> <span class=\"p\">{<\/span> <span class=\"no\">OpenSSL<\/span><span class=\"o\">::<\/span><span class=\"no\">PKey<\/span><span class=\"p\">.<\/span><span class=\"nf\">read<\/span><span class=\"p\">(<\/span><span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">read<\/span><span class=\"p\">(<\/span><span class=\"n\">ssl_private_key_path<\/span><span class=\"p\">))<\/span> <span class=\"p\">}<\/span>\n\n  <span class=\"c1\"># Define a location for the self-signed certs used to bootstrap a valid cert<\/span>\n  <span class=\"n\">lets_encrypt_bootstrap<\/span> <span class=\"p\">{<\/span> <span class=\"s2\">\"\/home\/deploy\/.config\/letsencrypt\/bootstrap\"<\/span> <span class=\"p\">}<\/span>\n  <span class=\"c1\"># Define a location for the live certificates<\/span>\n  <span class=\"n\">lets_encrypt_root<\/span> <span class=\"p\">{<\/span> <span class=\"s2\">\"\/home\/deploy\/.config\/letsencrypt\/live\"<\/span> <span class=\"p\">}<\/span>\n\n  <span class=\"c1\"># Fallback to the self-signed cert if a valid cert doesn't exist<\/span>\n  <span class=\"n\">ssl_certificate_path<\/span> <span class=\"p\">{<\/span>\n    <span class=\"n\">live_cert<\/span> <span class=\"o\">=<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">join<\/span><span class=\"p\">(<\/span><span class=\"n\">lets_encrypt_root<\/span><span class=\"p\">,<\/span> <span class=\"n\">authority<\/span><span class=\"p\">,<\/span> <span class=\"s2\">\"fullchain.pem\"<\/span><span class=\"p\">)<\/span>\n    <span class=\"n\">bootstrap_cert<\/span> <span class=\"o\">=<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">join<\/span><span class=\"p\">(<\/span><span class=\"n\">lets_encrypt_bootstrap<\/span><span class=\"p\">,<\/span> <span class=\"n\">authority<\/span><span class=\"p\">,<\/span> <span class=\"s2\">\"fullchain.pem\"<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">if<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">exist?<\/span><span class=\"p\">(<\/span><span class=\"n\">live_cert<\/span><span class=\"p\">)<\/span>\n      <span class=\"k\">return<\/span> <span class=\"n\">live_cert<\/span>\n    <span class=\"k\">else<\/span>\n      <span class=\"k\">return<\/span> <span class=\"n\">bootstrap_cert<\/span>\n    <span class=\"k\">end<\/span>\n  <span class=\"p\">}<\/span>\n\n  <span class=\"n\">ssl_private_key_path<\/span> <span class=\"p\">{<\/span>\n    <span class=\"n\">live_pk<\/span> <span class=\"o\">=<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">join<\/span><span class=\"p\">(<\/span><span class=\"n\">lets_encrypt_root<\/span><span class=\"p\">,<\/span> <span class=\"n\">authority<\/span><span class=\"p\">,<\/span> <span class=\"s2\">\"privkey.pem\"<\/span><span class=\"p\">)<\/span>\n    <span class=\"n\">bootstrap_pk<\/span> <span class=\"o\">=<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">join<\/span><span class=\"p\">(<\/span><span class=\"n\">lets_encrypt_bootstrap<\/span><span class=\"p\">,<\/span> <span class=\"n\">authority<\/span><span class=\"p\">,<\/span> <span class=\"s2\">\"privkey.pem\"<\/span><span class=\"p\">)<\/span>\n\n    <span class=\"k\">if<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">exist?<\/span><span class=\"p\">(<\/span><span class=\"n\">live_pk<\/span><span class=\"p\">)<\/span>\n      <span class=\"k\">return<\/span> <span class=\"n\">live_pk<\/span>\n    <span class=\"k\">else<\/span>\n      <span class=\"k\">return<\/span> <span class=\"n\">bootstrap_pk<\/span>\n    <span class=\"k\">end<\/span>\n  <span class=\"p\">}<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Create the directory structure for the certificates, and issue the self-signed certificate.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> ~\/.config\/letsencrypt\/bootstrap\n<span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> ~\/.config\/letsencrypt\/live\n<span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> ~\/.config\/letsencrypt\/log\n<span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> ~\/.config\/letsencrypt\/lib\n<\/code><\/pre><\/div><\/div>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> ~\/.config\/letsencrypt\/bootstrap\/example.com\n<span class=\"nv\">$ <\/span><span class=\"nb\">cd<\/span> ~\/.config\/letsencrypt\/bootstrap\/example.com\n<span class=\"nv\">$ <\/span>openssl req <span class=\"nt\">-x509<\/span> <span class=\"nt\">-newkey<\/span> rsa:2048 <span class=\"se\">\\<\/span>\n    <span class=\"nt\">-keyout<\/span> privkey.pem <span class=\"se\">\\<\/span>\n    <span class=\"nt\">-out<\/span> fullchain.pem <span class=\"se\">\\<\/span>\n    <span class=\"nt\">-days<\/span> 365 <span class=\"se\">\\<\/span>\n    <span class=\"nt\">-nodes<\/span> <span class=\"se\">\\<\/span>\n    <span class=\"nt\">-subj<\/span> <span class=\"s2\">\"\/CN=example.com\"<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<aside>\n  <p>Ensure you\u2019re replacing <code class=\"highlighter-rouge\">example.com<\/code> with your own domain name in all code examples.<\/p><\/aside>\n\n<h3 id=\"responding-to-the-acme-challenge\">Responding to the ACME challenge<\/h3>\n\n<p>Next, we need to handle the ACME challenge. Implement a Rack middleware to match and respond to the challenge\u2019s HTTP request:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># config.ru<\/span>\n\n<span class=\"k\">class<\/span> <span class=\"nc\">AcmeChallenge<\/span>\n  <span class=\"no\">MATCHER<\/span> <span class=\"o\">=<\/span> <span class=\"sr\">\/\\A\\\/\\.well-known\\\/acme-challenge\\\/([\\w-]+)\/<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nf\">initialize<\/span><span class=\"p\">(<\/span><span class=\"n\">app<\/span><span class=\"p\">)<\/span>\n    <span class=\"vi\">@app<\/span> <span class=\"o\">=<\/span> <span class=\"n\">app<\/span>\n  <span class=\"k\">end<\/span>\n\n  <span class=\"k\">def<\/span> <span class=\"nf\">call<\/span><span class=\"p\">(<\/span><span class=\"n\">env<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">match<\/span> <span class=\"o\">=<\/span> <span class=\"n\">env<\/span><span class=\"p\">[<\/span><span class=\"s2\">\"PATH_INFO\"<\/span><span class=\"p\">].<\/span><span class=\"nf\">match<\/span><span class=\"p\">(<\/span><span class=\"no\">AcmeChallenge<\/span><span class=\"o\">::<\/span><span class=\"no\">MATCHER<\/span><span class=\"p\">)<\/span>\n      <span class=\"n\">token<\/span> <span class=\"o\">=<\/span> <span class=\"n\">match<\/span><span class=\"p\">[<\/span><span class=\"mi\">1<\/span><span class=\"p\">]<\/span>\n      <span class=\"n\">challenge_file<\/span> <span class=\"o\">=<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">join<\/span><span class=\"p\">(<\/span><span class=\"n\">__dir__<\/span><span class=\"p\">,<\/span> <span class=\"s2\">\".well-known\"<\/span><span class=\"p\">,<\/span> <span class=\"s2\">\"acme-challenge\"<\/span><span class=\"p\">,<\/span> <span class=\"n\">token<\/span><span class=\"p\">)<\/span>\n\n      <span class=\"k\">if<\/span> <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">exist?<\/span><span class=\"p\">(<\/span><span class=\"n\">challenge_file<\/span><span class=\"p\">)<\/span>\n        <span class=\"p\">[<\/span><span class=\"mi\">200<\/span><span class=\"p\">,<\/span> <span class=\"p\">{},<\/span> <span class=\"p\">[<\/span><span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">read<\/span><span class=\"p\">(<\/span><span class=\"n\">challenge_file<\/span><span class=\"p\">)]]<\/span>\n      <span class=\"k\">else<\/span>\n        <span class=\"p\">[<\/span><span class=\"mi\">404<\/span><span class=\"p\">,<\/span> <span class=\"p\">{},<\/span> <span class=\"p\">[<\/span><span class=\"s2\">\"\"<\/span><span class=\"p\">]]<\/span>\n      <span class=\"k\">end<\/span>\n    <span class=\"k\">else<\/span>\n      <span class=\"vi\">@app<\/span><span class=\"p\">.<\/span><span class=\"nf\">call<\/span><span class=\"p\">(<\/span><span class=\"n\">env<\/span><span class=\"p\">)<\/span>\n    <span class=\"k\">end<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n\n<span class=\"k\">class<\/span> <span class=\"nc\">App<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nf\">call<\/span><span class=\"p\">(<\/span><span class=\"n\">env<\/span><span class=\"p\">)<\/span>\n    <span class=\"p\">[<\/span><span class=\"mi\">200<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span> <span class=\"s2\">\"content-type\"<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"s2\">\"text\/plain\"<\/span> <span class=\"p\">},<\/span> <span class=\"p\">[<\/span><span class=\"s2\">\"It is now <\/span><span class=\"si\">#{<\/span><span class=\"no\">Time<\/span><span class=\"p\">.<\/span><span class=\"nf\">now<\/span><span class=\"p\">.<\/span><span class=\"nf\">utc<\/span><span class=\"si\">}<\/span><span class=\"s2\">\"<\/span><span class=\"p\">]]<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n\n<span class=\"n\">use<\/span> <span class=\"no\">AcmeChallenge<\/span>\n<span class=\"n\">run<\/span> <span class=\"no\">App<\/span><span class=\"p\">.<\/span><span class=\"nf\">new<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The middleware will detect a challenge request, and read the file placed by certbot in the conventional location and return it to fulfil the challenge.<\/p>\n\n<p>We can now finally run Falcon:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">cd<\/span> ~\n<span class=\"nv\">$ <\/span>authbind <span class=\"nt\">--deep<\/span> falcon virtual \/home\/deploy\/http\/<span class=\"k\">**<\/span>\/falcon.rb\n<\/code><\/pre><\/div><\/div>\n\n<p>The glob pattern should find the <code class=\"highlighter-rouge\">falcon.rb<\/code> files for all sites on the server and start them. It will use the domain specified as the service name in <code class=\"highlighter-rouge\">falcon.rb<\/code> to match requests to the correct site.<\/p>\n\n<p>With the server running, create a second SSH connection as <code class=\"highlighter-rouge\">deploy<\/code> and run certbot:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>certbot certonly <span class=\"nt\">--webroot<\/span> <span class=\"nt\">--agree-tos<\/span> <span class=\"nt\">--email<\/span> &lt;your email&gt; <span class=\"nt\">-w<\/span> ~\/http\/example.com <span class=\"nt\">-d<\/span> example.com  <span class=\"nt\">--work-dir<\/span> ~\/.config\/letsencrypt\/lib <span class=\"nt\">--logs-dir<\/span> ~\/.config\/letsencrypt\/log <span class=\"nt\">--config-dir<\/span> ~\/.config\/letsencrypt\/\n<\/code><\/pre><\/div><\/div>\n\n<p>Certbot should issue your certificate and Falcon should automatically detect it after a few seconds. Once the certificate is issued, certbot will automatically renew it.<\/p>\n\n<p>Your Rack app should now be accessible from the internet, using your domain name, with a valid SSL certificate.<\/p>\n\n<h2 id=\"a-systemd-service\">A systemd service<\/h2>\n\n<p>You can create a <code class=\"highlighter-rouge\">systemd<\/code> service for Falcon Virtual as shown below:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>[Unit]\nDescription=Falcon Virtual\nAfter=network.target\n\n[Service]\nType=notify\nNotifyAccess=all\nUser=deploy\nExecStart=\/usr\/bin\/bash -lc 'authbind --deep falcon virtual %h\/http\/**\/falcon.rb'\nRestart=always\n\n[Install]\nWantedBy=default.target\n<\/code><\/pre><\/div><\/div>\n\n<p>Place the above contents in <code class=\"highlighter-rouge\">~\/.config\/systemd\/user\/falcon-virtual.service<\/code>, and then start the service using:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>systemctl <span class=\"nt\">--user<\/span> <span class=\"nb\">enable<\/span> <span class=\"nt\">--now<\/span> falcon-virtual\n<\/code><\/pre><\/div><\/div>\n\n<h2 id=\"why-this-setup-is-a-bad-idea\">Why this setup is a bad idea<\/h2>\n\n<p>If you followed along the previous post which used Caddy, as well as this one, you probably already understand why I don\u2019t recommend this approach.<\/p>\n\n<p>Beyond the added fiddliness to manage SSL certificates and the ACME challenge, it also makes zero-downtime deploys impossible when upgrading Ruby. Falcon Virtual starts a main process, and then forks off processes for each of your sites. While we can reload code without stopping processes, it\u2019s impossible to change the Ruby binary without stopping the main Falcon Virtual process.<\/p>\n\n<p>Also, Falcon is an application server. It\u2019s pretty damn great at being an application server. Caddy is a purpose-built web server and reverse proxy. Falcon will always be more tricky to use as a web server than Caddy.<\/p>\n\n<p>I recommend keeping your reverse proxy independent from your Ruby processes as this is the most flexible setup.<\/p>\n\n<p>In the next, and <a href=\"\/zero-downtime-ruby-deployment-without-docker\">final part<\/a> of this series, we\u2019ll look at deploying Ruby apps to a server.<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Serving Ruby apps with Caddy as a reverse proxy","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/serving-ruby-apps-with-caddy-as-a-reverse-proxy\/","rel":"alternate","type":"text\/html","title":"Serving Ruby apps with Caddy as a reverse proxy"}},"published":"2026-07-24T00:00:00+01:00","updated":"2026-07-24T00:00:00+01:00","id":"repo:\/\/posts.collection\/_posts\/2026-07-24-1-serving-ruby-apps-using-caddy.md","content":"<aside class=\"series_index\">\n  <h2>Deploying Ruby apps to a server<\/h2>\n  <ol>\n    <li>\n      1. &nbsp;<a href=\"\/server-setup-for-ruby-web-applications\">Server setup for Ruby web applications<\/a>\n    <\/li>\n    <li>2a. Serving Ruby apps with Caddy as a reverse proxy<\/li>\n    <li>2b. <a href=\"\/falcon-as-a-web-server-for-ruby-applications\">Falcon as a web server for Ruby applications<\/a><\/li>\n    <li>3. &nbsp;<a href=\"\/zero-downtime-ruby-deployment-without-docker\">Zero-downtime Ruby deployment without Docker<\/a><\/li>\n  <\/ol>\n<\/aside>\n\n<p>In this post, we\u2019ll cover how to serve a <a href=\"https:\/\/github.com\/rack\/rack\">Rack<\/a> app using <a href=\"http:\/\/caddyserver.com\">Caddy<\/a> as a reverse proxy. We won\u2019t be covering how to deploy code to the server as yet. We\u2019ll create the Rack app on the server to handle basic HTTP requests.<\/p>\n\n<p>This guide builds upon the server setup in <a href=\"\/server-setup-for-ruby-web-applications\">Part 1 of this series<\/a>.<\/p>\n\n<p>You\u2019ll need a domain name that you own, and can point at your server to follow along with this guide.<\/p>\n\n<h2 id=\"why-use-a-reverse-proxy\">Why use a reverse proxy?<\/h2>\n\n<p>Ruby apps are conventionally served through a reverse proxy. Popular Ruby app servers such as Puma aren\u2019t focused on being <em>web servers<\/em>. Their focus is on running Ruby code, and not on web server features like load balancing, SSL termination, response compression, or serving static assets. While Ruby servers may have those features, it\u2019s not their primary focus, and hence they\u2019re fiddly to use.<\/p>\n\n<p>A reverse proxy handles all the <em>web server<\/em> related tasks like SSL termination and serving static files, while handing off HTTP requests to the application server. Most popular Ruby app servers also don\u2019t support HTTP\/2, which provides a significant performance gain when serving static files.<\/p>\n\n<h2 id=\"installing-caddy\">Installing Caddy<\/h2>\n\n<p><a href=\"https:\/\/caddyserver.com\">Caddy<\/a> is a fantastic new-ish web server that can also be used as a reverse proxy. The documentation is accessible and thorough, and it can be configured with ease unlike older alternatives like Nginx.<\/p>\n\n<p>SSH to your server as the <code class=\"highlighter-rouge\">admin<\/code> user:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ssh admin@&lt;ip-address&gt;\n<\/code><\/pre><\/div><\/div>\n\n<p>Caddy can be installed using the system package manager (in this case <code class=\"highlighter-rouge\">apt<\/code>, since we\u2019re on Ubuntu). However, that also installs a systemd service, so I prefer downloading the binary to customise the service to my preference.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c\"># Update the apt source for Caddy<\/span>\n<span class=\"nb\">sudo <\/span>apt-get <span class=\"nb\">install<\/span> <span class=\"nt\">-y<\/span> debian-keyring debian-archive-keyring apt-transport-https\ncurl <span class=\"nt\">-1sLf<\/span> <span class=\"s1\">'https:\/\/dl.cloudsmith.io\/public\/caddy\/stable\/gpg.key'<\/span> | <span class=\"nb\">sudo <\/span>gpg <span class=\"nt\">--dearmor<\/span> <span class=\"nt\">--no-tty<\/span> <span class=\"nt\">--batch<\/span> <span class=\"nt\">--yes<\/span> <span class=\"nt\">-o<\/span> \/usr\/share\/keyrings\/caddy-stable-archive-keyring.gpg\ncurl <span class=\"nt\">-1sLf<\/span> <span class=\"s1\">'https:\/\/dl.cloudsmith.io\/public\/caddy\/stable\/debian.deb.txt'<\/span> | <span class=\"nb\">sudo tee<\/span> \/etc\/apt\/sources.list.d\/caddy-stable.list\n<span class=\"nb\">sudo chmod <\/span>o+r \/usr\/share\/keyrings\/caddy-stable-archive-keyring.gpg\n<span class=\"nb\">sudo chmod <\/span>o+r \/etc\/apt\/sources.list.d\/caddy-stable.list\n<span class=\"nb\">sudo <\/span>apt-get update\n\n<span class=\"c\"># Download and install the Caddy binary<\/span>\n<span class=\"nb\">mkdir<\/span> ~\/tmp\n<span class=\"nb\">cd<\/span> ~\/tmp\napt-get download caddy\n<span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> caddy-unpack\ndpkg <span class=\"nt\">-x<\/span> <span class=\"si\">$(<\/span>find caddy<span class=\"k\">*<\/span>.deb<span class=\"si\">)<\/span> ~\/tmp\/caddy-unpack\n<span class=\"nb\">sudo mv<\/span> ~\/tmp\/caddy-unpack\/usr\/bin\/caddy \/usr\/local\/bin\/caddy\n<span class=\"nb\">cd<\/span> ~\/\n<span class=\"nb\">rm<\/span> <span class=\"nt\">-rf<\/span> ~\/tmp\n<\/code><\/pre><\/div><\/div>\n\n<p>Caddy needs to bind to the standard ports 80 and 443 to serve HTTP and HTTPS traffic. These are privileged ports, meaning only processes running as root (or with elevated privileges using <code class=\"highlighter-rouge\">sudo<\/code>) can bind to them. Alternatively, a process has to be given access to bind to those ports.<\/p>\n\n<p>As such, the Caddy <code class=\"highlighter-rouge\">systemd<\/code> service has to be created using the <code class=\"highlighter-rouge\">admin<\/code> user with elevated privileges, even though it will run under the <code class=\"highlighter-rouge\">deploy<\/code> user. The service will contain a definition giving the process the releavant permissions.<\/p>\n\n<p>But, before we can install the service, let\u2019s create a Caddyfile containing a base configuration as the server can\u2019t start without a config file. The Caddyfile needs to be owned by and under the <code class=\"highlighter-rouge\">deploy<\/code> user\u2019s home directory so it can be modified without connecting as <code class=\"highlighter-rouge\">admin<\/code>.<\/p>\n\n<p>Instead of reconnecting as <code class=\"highlighter-rouge\">deploy<\/code>, create the config files under the <code class=\"highlighter-rouge\">deploy<\/code> user using <code class=\"highlighter-rouge\">sudo<\/code>, and change their ownership:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nb\">sudo mkdir<\/span> <span class=\"nt\">-p<\/span> \/home\/deploy\/http\n<span class=\"nb\">sudo touch<\/span> \/home\/deploy\/http\/Caddyfile\n<span class=\"nb\">sudo touch<\/span> \/home\/deploy\/http\/caddy.global\n\n<span class=\"nb\">sudo cat<\/span> <span class=\"o\">&lt;&lt;<\/span><span class=\"no\">STRING<\/span><span class=\"sh\"> &gt;&gt; \/home\/deploy\/http\/caddy.global\n{\n  servers {\n    protocols h1 h2\n  }\n}\n<\/span><span class=\"no\">STRING\n\n<\/span><span class=\"nb\">sudo cat<\/span> <span class=\"o\">&lt;&lt;<\/span><span class=\"no\">STRING<\/span><span class=\"sh\"> &gt;&gt; \/home\/deploy\/http\/Caddyfile\nimport caddy.global\nimport *\/Caddyfile\n<\/span><span class=\"no\">STRING\n\n<\/span><span class=\"nb\">sudo chown <\/span>deploy:deploy <span class=\"nt\">-R<\/span> \/home\/deploy\n<\/code><\/pre><\/div><\/div>\n\n<p><code class=\"highlighter-rouge\">caddy.global<\/code> contains global settings that apply across all websites on the server. I restrict it to HTTP\/1 and HTTP\/2 because I find HTTP\/3 causes weird connection issues in browsers in between deploys.<\/p>\n\n<p>The <code class=\"highlighter-rouge\">Caddyfile<\/code> is the main configuration file we\u2019ll pass into Caddy. It loads the global settings and the individual Caddyfiles of all our websites.<\/p>\n\n<p>The Caddy configuration is held in memory, so if you deploy a new site and need Caddy to pick up it\u2019s config, you\u2019ll need to <code class=\"highlighter-rouge\">reload<\/code> the service. Caddy handles this gracefully with zero downtime.<\/p>\n\n<p>Then, give the <code class=\"highlighter-rouge\">deploy<\/code> user just enough <code class=\"highlighter-rouge\">sudo<\/code> access to reload and monitor the caddy service.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nb\">sudo touch<\/span> \/etc\/sudoers.d\/deploy\n\n<span class=\"nb\">cat<\/span> <span class=\"o\">&lt;&lt;<\/span> <span class=\"no\">STRING<\/span><span class=\"sh\"> &gt;&gt; \/etc\/sudoers.d\/deploy\ndeploy <\/span><span class=\"si\">$(<\/span><span class=\"nb\">cat<\/span> \/etc\/hostname<span class=\"si\">)<\/span><span class=\"sh\">=(root) NOPASSWD: \/bin\/systemctl reload caddy\ndeploy <\/span><span class=\"si\">$(<\/span><span class=\"nb\">cat<\/span> \/etc\/hostname<span class=\"si\">)<\/span><span class=\"sh\">=(root) NOPASSWD: \/bin\/systemctl status caddy\n<\/span><span class=\"no\">STRING\n<\/span><\/code><\/pre><\/div><\/div>\n\n<p>Next, create the Caddy service file:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nb\">sudo cat<\/span> <span class=\"o\">&lt;&lt;<\/span> <span class=\"no\">STRING<\/span><span class=\"sh\"> &gt; \/etc\/systemd\/system\/caddy.service\n[Unit]\nDescription=Caddy\nDocumentation=https:\/\/caddyserver.com\/docs\/\nAfter=network.target network-online.target\nRequires=network-online.target\n\n[Service]\nType=notify\nUser=deploy\nExecStart=\/usr\/local\/bin\/caddy run --environ --config \/home\/deploy\/http\/Caddyfile\nExecReload=\/usr\/local\/bin\/caddy reload --config \/home\/deploy\/http\/Caddyfile --force\nTimeoutStopSec=5s\nLimitNOFILE=1048576\nPrivateTmp=true\nProtectSystem=full\nAmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE\n\n[Install]\nWantedBy=multi-user.target\n<\/span><span class=\"no\">STRING\n<\/span><\/code><\/pre><\/div><\/div>\n\n<p>The <code class=\"highlighter-rouge\">AmbientCapabilities<\/code> gives it access to bind to ports 80 and 443 to serve HTTP and HTTPS requests. This access can only be given by a user with elevated privileges, which is why we can\u2019t create this service within the <code class=\"highlighter-rouge\">deploy<\/code> user.<\/p>\n\n<p>The Ruby app\u2019s service will be created in the <code class=\"highlighter-rouge\">deploy<\/code> user as it doesn\u2019t need elevated privileges.<\/p>\n\n<p>Enable and start the service:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>systemctl daemon-reload <span class=\"o\">&amp;&amp;<\/span> <span class=\"nb\">sudo <\/span>systemctl <span class=\"nb\">enable<\/span> <span class=\"nt\">--now<\/span> caddy\n<\/code><\/pre><\/div><\/div>\n\n<p>Caddy should have started running. Verify it with:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>systemctl status caddy\n<span class=\"nv\">$ <\/span>curl <span class=\"nt\">-kv<\/span> http:\/\/localhost:2019\/config\/\n<\/code><\/pre><\/div><\/div>\n\n<p>The Caddyfile is blank, so it won\u2019t serve anything yet. Let\u2019s create an app for it to serve.<\/p>\n\n<h2 id=\"a-basic-rack-app\">A basic Rack app<\/h2>\n\n<p>Disconnect your SSH session and reconnect as the <code class=\"highlighter-rouge\">deploy<\/code> user:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ssh deploy@&lt;ip-address&gt;\n<\/code><\/pre><\/div><\/div>\n\n<p>Create a folder for your site named after your domain name:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> ~\/http\/example.com\n<\/code><\/pre><\/div><\/div>\n\n<aside>\n  <p>This is also a good time to ensure you've created a DNS A record to point your domain at the server's IP address.<\/p>\n<\/aside>\n\n<p>Create a Caddyfile specific to this site, and scaffold an empty Ruby project:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">cd<\/span> ~\/http\/example.com\n<span class=\"nv\">$ <\/span><span class=\"nb\">touch <\/span>Caddyfile\n<span class=\"nv\">$ <\/span>bundle init\n<span class=\"nv\">$ <\/span><span class=\"nb\">touch <\/span>config.ru\n<span class=\"nv\">$ <\/span>bundle add rack falcon\n<\/code><\/pre><\/div><\/div>\n\n<p>We\u2019re using Falcon as our server here, but you can use Puma if you wish.<\/p>\n\n<p>Open up a text editor (I use <code class=\"highlighter-rouge\">vim<\/code>), and create a basic Rack app as shown below:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># config.ru<\/span>\n\n<span class=\"k\">class<\/span> <span class=\"nc\">App<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nf\">call<\/span><span class=\"p\">(<\/span><span class=\"n\">env<\/span><span class=\"p\">)<\/span>\n    <span class=\"p\">[<\/span><span class=\"mi\">200<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span> <span class=\"s2\">\"content-type\"<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"s2\">\"text\/plain\"<\/span> <span class=\"p\">},<\/span> <span class=\"p\">[<\/span><span class=\"s2\">\"It is now <\/span><span class=\"si\">#{<\/span><span class=\"no\">Time<\/span><span class=\"p\">.<\/span><span class=\"nf\">now<\/span><span class=\"p\">.<\/span><span class=\"nf\">utc<\/span><span class=\"si\">}<\/span><span class=\"s2\">\"<\/span><span class=\"p\">]]<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n\n<span class=\"n\">run<\/span> <span class=\"no\">App<\/span><span class=\"p\">.<\/span><span class=\"nf\">new<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>You\u2019ll also need to create a Falcon config file:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">touch <\/span>falcon.rb\n<\/code><\/pre><\/div><\/div>\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># falcon.rb<\/span>\n\n<span class=\"c1\">#!\/usr\/bin\/env falcon-host<\/span>\n\n<span class=\"nb\">require<\/span> <span class=\"s2\">\"falcon\/environment\/rack\"<\/span>\n\n<span class=\"n\">service<\/span> <span class=\"s2\">\"example.com\"<\/span> <span class=\"k\">do<\/span>\n  <span class=\"kp\">include<\/span> <span class=\"no\">Falcon<\/span><span class=\"o\">::<\/span><span class=\"no\">Environment<\/span><span class=\"o\">::<\/span><span class=\"no\">Rack<\/span>\n\n  <span class=\"n\">rackup_path<\/span>   <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">expand_path<\/span><span class=\"p\">(<\/span><span class=\"s2\">\".\/config.ru\"<\/span><span class=\"p\">,<\/span> <span class=\"n\">root<\/span><span class=\"p\">)<\/span>\n  <span class=\"n\">ipc_path<\/span>      <span class=\"no\">File<\/span><span class=\"p\">.<\/span><span class=\"nf\">expand_path<\/span><span class=\"p\">(<\/span><span class=\"s2\">\".\/application.sock\"<\/span><span class=\"p\">,<\/span> <span class=\"n\">root<\/span><span class=\"p\">)<\/span>\n  <span class=\"n\">scheme<\/span>        <span class=\"s2\">\"http\"<\/span>\n  <span class=\"n\">protocol<\/span>      <span class=\"no\">Async<\/span><span class=\"o\">::<\/span><span class=\"no\">HTTP<\/span><span class=\"o\">::<\/span><span class=\"no\">Protocol<\/span><span class=\"o\">::<\/span><span class=\"no\">HTTP<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The Falcon configuration sets binds the app to a Unix socket at <code class=\"highlighter-rouge\">application.sock<\/code>. It also sets the scheme as <code class=\"highlighter-rouge\">http<\/code> since Caddy handles SSL termination, and the <code class=\"highlighter-rouge\">protocol<\/code> declaration automatically chooses between HTTP\/1.1 and HTTP\/2 depending on the request.<\/p>\n\n<p>We run the app on a Unix socket to avoid port clashes with other services. When there are multiple web apps, or multiple instances of an app running on a single server, managing port numbers between all of them would be impossible.<\/p>\n\n<p>At this point, you can do a quick test to check the app can be run:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>bundle <span class=\"nb\">exec <\/span>falcon host falcon.rb\n<\/code><\/pre><\/div><\/div>\n\n<p>Create a second SSH session, change to the site\u2019s directory, and use curl to check if the server is running:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>curl <span class=\"nt\">--unix-socket<\/span> application.sock http:\/\/localhost\/\n<\/code><\/pre><\/div><\/div>\n\n<p>If the request succeeds, close the second SSH session, go back to the first one, and use <code class=\"highlighter-rouge\">Ctrl+C<\/code> to stop the server. Now that we know it works, we can create a <code class=\"highlighter-rouge\">systemd<\/code> service to start it automatically and keep it running.<\/p>\n\n<h2 id=\"running-the-app-using-a-systemd-service\">Running the app using a systemd service<\/h2>\n\n<p>Create the service file:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">mkdir<\/span> <span class=\"nt\">-p<\/span> ~\/.config\/systemd\/user\n<span class=\"nv\">$ <\/span><span class=\"nb\">touch<\/span> ~\/.config\/systemd\/user\/example.com.service\n<\/code><\/pre><\/div><\/div>\n\n<p>Add the below code block to the service file:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>[Unit]\nDescription=example.com Web Service\nAfter=network.target\n\n[Service]\nType=notify\nNotifyAccess=all\nUser=deploy\nWorkingDirectory=\/home\/deploy\/http\/example.com\nExecStart=\/usr\/bin\/env bash -lc 'bundle exec falcon host falcon.rb'\nKillMode=mixed\nTimeoutStopSec=60\nRestart=always\n\n[Install]\nWantedBy=default.target\n<\/code><\/pre><\/div><\/div>\n\n<p>A couple of things to note in the above service:<\/p>\n\n<ol>\n  <li><code class=\"highlighter-rouge\">bash -lc<\/code> is used to start the server. This loads <code class=\"highlighter-rouge\">~\/.profile<\/code> ensuring <code class=\"highlighter-rouge\">chruby<\/code> loads and the environment variables pointing to the Ruby binary and Gems are set.<\/li>\n  <li><code class=\"highlighter-rouge\">KillMode=mixed<\/code> sends <code class=\"highlighter-rouge\">SIGTERM<\/code> only to the main process when the service is stopped. This allows the server to stop gracefully without dropping requests. If it fails to exit within the timeout, then <code class=\"highlighter-rouge\">SIGKILL<\/code> will be sent to the main process as well as child processes.<\/li>\n<\/ol>\n\n<p>Start the service:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>systemctl <span class=\"nt\">--user<\/span> <span class=\"nb\">enable<\/span> <span class=\"nt\">--now<\/span> example.com\n<\/code><\/pre><\/div><\/div>\n\n<p>Check that it is running using <code class=\"highlighter-rouge\">curl<\/code> as we did earlier. We can\u2019t access this via the internet as yet because we haven\u2019t pointed Caddy to it.<\/p>\n\n<h2 id=\"configuring-the-caddy-reverse-proxy\">Configuring the Caddy reverse proxy<\/h2>\n\n<p>Configure Caddy to act as a reverse proxy to the Unix socket for your domain name:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code># example.com\/Caddyfile\n\nexample.com {\n  reverse_proxy unix\/\/\/home\/deploy\/http\/example.com\/application.sock\n}\n<\/code><\/pre><\/div><\/div>\n\n<p>Reload Caddy:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>systemctl reload caddy\n<\/code><\/pre><\/div><\/div>\n\n<p>You can do this as the <code class=\"highlighter-rouge\">deploy<\/code> user despite it being a <code class=\"highlighter-rouge\">sudo<\/code> command because we gave it specific access for it earlier in this guide.<\/p>\n\n<p>Your Rack app should now be available at your domain!<\/p>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>Rack underpins every popular Ruby web framework and server, and hence you should be able to use this same techniqe to serve apps built with Rails, Hanami, Roda, or any other framework.<\/p>\n\n<p>In the next part, we\u2019ll look at an alternate approach where Falcon itself is internet facing, and used for SSL termination and as a reverse proxy for multiple websites. We\u2019ll also discuss why I don\u2019t recommend that approach.<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Server setup for Ruby web applications","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/server-setup-for-ruby-web-applications\/","rel":"alternate","type":"text\/html","title":"Server setup for Ruby web applications"}},"published":"2026-07-23T00:00:00+01:00","updated":"2026-07-23T00:00:00+01:00","id":"repo:\/\/posts.collection\/_posts\/2026-07-23-vps-setup-for-ruby-web-applications.md","content":"<aside class=\"series_index\">\n  <h2>Deploying Ruby apps to a server<\/h2>\n  <ol>\n    <li>\n      1. &nbsp;Server setup for Ruby web applications\n    <\/li>\n    <li>2a. <a href=\"\/serving-ruby-apps-with-caddy-as-a-reverse-proxy\">Serving Ruby apps with Caddy as a reverse proxy<\/a><\/li>\n    <li>2b. <a href=\"\/falcon-as-a-web-server-for-ruby-applications\">Falcon as a web server for Ruby applications<\/a><\/li>\n    <li>3. &nbsp;<a href=\"\/zero-downtime-ruby-deployment-without-docker\">Zero-downtime Ruby deployment without Docker<\/a><\/li>\n  <\/ol>\n<\/aside>\n\n<p>The ease of using Heroku and other similar platforms means a generation of developers, including me, have never really needed to setup and manage servers during our career. As such, I was a bit afraid of server admin and self-hosting my apps.<\/p>\n\n<p>Sadly, with the enshittifcation of PaaS providers, and an upward trend in pricing, I could no longer ignore the benefits of self-hosting. For example, <a href=\"https:\/\/scattergun.email\">Scattergun<\/a> is one of my side projects with exactly $0 ARR. I\u2019m paying $49\/month to host it on Render.com. It needs 1 web service, 1 job runner, a Postgres database, and a Redis instance. All very basic for a Rails app, and at that level, I can probably run it on a single \u20ac5.49\/month Hetzner box.<\/p>\n\n<p>The trade-off for the lower price is that I need to do more ops work myself, but again, for small to medium sized apps, I feel this is a worthy trade-off with an efficient deployment setup.<\/p>\n\n<p>Tools like Kamal have emerged recently to simplify self-hosting. However, nothing tackles the problem end-to-end, from securing the server, to log rotation, to database backups etc.<\/p>\n\n<p>Kamal also uses Docker, which I\u2019m not keen on as it\u2019s a bit of a behemoth. It makes a lot of sense when you\u2019re deploying to a fleet of servers, but I\u2019ll be deploying to a single server and I feel it\u2019s overkill for that.<\/p>\n\n<p>This is the first of a series of posts where I\u2019ll desribe my self-hosting setup for Ruby (and Rails) applications that covers the process of deploying apps to a single server end-to-end.<\/p>\n\n<p>The first step towards self-hosting is to secure the server. In this post I\u2019ll describe the security steps I like to take when I create a new VPS, and the software I install to run Ruby apps.<\/p>\n\n<h2 id=\"creating-a-vps\">Creating a VPS<\/h2>\n\n<p>Create a VPS using your preferred cloud provider. I personally use <a href=\"https:\/\/hetzner.com\">Hetzner<\/a>. This guide is based on Ubuntu 26.04, but you should be able to adapt it for other distros.<\/p>\n\n<p>Ensure you configure an SSH key for <code class=\"highlighter-rouge\">root<\/code> access, rather than using a password. Here\u2019s <a href=\"\/managing-ssh-key-pairs-for-server-access\/\">my blog post on managing SSH keys for server access<\/a>.<\/p>\n\n<h2 id=\"upgrade-packages-and-enable-firewall\">Upgrade packages and enable firewall<\/h2>\n\n<p>SSH to your server as <code class=\"highlighter-rouge\">root<\/code>.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ssh root@&lt;ip-address&gt;\n<\/code><\/pre><\/div><\/div>\n\n<p>Then, update all system packages:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>apt-get update <span class=\"o\">&amp;&amp;<\/span> apt-get <span class=\"nt\">-y<\/span> upgrade\n<\/code><\/pre><\/div><\/div>\n\n<p>Next, enable the firewall so only SSH, HTTP, and HTTPS ports are open:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ufw default deny incoming\n<span class=\"nv\">$ <\/span>ufw default allow outgoing\n<span class=\"nv\">$ <\/span>ufw allow OpenSSH\n<span class=\"nv\">$ <\/span>ufw allow http\n<span class=\"nv\">$ <\/span>ufw allow https\n<span class=\"nv\">$ <\/span>ufw <span class=\"nt\">--force<\/span> <span class=\"nb\">enable<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<h2 id=\"creating-users-and-restricting-permissions\">Creating users and restricting permissions<\/h2>\n\n<p>I create two users on my web servers: <code class=\"highlighter-rouge\">admin<\/code> and <code class=\"highlighter-rouge\">deploy<\/code>. <code class=\"highlighter-rouge\">admin<\/code> will have <code class=\"highlighter-rouge\">sudo<\/code> access, and will only be used for the initial server setup. After a server is provisioned, it will never again be used by an automated tool. <code class=\"highlighter-rouge\">root<\/code> login is completely disabled.<\/p>\n\n<p>Automated tools will connect as <code class=\"highlighter-rouge\">deploy<\/code> to deploy the apps and run related tasks. It will not have <code class=\"highlighter-rouge\">sudo<\/code> access as I\u2019m not comfortable with automated tools running <code class=\"highlighter-rouge\">sudo<\/code> commands, or with web applications running under a user that can gain elevated privileges.<\/p>\n\n<p>Password login is insecure, and maintaing passwords for multiple servers is unwieldy, so we\u2019ll restrict it and <a href=\"\/managing-ssh-key-pairs-for-server-access\/\">use SSH keys instead<\/a>.<\/p>\n\n<p>Create the admin user:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c\"># Add a new user, set the default shell,<\/span>\n<span class=\"c\"># and create a home directory<\/span>\n<span class=\"nv\">$ <\/span>useradd admin <span class=\"nt\">-s<\/span> \/bin\/bash <span class=\"nt\">-m<\/span>\n\n<span class=\"c\"># Disable password login<\/span>\n<span class=\"nv\">$ <\/span>passwd <span class=\"nt\">-ld<\/span> admin\n\n<span class=\"c\"># Sync the private keys to allow SSH access<\/span>\n<span class=\"nv\">$ <\/span>rsync <span class=\"nt\">--archive<\/span> <span class=\"nt\">--chown<\/span><span class=\"o\">=<\/span>admin:admin ~\/.ssh \/home\/admin\n\n<span class=\"c\"># Allow passwordless sudo for `admin`<\/span>\n<span class=\"nv\">$ <\/span><span class=\"nb\">echo<\/span> <span class=\"s2\">\"%admin <\/span><span class=\"si\">$(<\/span><span class=\"nb\">cat<\/span> \/etc\/hostname<span class=\"si\">)<\/span><span class=\"s2\">=(root) NOPASSWD:ALL\"<\/span> <span class=\"o\">&gt;<\/span> \/etc\/sudoers.d\/admin\n\n<span class=\"c\"># Validate the modified sudoers file<\/span>\n<span class=\"nv\">$ <\/span>visudo <span class=\"nt\">-c<\/span> <span class=\"nt\">-f<\/span> \/etc\/sudoers.d\/admin\n<\/code><\/pre><\/div><\/div>\n<aside>\n  <p>While passwordless sudo access might seem counter-intuitive, it allows\nadministrators and scripts to intentionally elevate privileges when required. We can\u2019t require a password because we\u2019ve disabled password logins in favour of SSH keys.<\/p>\n\n<p>This is another reason why automated deployment tools will only ever connect as <code class=\"highlighter-rouge\">deploy<\/code>.<\/p><\/aside>\n\n<p>Disable root login completely as the <code class=\"highlighter-rouge\">admin<\/code> user can now use <code class=\"highlighter-rouge\">sudo<\/code> to gain root privileges. Password authentication is also disabled at system level in the same file.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">touch<\/span> \/etc\/ssh\/sshd_config.d\/user.conf\n<span class=\"nv\">$ <\/span><span class=\"nb\">cat<\/span> <span class=\"o\">&lt;&lt;<\/span> <span class=\"no\">STRING<\/span><span class=\"sh\"> &gt;&gt; \/etc\/ssh\/sshd_config.d\/user.conf\nPermitRootLogin no\nPasswordAuthentication no\n<\/span><span class=\"no\">STRING\n\n<\/span><span class=\"nv\">$ <\/span>systemctl restart ssh\n<span class=\"nv\">$ <\/span>passwd <span class=\"nt\">-ld<\/span> root\n<\/code><\/pre><\/div><\/div>\n\n<p>Next, create the <code class=\"highlighter-rouge\">deploy<\/code> user:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>useradd deploy <span class=\"nt\">-s<\/span> \/bin\/bash <span class=\"nt\">-m<\/span>\n<span class=\"nv\">$ <\/span>passwd <span class=\"nt\">-ld<\/span> deploy\n\n<span class=\"c\"># Enable lingering so the user's systemd services keep running<\/span>\n<span class=\"c\"># when the user doesn't have an active session.<\/span>\n<span class=\"nv\">$ <\/span>loginctl enable-linger deploy\n\n<span class=\"nv\">$ <\/span>rsync <span class=\"nt\">--archive<\/span> <span class=\"nt\">--chown<\/span><span class=\"o\">=<\/span>deploy:deploy ~\/.ssh \/home\/deploy\n<\/code><\/pre><\/div><\/div>\n\n<p>Since the <code class=\"highlighter-rouge\">deploy<\/code> user won\u2019t have <code class=\"highlighter-rouge\">sudo<\/code> access, it can\u2019t create <a href=\"https:\/\/en.wikipedia.org\/wiki\/Systemd\">systemd<\/a> services in the system-wide location: <code class=\"highlighter-rouge\">\/etc\/systemd\/system<\/code>. As such, we\u2019ll create them in the user-specific location: <code class=\"highlighter-rouge\">~\/.config\/systemd\/user\/<\/code>. By default, these services start when the user starts a session and stop after the session concludes. Enabling <em>lingering<\/em> as demonstrated above ensures services start on system boot and keep running even when the user isn\u2019t connected.<\/p>\n\n<h2 id=\"additional-security-steps\">Additional security steps<\/h2>\n\n<p>The below snippet updates several kernel and system settings to harden it against network and file-system attack vectors. I\u2019ve adapted the settings from <a href=\"https:\/\/gist.github.com\/rameerez\/238927b78f9108a71a77aed34208de11\">this script by Javier Ramirez<\/a>. I won\u2019t get into the details as there\u2019s quite a lot, but you can search for each individual setting to learn more about it if you\u2019re curious.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">cat<\/span> <span class=\"o\">&lt;&lt;<\/span><span class=\"no\">STRING<\/span><span class=\"sh\"> &gt; \/etc\/sysctl.d\/network-security.conf\n# Disable ICMP redirects.\nnet.ipv4.conf.all.send_redirects = 0\nnet.ipv4.conf.default.send_redirects = 0\nnet.ipv4.conf.all.accept_redirects = 0\nnet.ipv4.conf.default.accept_redirects = 0\nnet.ipv6.conf.all.accept_redirects = 0\nnet.ipv6.conf.default.accept_redirects = 0\n\n# ICMP noise hygiene.\nnet.ipv4.icmp_echo_ignore_broadcasts = 1\nnet.ipv4.icmp_ignore_bogus_error_responses = 1\n\n# Drop spoofed\/martian packets at the door.\nnet.ipv4.conf.all.rp_filter = 1\nnet.ipv4.conf.default.rp_filter = 1\nnet.ipv4.conf.all.accept_source_route = 0\nnet.ipv4.conf.default.accept_source_route = 0\nnet.ipv6.conf.all.accept_source_route = 0\nnet.ipv6.conf.default.accept_source_route = 0\n\n# SYN-flood resilience as the server handles\n# port 80\/443 traffic from the internet.\nnet.ipv4.tcp_syncookies = 1\nnet.ipv4.tcp_synack_retries = 2\nnet.ipv4.tcp_syn_retries = 5\n\n# Hide kernel internals from unprivileged eyes (kernel pointer leaks, dmesg,\n# perf side channels) and disable unprivileged eBPF.\nkernel.kptr_restrict = 2\nkernel.dmesg_restrict = 1\nkernel.perf_event_paranoid = 3\nkernel.unprivileged_bpf_disabled = 1\nnet.core.bpf_jit_harden = 2\n\n# Only processes with CAP_SYS_PTRACE may ptrace.\nkernel.yama.ptrace_scope = 2\n\n# Block runtime kernel replacement via kexec (one-way until reboot).\n# CAVEAT: if you ever set up kdump crash dumps, remove this line first.\nkernel.kexec_load_disabled = 1\n\n# Filesystem link\/dump hardening.\nfs.protected_hardlinks = 1\nfs.protected_symlinks = 1\nfs.suid_dumpable = 0\n\n# Turn off Martian logging\nnet.ipv4.conf.all.log_martians = 0\nnet.ipv4.conf.default.log_martians = 0\n<\/span><span class=\"no\">STRING\n\n<\/span><span class=\"nv\">$ <\/span>sysctl <span class=\"nt\">--system<\/span> <span class=\"o\">&gt;<\/span>\/dev\/null\n<\/code><\/pre><\/div><\/div>\n\n<p>For additional security, install <code class=\"highlighter-rouge\">fail2ban<\/code> to block IPs trying to brute force SSH access to the server:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>apt-get <span class=\"nb\">install <\/span>fail2ban <span class=\"nt\">-y<\/span>\n<span class=\"nv\">$ <\/span><span class=\"nb\">touch<\/span> \/etc\/fail2ban\/jail.local\n<span class=\"nv\">$ <\/span><span class=\"nb\">cat<\/span> <span class=\"o\">&lt;&lt;<\/span> <span class=\"no\">STRING<\/span><span class=\"sh\"> &gt;&gt; \/etc\/fail2ban\/jail.local\n[DEFAULT]\nbantime = 1d\nfindtime = 10m\nmaxretry = 5\n\n[sshd]\nenabled = true\n<\/span><span class=\"no\">STRING\n\n<\/span><span class=\"nv\">$ <\/span>systemctl <span class=\"nb\">enable <\/span>fail2ban\n<\/code><\/pre><\/div><\/div>\n\n<p>And finally, install <code class=\"highlighter-rouge\">molly-guard<\/code> to prevent accidental reboots. This requires the user to enter the server\u2019s hostname to reboot it, or run <code class=\"highlighter-rouge\">reboot.no-molly-guard<\/code> to bypass the check. It adds an additional layer of scrutiny to avoid accidental reboots.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>apt-get <span class=\"nb\">install <\/span>molly-guard\n<\/code><\/pre><\/div><\/div>\n\n<p>Reboot the server for changes to take effect:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>reboot.no-molly-guard\n<\/code><\/pre><\/div><\/div>\n\n<p><code class=\"highlighter-rouge\">root<\/code> connections to the server will no longer be possible after reboot. Use <code class=\"highlighter-rouge\">admin<\/code> instead. You may wish to add some additional layers of security, but this provides a good baseline.<\/p>\n\n<p>Here\u2019s a script with all the above steps: <a href=\"https:\/\/gist.github.com\/ayushn21\/41b1ceb4fc2651d7d849c432c0a4fa05\">https:\/\/gist.github.com\/ayushn21\/41b1ceb4fc2651d7d849c432c0a4fa05<\/a>.<\/p>\n\n<p>Next, we\u2019ll install the prerequisite software to run Ruby apps.<\/p>\n\n<h2 id=\"installing-ruby-prerequisites\">Installing Ruby prerequisites<\/h2>\n\n<p>Re-connect to your server as the <code class=\"highlighter-rouge\">admin<\/code> user:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ssh admin@&lt;ip-address&gt;\n<\/code><\/pre><\/div><\/div>\n\n<p>Install Ruby\u2019s prerequisites:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>apt-get <span class=\"nb\">install <\/span>curl jq xz-utils build-essential zlib1g-dev <span class=\"se\">\\<\/span>\n  libyaml-dev libssl-dev libncurses-dev libffi-dev <span class=\"se\">\\<\/span>\n  rustc libjemalloc-dev <span class=\"nt\">-y<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The above list includes <code class=\"highlighter-rouge\">rustc<\/code> to enable <a href=\"https:\/\/docs.ruby-lang.org\/en\/master\/jit\/yjit_md.html\">YJIT<\/a>, and <code class=\"highlighter-rouge\">libjemalloc-dev<\/code> to use <code class=\"highlighter-rouge\">jemalloc<\/code> for enhanced memory management.<\/p>\n\n<aside>\n  <p>I got the list of prerequisites from the <a href=\"https:\/\/github.com\/postmodern\/ruby-install\/blob\/master\/share\/ruby-install\/ruby\/dependencies.sh\"><code class=\"highlighter-rouge\">ruby-install<\/code><\/a> repo.<\/p><\/aside>\n\n<p>I prefer using <code class=\"highlighter-rouge\">ruby-install<\/code> and <code class=\"highlighter-rouge\">chruby<\/code> to install and manage multiple Ruby versions, rather than alternatives such as <code class=\"highlighter-rouge\">rbenv<\/code>. I feel it has a cleaner approach as it works by configuring environment variables, rather than using shims to intercept commands and direct them to the correct Ruby binary.<\/p>\n\n<p>Install both utilities:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c\"># Install ruby-install<\/span>\n<span class=\"nv\">$ <\/span>wget https:\/\/github.com\/postmodern\/ruby-install\/releases\/download\/v0.10.2\/ruby-install-0.10.2.tar.gz\n<span class=\"nv\">$ <\/span><span class=\"nb\">tar<\/span> <span class=\"nt\">-xzvf<\/span> ruby-install-0.10.2.tar.gz\n<span class=\"nv\">$ <\/span><span class=\"nb\">cd <\/span>ruby-install-0.10.2\/\n<span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>make <span class=\"nb\">install<\/span>\n<span class=\"nv\">$ <\/span><span class=\"nb\">cd<\/span> ..\n<span class=\"nv\">$ <\/span><span class=\"nb\">rm <\/span>ruby-install-0.10.2.tar.gz\n<span class=\"nv\">$ <\/span><span class=\"nb\">rm<\/span> <span class=\"nt\">-rf<\/span> ruby-install-0.10.2\/\n\n<span class=\"c\"># Install chruby<\/span>\n<span class=\"nv\">$ <\/span>wget https:\/\/github.com\/postmodern\/chruby\/releases\/download\/v0.3.9\/chruby-0.3.9.tar.gz\n<span class=\"nv\">$ <\/span><span class=\"nb\">tar<\/span> <span class=\"nt\">-xzvf<\/span> chruby-0.3.9.tar.gz\n<span class=\"nv\">$ <\/span><span class=\"nb\">cd <\/span>chruby-0.3.9\/\n<span class=\"nv\">$ <\/span><span class=\"nb\">sudo <\/span>make <span class=\"nb\">install<\/span>\n<span class=\"nv\">$ <\/span><span class=\"nb\">cd<\/span> ..\n<span class=\"nv\">$ <\/span><span class=\"nb\">rm <\/span>chruby-0.3.9.tar.gz\n<span class=\"nv\">$ <\/span><span class=\"nb\">rm<\/span> <span class=\"nt\">-rf<\/span> chruby-0.3.9\/\n<\/code><\/pre><\/div><\/div>\n\n<p>Disconnect from the <code class=\"highlighter-rouge\">admin user<\/code>:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">exit<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<h2 id=\"installing-ruby\">Installing Ruby<\/h2>\n\n<p>Install Ruby as <code class=\"highlighter-rouge\">deploy<\/code> as that\u2019s the user the apps will be run under.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ssh deploy@&lt;ip-address&gt;\n<\/code><\/pre><\/div><\/div>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span>ruby-install <span class=\"nt\">--no-install-deps<\/span> <span class=\"nt\">--cleanup<\/span> ruby <span class=\"nt\">--<\/span> <span class=\"nt\">--enable-yjit<\/span> <span class=\"nt\">--with-jemalloc<\/span> <span class=\"nt\">--disable-install-rdoc<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Building Ruby will take a bit of time. After it completes, enable <code class=\"highlighter-rouge\">chruby<\/code> by sourcing its scripts in your <code class=\"highlighter-rouge\">.profile<\/code>:<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">cat<\/span> <span class=\"o\">&lt;&lt;<\/span><span class=\"no\">EOF<\/span><span class=\"sh\"> &gt;&gt; ~\/.profile\n\n# Use chruby to auto-switch Ruby versions\nsource \/usr\/local\/share\/chruby\/chruby.sh\nsource \/usr\/local\/share\/chruby\/auto.sh\n<\/span><span class=\"no\">EOF\n<\/span><\/code><\/pre><\/div><\/div>\n\n<p>Finally, source the scripts into your current session, set a default Ruby, and setup bundler.<\/p>\n\n<div class=\"language-shell highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ <\/span><span class=\"nb\">source<\/span> \/usr\/local\/share\/chruby\/chruby.sh\n<span class=\"nv\">$ <\/span><span class=\"nb\">source<\/span> \/usr\/local\/share\/chruby\/auto.sh\n\n<span class=\"nv\">$ <\/span><span class=\"nb\">rm<\/span> <span class=\"nt\">-f<\/span> <span class=\"nt\">--<\/span> ~\/.ruby-version\n<span class=\"nv\">$ <\/span><span class=\"nb\">echo<\/span> <span class=\"si\">$(<\/span>chruby<span class=\"si\">)<\/span> <span class=\"o\">&gt;<\/span> ~\/.ruby-version\n<span class=\"nv\">$ <\/span><span class=\"nb\">echo<\/span> <span class=\"s1\">'gem: --no-document'<\/span> <span class=\"o\">&gt;&gt;<\/span> ~\/.gemrc\n\n<span class=\"nv\">$ <\/span>gem update <span class=\"nt\">--system<\/span>\n<span class=\"nv\">$ <\/span>gem <span class=\"nb\">install <\/span>bundler\n<span class=\"nv\">$ <\/span>bundle config <span class=\"nb\">set<\/span> <span class=\"nt\">--global<\/span> without <span class=\"s1\">'development test'<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The server is now ready to serve Ruby applications. In the next couple of posts, we\u2019ll explore two different deployment setups.<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Is your Action Cable connection secure when using Turbo?","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/is-your-action-cable-connection-secure-when-using-turbo\/","rel":"alternate","type":"text\/html","title":"Is your Action Cable connection secure when using Turbo?"}},"published":"2025-02-10T00:00:00+00:00","updated":"2025-02-10T00:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2025-02-10-turbo-action-cable-security.md","content":"<p>When Turbo 7 and its Rails integration <a href=\"http:\/\/github.com\/hotwired\/turbo-rails\"><code class=\"highlighter-rouge\">turbo-rails<\/code><\/a> dropped in late 2020, it became easier than ever to use Action Cable. With just a few lines of code, you could broadcast updates to the client over WebSockets and build all kinds of real-time interactivity with ease.<\/p>\n\n<p>This ease does come with a trade-off. There\u2019s a security pitfall that\u2019s all too easy to fall into. By default, WebSocket connections aren\u2019t protected by any authentication or authorisation checks. There\u2019s also no mention in the Turbo documentation that I\u2019m aware of explaining how the developer can add their own checks.<\/p>\n\n<p>Before discussing the issue itself, let\u2019s back up a bit and look at how the WebSocket connection is made.<\/p>\n\n<h2 id=\"creating-the-websocket-connection-for-streaming-turbo-streams\">Creating the WebSocket connection for streaming Turbo Streams<\/h2>\n\n<p>In the view, we\u2019d establish a connection and subscribe to a channel using an Active Model\/Active Record object, or a string:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_stream_from<\/span> <span class=\"vi\">@conversation<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_stream_from<\/span> <span class=\"s2\">\"my_channel\"<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>This will render as:<\/p>\n\n<div class=\"language-html highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nt\">&lt;turbo-cable-stream-source<\/span>\n  <span class=\"na\">channel=<\/span><span class=\"s\">\"Turbo::StreamsChannel\"<\/span>\n  <span class=\"na\">signed-stream-name=<\/span><span class=\"s\">\"...\"<\/span><span class=\"nt\">&gt;<\/span>\n<span class=\"nt\">&lt;\/turbo-cable-stream-source&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>We could then broadcast to this channel from the server:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"no\">Turbo<\/span><span class=\"o\">::<\/span><span class=\"no\">StreamsChannel<\/span><span class=\"p\">.<\/span><span class=\"nf\">broadcast_action_to<\/span><span class=\"p\">(<\/span>\n  <span class=\"vi\">@conversation<\/span><span class=\"p\">,<\/span>\n  <span class=\"ss\">action: :append<\/span><span class=\"p\">,<\/span>\n  <span class=\"ss\">target: <\/span><span class=\"n\">dom_id<\/span><span class=\"p\">(<\/span><span class=\"vi\">@conversation<\/span><span class=\"p\">,<\/span> <span class=\"ss\">:messages<\/span><span class=\"p\">),<\/span>\n  <span class=\"ss\">partial: <\/span><span class=\"s2\">\"messages\/message\"<\/span>\n<span class=\"p\">)<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The <em>signed stream name<\/em> is generated from the <code class=\"highlighter-rouge\">@conversation<\/code> object or the string.<\/p>\n\n<h3 id=\"a-closer-look-at-stream-names\">A closer look at stream names<\/h3>\n\n<p>Any type of object can used to generate the stream name as long as it responds to <code class=\"highlighter-rouge\">to_gid_param<\/code> or <code class=\"highlighter-rouge\">to_param<\/code>. Hence, it\u2019s usually an Active Record object, string, or symbol.<\/p>\n\n<p><code class=\"highlighter-rouge\">to_gid_param<\/code> generates the <a href=\"https:\/\/github.com\/rails\/globalid\" target=\"_blank\">Global ID<\/a> for a model and then Base64 encodes it. For strings, <code class=\"highlighter-rouge\">to_param<\/code> returns the object itself. For symbols, it returns the object\u2019s string representation.<\/p>\n\n<p>The stream name for a <code class=\"highlighter-rouge\">Conversation<\/code> object would look something like:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>Z2lkOi8vcGlhenphL0NvbnZlcnNhdGlvbi8x\n<\/code><\/pre><\/div><\/div>\n\n<p>Base64 decoding the above value will reveal the object\u2019s Global ID:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"o\">&gt;&gt;<\/span> <span class=\"n\">stream<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">\"Z2lkOi8vcGlhenphL0NvbnZlcnNhdGlvbi8x\"<\/span>\n<span class=\"o\">&gt;&gt;<\/span> <span class=\"no\">Base64<\/span><span class=\"p\">.<\/span><span class=\"nf\">urlsafe_decode64<\/span><span class=\"p\">(<\/span><span class=\"n\">stream<\/span><span class=\"p\">)<\/span>\n\n<span class=\"c1\"># =&gt; \"gid:\/\/piazza\/Conversation\/1\"<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The stream name is run through a <a href=\"https:\/\/api.rubyonrails.org\/classes\/ActiveSupport\/MessageVerifier.html\"><code class=\"highlighter-rouge\">MessageVerifier<\/code><\/a> to obtain the <em>signed stream name<\/em> which is rendered in the HTML attribute. This ensures the stream name can\u2019t be tampered with on the client.<\/p>\n\n<p>Using the signed stream name and channel name, the custom <code class=\"highlighter-rouge\">&lt;turbo-cable-stream-source&gt;<\/code> element establishes a WebSocket connection using Action Cable and subscribes to the desired channel.<\/p>\n\n<h2 id=\"the-security-vulnerability\">The security vulnerability<\/h2>\n\n<p>The signed stream names themselves cannot be spoofed easily as they are securely signed with your app\u2019s <code class=\"highlighter-rouge\">secret_key_base<\/code>.<\/p>\n\n<p>The security issue is when a malicious user gets their hands on a signed stream name for a resource they shouldn\u2019t have access to. This could happen in a variety of ways, but the most likely scenario is that they had access to a resource, but that access was revoked. Then, in the absence of any additional checks, they use the signed stream name to continue receiving messages broadcast to that channel.<\/p>\n\n<p>Let\u2019s look at some code to understand this further. This is the default <code class=\"highlighter-rouge\">ApplicationCable::Connection<\/code> class which is the entry point for all WebSocket connections.<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">module<\/span> <span class=\"nn\">ApplicationCable<\/span>\n  <span class=\"k\">class<\/span> <span class=\"nc\">Connection<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">ActionCable<\/span><span class=\"o\">::<\/span><span class=\"no\">Connection<\/span><span class=\"o\">::<\/span><span class=\"no\">Base<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>There\u2019s no logic in there to do any checks on the incoming connections. It will automatically accept all WebSocket connections. The next step is that the connection will try to subscribe to a channel. The default <code class=\"highlighter-rouge\">Turbo::StreamsChannel<\/code> in the <code class=\"highlighter-rouge\">turbo-rails<\/code> gem looks like:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class<\/span> <span class=\"nc\">Turbo::StreamsChannel<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">ActionCable<\/span><span class=\"o\">::<\/span><span class=\"no\">Channel<\/span><span class=\"o\">::<\/span><span class=\"no\">Base<\/span>\n  <span class=\"kp\">extend<\/span> <span class=\"no\">Turbo<\/span><span class=\"o\">::<\/span><span class=\"no\">Streams<\/span><span class=\"o\">::<\/span><span class=\"no\">Broadcasts<\/span><span class=\"p\">,<\/span> <span class=\"no\">Turbo<\/span><span class=\"o\">::<\/span><span class=\"no\">Streams<\/span><span class=\"o\">::<\/span><span class=\"no\">StreamName<\/span>\n  <span class=\"kp\">include<\/span> <span class=\"no\">Turbo<\/span><span class=\"o\">::<\/span><span class=\"no\">Streams<\/span><span class=\"o\">::<\/span><span class=\"no\">StreamName<\/span><span class=\"o\">::<\/span><span class=\"no\">ClassMethods<\/span>\n\n  <span class=\"k\">def<\/span> <span class=\"nf\">subscribed<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">stream_name<\/span> <span class=\"o\">=<\/span> <span class=\"n\">verified_stream_name_from_params<\/span>\n      <span class=\"n\">stream_from<\/span> <span class=\"n\">stream_name<\/span>\n    <span class=\"k\">else<\/span>\n      <span class=\"n\">reject<\/span>\n    <span class=\"k\">end<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>This does a check to verify the stream name hasn\u2019t been tampered with, but then accepts the subscription without any additional authorisation checks.<\/p>\n\n<p>Depending on the messages your app broadcasts, this might not be a big problem. I\u2019ll also say that the scenario of a malicious user getting the markup they need is pretty unlikely. But in the event that they do, there\u2019s absolutely no mitigation in place to stop them. It\u2019s a vulnerability that makes me nervous.<\/p>\n\n<p>I don\u2019t think we should accept any connections without the appropriate authentication and authorisation checks. This applies to WebSockets just as much as it applies to HTTP.<\/p>\n\n<h2 id=\"fixing-the-vulnerability\">Fixing the vulnerability<\/h2>\n\n<p>We can address this issue by doing an authentication check before the connection is ever established, and then doing an authorisation check when subscribing to a channel.<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">module<\/span> <span class=\"nn\">ApplicationCable<\/span>\n  <span class=\"k\">class<\/span> <span class=\"nc\">Connection<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">ActionCable<\/span><span class=\"o\">::<\/span><span class=\"no\">Connection<\/span><span class=\"o\">::<\/span><span class=\"no\">Base<\/span>\n    <span class=\"n\">identified_by<\/span> <span class=\"ss\">:user<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"nf\">connect<\/span>\n      <span class=\"n\">user_session<\/span> <span class=\"o\">=<\/span> <span class=\"n\">authenticate_session<\/span>\n      <span class=\"n\">reject_unauthorized_connection<\/span> <span class=\"k\">unless<\/span> <span class=\"n\">user_session<\/span><span class=\"p\">.<\/span><span class=\"nf\">present?<\/span>\n\n      <span class=\"nb\">self<\/span><span class=\"p\">.<\/span><span class=\"nf\">user<\/span> <span class=\"o\">=<\/span> <span class=\"n\">user_session<\/span><span class=\"p\">.<\/span><span class=\"nf\">user<\/span>\n    <span class=\"k\">end<\/span>\n\n    <span class=\"kp\">private<\/span>\n      <span class=\"k\">def<\/span> <span class=\"nf\">authenticate_session<\/span>\n        <span class=\"n\">session<\/span> <span class=\"o\">=<\/span> <span class=\"n\">cookies<\/span><span class=\"p\">.<\/span><span class=\"nf\">encrypted<\/span><span class=\"p\">[<\/span><span class=\"ss\">:_piazza_session<\/span><span class=\"p\">]<\/span>\n        <span class=\"c1\"># Authentication logic here<\/span>\n      <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Depending on how your app\u2019s authentication is setup, the above code will look slightly different. The general idea is to use a cookie to authenticate the user just as you would an HTTP request. The Rails <code class=\"highlighter-rouge\">session<\/code> variable isn\u2019t available in this context so we need to read the session cookie manually.<\/p>\n\n<p>The parameters passed to <code class=\"highlighter-rouge\">identified_by<\/code> are analogous to <code class=\"highlighter-rouge\">ActiveSupport::CurrentAttributes<\/code>, but in the context of an Action Cable connection. We can set global attributes for the connection in those params.<\/p>\n\n<p>Next, we create a custom channel and add our authorization logic:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class<\/span> <span class=\"nc\">ConversationsChannel<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">Turbo<\/span><span class=\"o\">::<\/span><span class=\"no\">StreamsChannel<\/span>\n\n  <span class=\"k\">def<\/span> <span class=\"nf\">subscribed<\/span>\n    <span class=\"k\">if<\/span> <span class=\"n\">authorized?<\/span>\n      <span class=\"n\">stream_from<\/span> <span class=\"n\">stream_name<\/span>\n    <span class=\"k\">else<\/span>\n      <span class=\"n\">reject<\/span>\n    <span class=\"k\">end<\/span>\n  <span class=\"k\">end<\/span>\n\n  <span class=\"kp\">private<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"nf\">stream_name<\/span>\n      <span class=\"vi\">@stream_name<\/span> <span class=\"o\">||=<\/span> <span class=\"n\">verified_stream_name_from_params<\/span>\n    <span class=\"k\">end<\/span>\n\n    <span class=\"k\">def<\/span> <span class=\"nf\">authorized?<\/span>\n      <span class=\"c1\"># authorize self.user<\/span>\n    <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>And finally, we need to specify this channel in our markup:<\/p>\n\n<div class=\"language-erb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">&lt;%=<\/span> <span class=\"n\">turbo_stream_from<\/span> <span class=\"vi\">@conversation<\/span><span class=\"p\">,<\/span> <span class=\"ss\">channel: <\/span><span class=\"no\">ConversationsChannel<\/span> <span class=\"cp\">%&gt;<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>That\u2019s it! Now, we\u2019re doing an authentication and authorisation check before accepting all WebSocket connections.<\/p>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>Rails and Turbo both needed to implement the WebSocket mechanism in a generic way. There\u2019s just no way to anticipate the intricacies of an app\u2019s authentication and authorisation logic to provide any default setup.<\/p>\n\n<p>However, I do wish this gotcha was made more obvious. It can be a rather sizeable security hole in the right context. Every app I\u2019ve encountered in my freelance work has been missing these checks.<\/p>\n\n<p>As unlikely as such an attack might be, the fix is easy enough that I think it\u2019s absolutely worth doing. I believe that application security is something everyone should take very seriously.<\/p>\n\n<hr \/>\n\n<p class=\"text-center\">\n  For more useful tips like this, and to level up your Rails skills, buy <a href=\"https:\/\/railsandhotwirecodex.com\" target=\"_blank\">The Rails and Hotwire Codex.<\/a>\n<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Indie SaaS and boring tax stuff","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/indie-saas-and-boring-tax-stuff\/","rel":"alternate","type":"text\/html","title":"Indie SaaS and boring tax stuff"}},"published":"2025-01-21T00:00:00+00:00","updated":"2025-01-21T00:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2025-01-21-indie-saas-and-boring-tax-stuff.md","content":"<p>After about 3 years in hibernation I\u2019ve finally <a href=\"https:\/\/blog.scattergun.email\/scattergun-is-open-for-new-users-once-again\/\" target=\"_blank\">resurrected<\/a> my app <a href=\"https:\/\/scattergun.email\" target=\"_blank\">Scattergun<\/a>. It\u2019d been on pause for so long due to the joys of EU VAT (also known as Sales Tax or GST in other countries).<\/p>\n\n<h2 id=\"why-does-this-affect-me\">Why does this affect me?<\/h2>\n\n<p>I\u2019m based in the UK which is (sadly) no longer in the EU, but these laws affect everyone selling to consumers the EU, no matter where you\u2019re located.<\/p>\n\n<p>When selling <a href=\"https:\/\/www.gov.uk\/guidance\/the-vat-rules-if-you-supply-digital-services-to-private-consumers#definition-of-electronically-supplied\" target=\"_blank\">digital services<\/a> to consumers in the EU, you need to charge them VAT at <strong><em>their<\/em><\/strong> local rate, and then file a return with the EU and pay the appropriate tax.<\/p>\n\n<p>A number of US states have similar laws and differing rates of sales tax, but the key difference in the US is most states have a minimum sales threshold below which you don\u2019t need to worry about this stuff.<\/p>\n\n<p>The EU has no such threshold because \u2026 well, fuck common sense, that\u2019s why. The moment you have a single sale to an EU consumer, you\u2019re liable to collect and remit VAT. Other countries like Australia also <a href=\"https:\/\/www.ato.gov.au\/businesses-and-organisations\/international-tax-for-business\/gst-on-imported-goods-and-services\/gst-on-imported-services-and-digital-products\" target=\"_blank\">have similar laws<\/a>.<\/p>\n\n<p>This is completely unworkable for indie SaaS developers and small businesses. The admin overhead is just way too much, especially if you\u2019re outside the EU. Accountants outside the EU are also unlikely to know their way around this system. The reason I got all the way to the initial launch of Scattergun without understanding the tax implications was that my previous accountant just had no idea about this stuff.<\/p>\n\n<p>I think most indie developers outside the EU are not aware of these laws and small enough to fly under the radar. That also proves that the law in its current state is impossible to enforce given the sheer scale of tech services provided worldwide. I really wonder how moronic the politicians needed to be to pass such legislation.<\/p>\n\n<h2 id=\"stripe-vs-paddle-vs-something-else\">Stripe vs Paddle vs something else<\/h2>\n\n<p>Scattergun used <a href=\"https:\/\/stripe.com\" target=\"_blank\">Stripe<\/a> for billing for the initial launch. It was a no-brainer, I mean, everyone uses Stripe right? When using Stripe, the consumer is buying directly from you (the developer), meaning the tax liability is on you. After understanding the EU tax laws, I couldn\u2019t continue using Stripe, so I migrated billing over to <a href=\"https:\/\/paddle.com\" target=\"_blank\">Paddle<\/a>.<\/p>\n\n<p>Paddle is a <em>Merchant of Record<\/em>, meaning the consumer technically buys the subscription or product from them, and then Paddle has a B2B relationship with you, the developer. They take on all the tax liability so you don\u2019t have to worry about it. It also means they need to run more stringent checks on you and your business before you\u2019re approved to use their platform, since they take all the risk. That\u2019s a few extra hoops to jump through before you can get up and running.<\/p>\n\n<p><a href=\"https:\/\/lemonsqueezy.com\" target=\"_blank\">LemonSqueezy<\/a> is an alternate option for a Merchant of Record, and they were recently acquired by Stripe.<\/p>\n\n<p>I see a lot of nonsense on social media calling Paddle\/LemonSqueezy a Stripe wrapper, and claiming it\u2019s pointless using them instead of Stripe. They do use Stripe under the hood but they\u2019re <strong>so<\/strong> much more than a simple wrapper. The tax shield benefit is absolutely massive.<\/p>\n\n<h2 id=\"what-should-i-use-for-my-indie-app\">What should I use for my indie app?<\/h2>\n\n<p>I\u2019m going be a typical developer and say: <em>it depends<\/em>. But, you\u2019re almost definitely better off going with a Merchant of Record instead of Stripe.<\/p>\n\n<p>My guess is most indie developers outside the EU who use Stripe are non-compliant with EU VAT laws. They\u2019re just too small to attract attention. This might not always be the case.<\/p>\n\n<p>It took a very on-the-ball accountant to point this issue out to me, and I\u2019d rather avoid a tax problem completely than just hope I don\u2019t get caught.<\/p>\n\n<p>One can hope the EU implements some common sense thresholds below which you don\u2019t need to worry about place-of-supply VAT, but I wouldn\u2019t hold my breath.<\/p>\n\n<p>Further reading:<\/p>\n<ul>\n  <li><a href=\"https:\/\/www.lemonsqueezy.com\/blog\/eu-saas-vat\" target=\"_blank\">https:\/\/www.lemonsqueezy.com\/blog\/eu-saas-vat<\/a><\/li>\n  <li><a href=\"https:\/\/benfoster.io\/blog\/vat-for-saas-businesses-in-the-eu\" target=\"_blank\">https:\/\/benfoster.io\/blog\/vat-for-saas-businesses-in-the-eu<\/a><\/li>\n  <li><a href=\"https:\/\/www.gov.uk\/guidance\/the-vat-rules-if-you-supply-digital-services-to-private-consumers#the-place-of-supply-of-digital-services\" target=\"_blank\">https:\/\/www.gov.uk\/guidance\/the-vat-rules-if-you-supply-digital-services-to-private-consumers#the-place-of-supply-of-digital-services<\/a><\/li>\n<\/ul>","author":{"name":"Ayush Newatia"}},{"title":"Upskilling as a professional programmer","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/upskilling-as-a-professional-programmer\/","rel":"alternate","type":"text\/html","title":"Upskilling as a professional programmer"}},"published":"2025-01-13T00:00:00+00:00","updated":"2025-01-13T00:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2025-01-13-upskilling-as-a-professional-programmer.md","content":"<p>I got my first programming job right out of university in 2014. I worked full-time for 5 years before taking a sabbatical, and then going freelance in 2021.<\/p>\n\n<p>Freelancing offers me a cadence of client work followed by unbooked time. I usually work with long term clients so a bit of downtime after a contract ends is very welcome.<\/p>\n\n<p>This allows me an opportunity to upskill. It\u2019s also when I blog a whole lot more about stuff I\u2019ve learned. When I worked a full-time job, I wrote a grand total of 1 blog post in 5 years.<\/p>\n\n<p>The job was, as most tech jobs, fairly relentless and repetitive. Constant 2-week sprints delivering pretty standard stuff with the odd interesting project thrown in. After a couple of years of getting to grips with the basics, I plateaued as a programmer. To mix two metaphors, my experience was like running on a hamster wheel inside a pigeon hole.<\/p>\n\n<p>I worked on other stuff outside the job to keep up to date with the latest tech, but all it did was contribute to burnout. Since then, I maintain a very strong work-life boundary. Programming is my job, not my life. This separation is healthy for me.<\/p>\n\n<p>Tech jobs don\u2019t encourage upskilling nearly as much they should.<\/p>\n\n<p>To draw a comparison with my favourite sport: cricket. The international schedule is a complete mess \u2026 it\u2019s relentless. Players go from training, to a match, back to training. They sometimes get a break to allow their bodies to recover, but never enough time off to work on their skills.<\/p>\n\n<p>Joe Root is one of England\u2019s greatest batters, but was going through a bit of a lean spell (by his standards) in 2019. Then COVID hit and all cricket stopped. He used the time off to analyse another great batter of the generation, New Zealand\u2019s Kane Williamson. Stealing some ideas from him, Root tweaked his technique by practicing in his garden.<\/p>\n\n<p>This led into a stunning run of form into the 2021 season. His batting average went up from 37 in 2019 to 61 in 2021. He\u2019s currently ranked the number 1 batter in the world. All because he had some time off to upskill.<\/p>\n\n<p>Endlessly running on a hamster wheel doesn\u2019t do anyone any good.<\/p>\n\n<p>For the last couple of months I\u2019ve been unbooked, and used the time to experiment with a bunch of new stuff. I\u2019ve been exploring Linux admin, managing Linux processes using Ruby, concurrent and system programming in Ruby \u2026 and a lot more.<\/p>\n\n<p>All this has helped me become a better programmer, and better serve my clients. And it would\u2019ve been totally impossible without the downtime. As I mentioned earlier, programming is my job, not my life. I don\u2019t want to be doing this stuff \u201cin my own time\u201d so to speak.<\/p>\n\n<p>I really wish tech companies would recognise the value of time spent upskilling. Software is such a ridiculously high margin business, made low margin by ludicrous inefficiency.<\/p>\n\n<p>Endless meetings, poor leadership, non-existent management, and career progression aims that don\u2019t align with good product work are all typical in the average tech job. Companies rarely optimise for uninterrupted, focused time to just do work.<\/p>\n\n<p>I\u2019ll avoid addressing Scrum here because I could fill a book with rage about how much I loathe that ruddy excuse for a framework.<\/p>\n\n<p>I\u2019d go as far as to say the average development team is probably operating at 20% efficiency (full disclosure: that\u2019s a number pulled out of my arse, based on my experience and nothing scientific whatsoever).<\/p>\n\n<p>Efficient, well led, and well organised teams can deliver software at a scarcely believeable scale. WhatsApp <a href=\"https:\/\/blog.whatsapp.com\/400-million-stories?lang=en\" target=\"_blank\">had 50 employees servicing 400 million monthly active users<\/a> when they were acquired by Facebook for $19 billion.<\/p>\n\n<p>With good management, programmers having a month off every year to do their own thing should be perfectly feasible. They could learn something new. Build something interesting. Just do some programming that isn\u2019t their day-to-day job. I reckon the investment would be well worth it.<\/p>\n\n<p>I honestly think this is an organisational problem and nothing to do with a company\u2019s size or finances. Maybe I\u2019m just too idealistic \u2026<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Box brackets in Ruby class declarations","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/box-brackets-in-ruby-class-declarations\/","rel":"alternate","type":"text\/html","title":"Box brackets in Ruby class declarations"}},"published":"2024-12-27T00:00:00+00:00","updated":"2024-12-27T00:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2024-12-27-box-brackets-in-ruby-class-declarations.md","content":"<p>If you\u2019ve ever used Rails, you\u2019ll almost certainly have created a database migration and seen the below syntax in the class declaration:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class<\/span> <span class=\"nc\">CreateUserTable<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">ActiveRecord<\/span><span class=\"o\">::<\/span><span class=\"no\">Migration<\/span><span class=\"p\">[<\/span><span class=\"mf\">7.1<\/span><span class=\"p\">]<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nf\">change<\/span>\n    <span class=\"c1\"># ...<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>I\u2019d never understood how the <code class=\"highlighter-rouge\">[7.1]<\/code> in the superclass definition worked, and never really questioned it to be honest. I thought it was a fancy Ruby language feature that I didn\u2019t need to bother myself with at the moment.<\/p>\n\n<p>While looking through the code for <a href=\"https:\/\/github.com\/socketry\/async-rest\/\" target=\"_blank\"><code class=\"highlighter-rouge\">async-rest<\/code><\/a>, I came across the <a href=\"https:\/\/socketry.github.io\/async-rest\/guides\/getting-started\/index.html#usage\" target=\"_blank\">same syntax<\/a>.<\/p>\n\n<p>I\u2019m attempting to build a client for <a href=\"https:\/\/paddle.com\" target=\"_blank\">Paddle<\/a> to use in <a href=\"https:\/\/scattergun.email\" target=\"_blank\">Scattergun<\/a> using <code class=\"highlighter-rouge\">async-rest<\/code>, so I needed to understand what that syntax was actually doing.<\/p>\n\n<p>It\u2019s such a typically elegant mechanism. The box brackets aren\u2019t a language feature, but a class level method which returns a class.<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class<\/span> <span class=\"nc\">SomeClass<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nc\">self<\/span><span class=\"o\">.<\/span><span class=\"nf\">[]<\/span>\n    <span class=\"k\">return<\/span> <span class=\"no\">SomeOtherClass<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Going back to the Active Record example:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class<\/span> <span class=\"nc\">CreateUserTable<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">ActiveRecord<\/span><span class=\"o\">::<\/span><span class=\"no\">Migration<\/span><span class=\"p\">[<\/span><span class=\"mf\">7.1<\/span><span class=\"p\">]<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nf\">change<\/span>\n    <span class=\"c1\"># ...<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>The above is functionally equivalent to:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">class<\/span> <span class=\"nc\">CreateUserTable<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">ActiveRecord<\/span><span class=\"o\">::<\/span><span class=\"no\">Migration<\/span><span class=\"o\">::<\/span><span class=\"no\">Compatibility<\/span><span class=\"o\">::<\/span><span class=\"no\">V7_1<\/span>\n  <span class=\"k\">def<\/span> <span class=\"nf\">change<\/span>\n    <span class=\"c1\"># ...<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>Run <code class=\"highlighter-rouge\">ActiveRecord::Migration[7.1]<\/code> in a Rails console and see that it returns a class. It\u2019s just like calling a method!<\/p>\n\n<p>Here\u2019s the method definition: <a href=\"https:\/\/github.com\/rails\/rails\/blob\/af9ecc7a35dcec5c744353d9494d92709f6e1381\/activerecord\/lib\/active_record\/migration.rb#L630\" target=\"_blank\">https:\/\/github.com\/rails\/rails\/blob\/af9ecc7a35dcec5c744353d9494d92709f6e1381\/activerecord\/lib\/active_record\/migration.rb#L630<\/a>.<\/p>\n\n<p>In the case of <code class=\"highlighter-rouge\">async-rest<\/code>, it\u2019s a really elegant way to set the representation format (JSON, form etc.) for a resource. The <a href=\"https:\/\/github.com\/socketry\/async-rest\/blob\/fb408f455658b9a0181949dcfc1d5a2842d8ff9a\/lib\/async\/rest\/representation.rb#L21\" target=\"_blank\">method definition<\/a> shows how it injects the wrapper class into a new class definition and then returns that.<\/p>\n\n<p>That enables us to write code like:<\/p>\n\n<div class=\"language-ruby highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">module<\/span> <span class=\"nn\">Paddle<\/span>\n  <span class=\"k\">class<\/span> <span class=\"nc\">PaymentMethod<\/span> <span class=\"o\">&lt;<\/span> <span class=\"no\">Async<\/span><span class=\"o\">::<\/span><span class=\"no\">REST<\/span><span class=\"o\">::<\/span><span class=\"no\">Representation<\/span><span class=\"p\">[<\/span><span class=\"no\">Async<\/span><span class=\"o\">::<\/span><span class=\"no\">REST<\/span><span class=\"o\">::<\/span><span class=\"no\">Wrapper<\/span><span class=\"o\">::<\/span><span class=\"no\">JSON<\/span><span class=\"p\">]<\/span>\n    <span class=\"c1\"># ...<\/span>\n  <span class=\"k\">end<\/span>\n<span class=\"k\">end<\/span>\n<\/code><\/pre><\/div><\/div>\n\n<p>It\u2019s makes things so clear to a reader!<\/p>","author":{"name":"Ayush Newatia"}},{"title":"Managing SSH Key Pairs for server access","link":{"@attributes":{"href":"https:\/\/binarysolo.blog\/managing-ssh-key-pairs-for-server-access\/","rel":"alternate","type":"text\/html","title":"Managing SSH Key Pairs for server access"}},"published":"2024-11-13T00:00:00+00:00","updated":"2024-11-13T00:00:00+00:00","id":"repo:\/\/posts.collection\/_posts\/2024-11-13-managing-ssh-keys-for-deployment.md","content":"<p>I\u2019ve recently dipped my toe into deploying Ruby and Rails apps to a VPS, mainly to improve the deployment story for <a href=\"https:\/\/bridgetownrb.com\" target=\"_blank\">Bridgetown<\/a>. <em>No PaaS<\/em> is all the rage with Rails 8, and server management is a huge gap in my skillset anyway so I figured it\u2019s worth investing some time into it.<\/p>\n\n<p>Using password authentication to SSH into servers is <a href=\"https:\/\/security.stackexchange.com\/questions\/69407\/why-is-using-an-ssh-key-more-secure-than-using-passwords\" target=\"_blank\">considered insecure<\/a>. The preferred method is to use <strong>SSH Key Pairs<\/strong> which led me down the rabbit hole of the best practices to manage these keys and keep them safe.<\/p>\n\n<p>In this blog post, I\u2019ll describe what I\u2019ve learned.<\/p>\n\n<h2 id=\"what-is-an-ssh-key-pair\">What is an SSH key-pair?<\/h2>\n\n<p>An SSH Key Pair consists of a <strong>public key<\/strong> and a <strong>private key<\/strong>. The private key should never leave the system it\u2019s generated on. It is incredibly sensitive and must be kept safe.<\/p>\n\n<p>The public key is installed on the server we want to securely connect to. The <code class=\"highlighter-rouge\">~\/.ssh\/authorized_keys<\/code> file on a server contains all the public keys authorized to create SSH connections.<\/p>\n\n<p>The SSH handshake uses your local private key along with the public key on the server to authenticate access. This video is a great explainer: <a href=\"https:\/\/www.youtube.com\/watch?v=dPAw4opzN9g\" target=\"_blank\">https:\/\/www.youtube.com\/watch?v=dPAw4opzN9g<\/a>.<\/p>\n\n<p>A key serves to identify a single entity. If you\u2019re deploying from your machine to multiple servers, then it\u2019s your personal key on all the servers. You can also use the same key to <a href=\"https:\/\/docs.github.com\/en\/authentication\/connecting-to-github-with-ssh\/adding-a-new-ssh-key-to-your-github-account\" target=\"_blank\">write to GitHub repositories<\/a> and <a href=\"https:\/\/docs.github.com\/en\/authentication\/managing-commit-signature-verification\/signing-commits\" target=\"_blank\">sign your git commits<\/a> because it uniquely identifies you.<\/p>\n\n<h2 id=\"generating-a-key-pair\">Generating a key pair<\/h2>\n\n<p>We can generate a key pair using the <code class=\"highlighter-rouge\">ssh-keygen<\/code> CLI utility. The <a href=\"https:\/\/en.wikipedia.org\/wiki\/EdDSA#Ed25519\" target=\"_blank\">ED25519<\/a> is the most secure key type available today, so we\u2019ll use that.<\/p>\n\n<p>We\u2019ll also pass in a comment using the <code class=\"highlighter-rouge\">-C<\/code> flag which is appended to the public key. That gives us context about each public key installed on a server.<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ ssh-keygen -t ed25519 -C \"your_email@example.com\"\n<\/code><\/pre><\/div><\/div>\n\n<p>You\u2019ll be asked to enter a filename for the key. Press <code class=\"highlighter-rouge\">enter<\/code> as the default is fine.<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>Enter file in which to save the key (\/Users\/[USER]\/.ssh\/id_ed25519):\n<\/code><\/pre><\/div><\/div>\n\n<p>Next, you\u2019ll be asked for a passphrase to encrypt your private key on disk. This prevents people with access to your disk from reading your private key unless they know your passphrase.<\/p>\n\n<p>You should generate a secure passphrase and store it in a password manager for security.<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>Enter passphrase (empty for no passphrase):\n<\/code><\/pre><\/div><\/div>\n\n<p>After confirming your passphrase, your key pair will be generated:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>Your identification has been saved in \/Users\/[USER]\/.ssh\/id_ed25519\nYour public key has been saved in \/Users\/[USER]\/.ssh\/id_ed25519.pub\nThe key fingerprint is:\nSHA256:GcsT23BKVXAugQoK0z+gWidXIqqzyg7O9KlrOQWRdNY your_email@example.com\nThe key's randomart image is:\n+--[ED25519 256]--+\n|.o.o.    .+oo    |\n|oo* oE. .. +     |\n| =.= + .= o .    |\n|o.+ = .o @ .     |\n|o..+ .  S .      |\n|+  .     .       |\n|.+o              |\n|B+. .            |\n|=B+o             |\n+----[SHA256]-----+\n<\/code><\/pre><\/div><\/div>\n\n<p>You can now view your public key:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ cat ~\/.ssh\/id_ed25519.pub\n\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHD9BSFIEWuUq0ZCnlwRG\/68rSbV0LcEAzLTYjOUqf2L your_email@example.com\n<\/code><\/pre><\/div><\/div>\n\n<aside>\n  <p><strong>NOTE<\/strong>: Storing your private key passphrase in a password manager, and your key on disk can get a bit clunky. I prefer <a href=\"https:\/\/developer.1password.com\/docs\/ssh\" target=\"_blank\">using 1Password<\/a> to generate and store my SSH keys. This way, the private key is secured using 1Password\u2019s encryption model, and available on every device where you\u2019re logged into 1Password. It also mitigates against losing your private key if you lose your laptop.<\/p><\/aside>\n\n<h3 id=\"private-keys-for-automated-deployments\">Private keys for automated deployments<\/h3>\n\n<p>Earlier in this post, I said that a private key should never leave the system it\u2019s generated on. When setting up automated deployments as part of a CI\/CD pipeline, we need to break this rule.<\/p>\n\n<p>The CI server is a different entity to yourself meaning it needs its own key pair. It is also a trusted entity so we can safely generate the private key on our machine and transfer it to the CI server.<\/p>\n\n<aside>\n  <p><strong>NEVER<\/strong> use your own personal SSH key in a CI workflow. Always create a new key pair specifically for that purpose.<\/p><\/aside>\n\n<p>CI services have a secure mechanism to store sensitive information. GitHub Actions calls these <a href=\"https:\/\/docs.github.com\/en\/actions\/security-for-github-actions\/security-guides\/using-secrets-in-github-actions\" target=\"_blank\"><strong>secrets<\/strong><\/a>, and they can be referenced in your workflows. Other services will have an equivalent. Store the private key on the CI service securely using this technique.<\/p>\n\n<p>Ensure you treat the private key with utmost care and never store it unencrypted. If you choose to keep a copy of the deployment key pair on your machine, ensure that it is stored safely. 1Password or another password manager can help here.<\/p>\n\n<h2 id=\"the-ssh-agent\">The SSH Agent<\/h2>\n\n<p>As we saw earlier, the private key is encrypted on disk. Constantly entering the passphrase to decrypt the private key every time we connect to a server is tedious, which is where SSH agents come in.<\/p>\n\n<p>The SSH agent holds your keys and certificates in memory, unencrypted, and ready for use by <code class=\"highlighter-rouge\">ssh<\/code>. This is safe because the unencrypted data is only ever held in memory, never written to disk.<\/p>\n\n<p>Add your key to the SSH agent using:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ ssh-add ~\/.ssh\/id_ed25519\n<\/code><\/pre><\/div><\/div>\n\n<p>On a Mac, you can store your passphrase in the Keychain so it\u2019s automatically used to decrypt the key when you need it:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ ssh-add --apple-use-keychain ~\/.ssh\/id_ed25519\n<\/code><\/pre><\/div><\/div>\n\n<p><a href=\"https:\/\/developer.1password.com\/docs\/ssh\/agent\" target=\"_blank\">1Password has its own SSH Agent<\/a> using which you can access keys stored within your 1Password vaults.<\/p>\n\n<h2 id=\"ssh-config\">SSH Config<\/h2>\n\n<p>There\u2019s an SSH config file you can use to streamline your SSH usage. If you haven\u2019t set it up before, it won\u2019t exist:<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ touch ~\/.ssh\/config\n$ cat ~\/.ssh\/config\n<\/code><\/pre><\/div><\/div>\n\n<p>Within it, you can configure options for different hosts. The below examples shows a config file which uses the user\u2019s personal key by default, but uses the 1Password agent for GitHub only.<\/p>\n\n<div class=\"highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>Host *\n  IdentityFile ~\/.ssh\/id_ed25519\n\nHost github.com\n  HostName github.com\n  IdentityAgent ~\/.1password\/agent.sock\n  IdentityFile none\n<\/code><\/pre><\/div><\/div>\n\n<p>Here\u2019s a more in-depth explanation of the config file: <a href=\"https:\/\/www.ssh.com\/academy\/ssh\/config\" target=\"_blank\">https:\/\/www.ssh.com\/academy\/ssh\/config<\/a>.<\/p>\n\n<h2 id=\"conclusion\">Conclusion<\/h2>\n\n<p>To sum up: each SSH key pair uniquely identifies a single entity. You should have one for yourself to sign git commits and SSH into servers. Use separate keys for automated deployments.<\/p>\n\n<p>Use a password manager like 1Password to manage your SSH keys. Alternatively, ensure your private keys are encrypted with a secure passphrase and store that passphrase safely.<\/p>\n\n<p>Finally, use the SSH config file to streamline your workflow.<\/p>\n\n<p><strong>P.S.<\/strong>: <em>Massive thanks to <a href=\"https:\/\/ruby.social\/@wj\" target=\"_blank\">Will Jessop<\/a> for answering my silly questions about this stuff and for reviewing this blog post.<\/em><\/p>\n\n<h3 id=\"further-reading\">Further reading<\/h3>\n\n<ul>\n  <li>\n    <p>Connecting to GitHub using SSH: <a href=\"https:\/\/docs.github.com\/en\/authentication\/connecting-to-github-with-ssh\/about-ssh\" target=\"_blank\">https:\/\/docs.github.com\/en\/authentication\/connecting-to-github-with-ssh\/about-ssh<\/a>.<\/p>\n  <\/li>\n  <li>\n    <p>GitHub\u2019s guide to generate SSH keys: <a href=\"https:\/\/docs.github.com\/en\/authentication\/connecting-to-github-with-ssh\/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent\" target=\"_blank\">https:\/\/docs.github.com\/en\/authentication\/connecting-to-github-with-ssh\/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent<\/a>.<\/p>\n  <\/li>\n  <li>\n    <p>SSH agent forwarding (A technique to use your local private key when connecting to a another server from a remote server. For example, to check out a git repo during an SSH session): <a href=\"https:\/\/docs.github.com\/en\/authentication\/connecting-to-github-with-ssh\/using-ssh-agent-forwarding\" target=\"_blank\">https:\/\/docs.github.com\/en\/authentication\/connecting-to-github-with-ssh\/using-ssh-agent-forwarding<\/a>.<\/p>\n  <\/li>\n  <li>\n    <p>How SSH knows which public key to use on a server: <a href=\"https:\/\/security.stackexchange.com\/questions\/182804\/how-does-ssh-know-which-public-key-to-use-from-authorized-keys\" target=\"_blank\">https:\/\/security.stackexchange.com\/questions\/182804\/how-does-ssh-know-which-public-key-to-use-from-authorized-keys<\/a>.<\/p>\n  <\/li>\n  <li>\n    <p>Installing public keys on a server: <a href=\"https:\/\/linuxhandbook.com\/add-ssh-public-key-to-server\/\" target=\"_blank\">https:\/\/linuxhandbook.com\/add-ssh-public-key-to-server\/<\/a>.<\/p>\n  <\/li>\n<\/ul>","author":{"name":"Ayush Newatia"}}]}