Setup Ruby Sinatra + WEBrick with SSL

Windix Feng
5 min readSep 14, 2024

--

I am still a big fan of Ruby Sinatra given its simplicity especially with inline template to write everything in a single file.

Here is a hello world example we will use as a base:

require 'sinatra'

# need this one otherwise can only access locally
set :bind, '0.0.0.0'

get '/' do
"Hello!"
end

With ruby server.rb -p PORT we can spin it up with default WEBrick backend without any other dependencies.

Recently with Chrome, one annoying thing is that, it tries to request via HTTPS first and you will see weird requests on server side like this:

$ ruby server.rb -p 3000
[2024-09-14 16:49:21] INFO WEBrick 1.7.0
[2024-09-14 16:49:21] INFO ruby 3.0.2 (2021-07-07) [x86_64-linux-gnu]
== Sinatra (v2.1.0) has taken the stage on 3000 for development with backup from WEBrick
[2024-09-14 16:49:21] INFO WEBrick::HTTPServer#start: pid=679011 port=3000
[2024-09-14 16:49:33] ERROR bad Request-Line `\x16\x03\x01\a\x16\x01\x00\a\x12\x03\x03L��S�O�\b\x14��\x1D�ݣ��/ �PG\x17��K�\x0E���# \e��\x1E|�\x13�\t�ˍ��\x04J�y�qc'.
1.2.3.4 - - [14/Sep/2024:16:49:33 AEST] "\x16\x03\x01\a\x16\x01\x00\a\x12\x03\x03L��S�O�\b\x14��\x1D�ݣ��/ �PG\x17��K�\x0E���# \e��\x1E|�\x13�\t�ˍ��\x04J�y�qc" 400 339
- ->
[2024-09-14 16:49:33] ERROR bad Request-Line `\x16\x03\x01\x06�\x01\x00\x06�\x03\x03\v��\tu�2���0\x03�\x14{z��<\x1F[��\v&\x11�SU@ �\x1E�\x18��\x15�\a����\x15�p\x11�\x1A�+]=\bc:\f\t'�%�\x00 **\x13\x01\x13\x02\x13\x03�+�/�,�0̨̩�\x13�\x14\x00�\x00�\x00/\x005\x01\x00\x06IJJ\x00\x00\x00\x17\x00\x00�\x01\x00\x01\x00\x00-\x00\x02\x01\x01\x00\x00\x00\x12\x00\x10\x00\x00\rhc2.windix.au\x00\v\x00\x02\x01\x00\x00'.
1.2.3.4 - - [14/Sep/2024:16:49:33 AEST] "\x16\x03\x01\x06�\x01\x00\x06�\x03\x03\v��\tu�2���0\x03�\x14{z��<\x1F[��\v&\x11�SU@ �\x1E�\x18��\x15�\a����\x15�p\x11�\x1A�+]=\bc:\f\t'�%�\x00 **\x13\x01\x13\x02\x13\x03�+�/�,�0̨̩�\x13�\x14\x00�\x00�\x00/\x005\x01\x00\x06IJJ\x00\x00\x00\x17\x00\x00�\x01\x00\x01\x00\x00-\x00\x02\x01\x01\x00\x00\x00\x12\x00\x10\x00\x00\rhc2.windix.au\x00\v\x00\x02\x01\x00\x00" 400 443
- ->
[2024-09-14 16:49:33] ERROR bad Request-Line `\x16\x03\x01\x06�\x01\x00\x06�\x03\x03���\x1Ab�mߌ��N��\x17��a\x19&j'.
1.2.3.4 - - [14/Sep/2024:16:49:33 AEST] "\x16\x03\x01\x06�\x01\x00\x06�\x03\x03���\x1Ab�mߌ��N��\x17��a\x19&j" 400 310
- ->
[2024-09-14 16:49:34] ERROR bad Request-Line `\x16\x03\x01\x06�\x01\x00\x06�\x03\x03�ɍ+�\x12L�*�픹U�wr�\x14<\x1Cu�r;\x14�\f��~� X&\x11�ʷ�&��\v�x��Еu��T=���h�\t3��\x00 JJ\x13\x01\x13\x02\x13\x03�+�/�,�0̨̩�\x13�\x14\x00�\x00�\x00/\x005\x01\x00\x06I\x1A\x1A\x00\x00\x00\x00\x00\x12\x00\x10\x00\x00\rhc2.windix.au�\r\x00�\x00\x00\x01\x00\x01�\x00 W���+}�/��m����ZY�S5�!�'.
1.2.3.4 - - [14/Sep/2024:16:49:34 AEST] "\x16\x03\x01\x06�\x01\x00\x06�\x03\x03�ɍ+�\x12L�*�픹U�wr�\x14<\x1Cu�r;\x14�\f��~� X&\x11�ʷ�&��\v�x��Еu��T=���h�\t3��\x00 JJ\x13\x01\x13\x02\x13\x03�+�/�,�0̨̩�\x13�\x14\x00�\x00�\x00/\x005\x01\x00\x06I\x1A\x1A\x00\x00\x00\x00\x00\x12\x00\x10\x00\x00\rhc2.windix.au�\r\x00�\x00\x00\x01\x00\x01�\x00 W���+}�/��m����ZY�S5�!�" 400 461
- ->
1.2.3.4 - - [14/Sep/2024:16:49:34 +1000] "GET / HTTP/1.1" 200 6 0.0062
1.2.3.4 - - [14/Sep/2024:16:49:34 AEST] "GET / HTTP/1.1" 200 6

You can either just ignore it, type full address with http:// in address bar, or add your website to a Chrome whitelist. But hey, it is ruby, it should be easy to resolve.

I tried to google around, but cannot find up-to-date working solution. So here is what I worked out at the time of writing.

Example 1: with a self-signed certificate

require 'sinatra'
require 'webrick/https'
require 'openssl'

set :bind, '0.0.0.0'

set :server_settings,
SSLEnable: true,
SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
SSLCertificate: OpenSSL::X509::Certificate.new(File.open(File.dirname(__FILE__) + '/cert/fullchain.pem').read),
SSLPrivateKey: OpenSSL::PKey::EC.new(File.open(File.dirname(__FILE__) + '/cert/privkey.pem').read)

get '/' do
"Hello!"
end

Above code assumes we have HTTPS certificate (fullchain.pem) and private key (privkey.pem) files under cert directory relative to the ruby script.

A self-signed version can be created via this one-liner (for website my.example.com with 10 years validity):

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 \
-nodes -keyout privkey.pem -out fullchain.pem -subj "/CN=my.example.com" \
-addext "subjectAltName=DNS:my.example.com"

(Source: https://stackoverflow.com/questions/10175812/how-to-generate-a-self-signed-ssl-certificate-using-openssl)

You still need to allow browser to bypass the NET::ERR_CERT_AUTHORITY_INVALID warning. Can still see couple error lines due to self-signed certificate. But we are on REAL HTTPS now.

$ ruby server-https-self-signed.rb -p 3000
[2024-09-14 17:09:20] INFO WEBrick 1.7.0
[2024-09-14 17:09:20] INFO ruby 3.0.2 (2021-07-07) [x86_64-linux-gnu]
[2024-09-14 17:09:20] INFO \nCertificate:\n Data:\n Version: 3 (0x2)\n Serial Number:\n 2c:82:19:db:80:bc:eb:5d:57:b8:d1:b8:0c:a8:f1:4e:04:aa:33:d5\n Signature Algorithm: ecdsa-with-SHA256\n Issuer: CN=my.example.com\n Validity\n Not Before: Sep 14 07:07:04 2024 GMT\n Not After : Sep 12 07:07:04 2034 GMT\n Subject: CN=my.example.com\n Subject Public Key Info:\n Public Key Algorithm: id-ecPublicKey\n Public-Key: (384 bit)\n pub:\n 04:cc:09:3d:db:ab:e1:e6:f0:cc:eb:b1:05:5d:b2:\n 8a:ea:39:5e:2d:29:65:75:87:cb:d9:ef:f8:d8:51:\n 56:92:f3:01:65:57:a7:e6:44:6d:53:0a:f4:42:73:\n f9:4f:c3:47:56:5c:50:9c:22:53:f4:8b:6b:5b:ec:\n 80:40:df:71:a7:e6:ba:82:9a:50:6f:70:06:5f:cd:\n 2e:33:b5:e8:5b:18:d5:c3:67:9a:00:aa:39:7a:8f:\n
33:0c:61:5c:cf:06:fc\n ASN1 OID: secp384r1\n NIST CURVE: P-384\n X509v3 extensions:\n X509v3 Subject Key Identifier: \n 7A:9C:AE:0A:DB:5E:2E:40:4E:84:A1:FD:3D:E0:9F:E9:8F:0B:CC:F3\n X509v3 Authority Key Identifier: \n 7A:9C:AE:0A:DB:5E:2E:40:4E:84:A1:FD:3D:E0:9F:E9:8F:0B:CC:F3\n X509v3 Basic Constraints: critical\n CA:TRUE\n X509v3 Subject Alternative Name: \n DNS:my.example.com\n Signature Algorithm: ecdsa-with-SHA256\n Signature Value:\n 30:66:02:31:00:81:d0:a6:be:c6:f6:fd:be:af:8b:9d:6e:a6:\n 74:1d:68:24:bf:75:01:e4:a0:c4:54:76:98:57:89:2b:b3:f3:\n 9b:26:eb:43:43:7c:71:10:ba:3e:58:bf:60:05:6c:c1:c3:02:\n 31:00:e3:4b:e9:b4:e3:53:f7:53:9e:7a:de:ad:b1:4d:23:86:\n 73:8c:6d:93:f4:1d:a4:c0:d7:4e:8d:1d:6f:74:4e:f1:3b:1b:\n 67:bb:42:36:2a:e3:48:49:cf:69:3f:0d:41:03\n
== Sinatra (v2.1.0) has taken the stage on 3000 for development with backup from WEBrick
[2024-09-14 17:09:20] INFO WEBrick::HTTPServer#start: pid=679395 port=3000
[2024-09-14 17:09:23] ERROR OpenSSL::SSL::SSLError: SSL_accept returned=1 errno=0 peeraddr=1.2.3.4:57790 state=error: sslv3 alert certificate unknown
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `accept'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `block (2 levels) in start_thread'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/utils.rb:258:in `timeout'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:300:in `block in start_thread'
[2024-09-14 17:09:24] ERROR OpenSSL::SSL::SSLError: SSL_accept returned=1 errno=0 peeraddr=1.2.3.4:57791 state=error: sslv3 alert certificate unknown
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `accept'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `block (2 levels) in start_thread'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/utils.rb:258:in `timeout'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:300:in `block in start_thread'
[2024-09-14 17:09:27] ERROR OpenSSL::SSL::SSLError: SSL_accept returned=1 errno=0 peeraddr=1.2.3.4:57797 state=error: sslv3 alert certificate unknown
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `accept'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `block (2 levels) in start_thread'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/utils.rb:258:in `timeout'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:300:in `block in start_thread'
[2024-09-14 17:09:27] ERROR OpenSSL::SSL::SSLError: SSL_accept returned=1 errno=0 peeraddr=1.2.3.4:57798 state=error: sslv3 alert certificate unknown
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `accept'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:302:in `block (2 levels) in start_thread'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/utils.rb:258:in `timeout'
/usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/server.rb:300:in `block in start_thread'
1.2.3.4 - - [14/Sep/2024:17:09:27 +1000] "GET / HTTP/1.1" 200 6 0.0060
1.2.3.4 - - [14/Sep/2024:17:09:27 AEST] "GET / HTTP/1.1" 200 6
- -> /
1.2.3.4 - - [14/Sep/2024:17:09:28 +1000] "GET /favicon.ico HTTP/1.1" 404 474 0.0021
1.2.3.4 - - [14/Sep/2024:17:09:28 AEST] "GET /favicon.ico HTTP/1.1" 404 474
https://my.example.com:3000/ -> /favicon.ico

Example 2: with an existing Let’s Encrypt certificate

Since I have already set up free HTTPS certificate auto-renewal via Let’s Encrypt on my server, why not use that directly?

They are under /etc/letsencrypt/live/WEBSITE/ which only root user can access. Make a copy of fullchain.pem and privkey.pem to replace the self-signed version in previous example.

The code is almost the same as previous version, except we no longer need SSLVerifyClient option.

require 'sinatra'
require 'webrick/https'
require 'openssl'

set :bind, '0.0.0.0'

set :server_settings,
SSLEnable: true,
SSLCertificate: OpenSSL::X509::Certificate.new(File.open(File.dirname(__FILE__) + '/cert/fullchain.pem').read),
SSLPrivateKey: OpenSSL::PKey::EC.new(File.open(File.dirname(__FILE__) + '/cert/privkey.pem').read)

get '/' do
"Hello!"
end

It works perfectly now! No more errors from server side.

$ ruby server-https-letsencrypt.rb -p 3000
[2024-09-14 17:36:10] INFO WEBrick 1.7.0
[2024-09-14 17:36:10] INFO ruby 3.0.2 (2021-07-07) [x86_64-linux-gnu]
[2024-09-14 17:36:10] INFO \nCertificate:\n Data:\n Version: 3 (0x2)\n Serial Number:\n 04:cd:a0:7a:e0:c5:46:1c:be:49:3e:70:f0:d5:70:79:06:76\n Signature Algorithm: ecdsa-with-SHA384\n Issuer: C=US, O=Let's Encrypt, CN=E5\n Validity\n Not Before: Jul 26 10:10:39 2024 GMT\n Not After : Oct 24 10:10:38 2024 GMT\n Subject: CN=my.example.com\n Subject Public Key Info:\n Public Key Algorithm: id-ecPublicKey\n Public-Key: (256 bit)\n pub:\n 04:a1:29:f4:3f:ca:f8:c1:33:b2:cd:80:04:ae:27:\n f9:84:02:73:2c:56:15:32:61:f3:80:f5:84:df:50:\n d6:c6:97:b4:8d:28:98:8b:f8:25:00:9f:3f:f1:36:\n
04:ba:c9:fa:2d:d9:96:0a:83:5b:9e:47:5e:f8:37:\n c3:08:65:75:2d\n ASN1 OID: prime256v1\n NIST CURVE: P-256\n X509v3 extensions:\n X509v3 Key Usage: critical\n Digital Signature\n X509v3 Extended Key Usage: \n TLS Web Server Authentication, TLS Web Client Authentication\n X509v3 Basic Constraints: critical\n CA:FALSE\n X509v3 Subject Key Identifier: \n 60:C8:50:43:40:D1:54:9E:24:4E:B3:9E:E6:FE:63:F1:6B:79:84:3A\n X509v3 Authority Key Identifier: \n 9F:2B:5F:CF:3C:21:4F:9D:04:B7:ED:2B:2C:C4:C6:70:8B:D2:D7:0D\n Authority Information Access: \n OCSP - URI:http://e5.o.lencr.org\n CA Issuers - URI:http://e5.i.lencr.org/\n X509v3 Subject Alternative Name: \n DNS:my.example.com\n X509v3 Certificate Policies: \n Policy: 2.23.140.1.2.1\n CT Precertificate SCTs: \n Signed Certificate Timestamp:\n Version : v1 (0x0)\n Log ID : 19:98:10:71:09:F0:D6:52:2E:30:80:D2:9E:3F:64:BB:\n 83:6E:28:CC:F9:0F:52:8E:EE:DF:CE:4A:3F:16:B4:CA\n Timestamp : Jul 26 11:10:40.054 2024 GMT\n Extensions: none\n Signature : ecdsa-with-SHA256\n 30:45:02:21:00:BD:5B:F7:80:12:51:E4:52:6A:DF:13:\n
5C:DD:90:FC:53:36:4E:D3:02:DE:47:7B:2A:2C:81:3B:\n 42:16:44:8F:18:02:20:08:85:F3:A4:9A:8E:B0:55:F0:\n 95:15:98:E0:9C:63:BE:80:38:8F:F6:6E:E5:6C:91:30:\n 07:35:17:63:F7:43:FC\n Signed Certificate Timestamp:\n Version : v1 (0x0)\n
Log ID : 3F:17:4B:4F:D7:22:47:58:94:1D:65:1C:84:BE:0D:12:\n ED:90:37:7F:1F:85:6A:EB:C1:BF:28:85:EC:F8:64:6E\n Timestamp : Jul 26 11:10:40.045 2024 GMT\n Extensions: none\n Signature : ecdsa-with-SHA256\n 30:46:02:21:00:BE:75:1C:FC:C0:C3:3E:28:1E:01:A3:\n
DC:C5:40:D1:61:97:E5:BF:15:14:2D:95:FE:87:E9:D8:\n AA:DF:26:3C:7E:02:21:00:A3:BA:9C:56:E6:A6:6C:C5:\n 36:B8:2B:C4:73:CE:98:D4:11:65:43:54:49:72:01:54:\n 16:C9:F1:BF:80:50:B4:58\n Signature Algorithm: ecdsa-with-SHA384\n Signature Value:\n 30:64:02:30:40:01:54:2f:39:4f:66:00:dc:b5:32:e5:f4:78:\n 6b:6f:2c:0a:c3:0b:38:45:85:11:b3:3b:6f:01:5e:30:e5:b9:\n 54:13:97:31:1f:88:5c:9d:10:89:ac:b0:1e:fe:c7:b6:02:30:\n 7d:c3:c1:a0:e7:49:19:64:c1:c6:07:e0:ee:73:cf:c8:27:31:\n 68:b0:49:91:1b:f7:cf:10:b5:b1:c7:bf:33:74:91:40:08:7b:\n 2e:29:56:8f:49:de:6a:14:82:9e:72:f2\n
== Sinatra (v2.1.0) has taken the stage on 3000 for development with backup from WEBrick
[2024-09-14 17:36:10] INFO WEBrick::HTTPServer#start: pid=679972 port=3000
1.2.3.4 - - [14/Sep/2024:17:36:25 +1000] "GET / HTTP/1.1" 200 6 0.0072
1.2.3.4 - - [14/Sep/2024:17:36:25 AEST] "GET / HTTP/1.1" 200 6

You just need to redo the copying of latest certificate from time to time (since Let’s Encrypt certificate only validates for 3 months).

Couple notes:

  1. WEBrick itself supports create self-signed certificate on-the-fly: https://gist.github.com/suruseas/3489236
  2. Almost all the examples I found uses RSA format private key OpenSSL::PKey::RSA. But since Let’s Encrypt uses ECC / Elliptic Curve CryptographyOpenSSL::PKey::EC, I used ECC even for self-signed version.
  3. Most examples pass the WEBrick server options either via additional lines or via an extra config.ru. Simply using Sinatra server_settings idea is from here: https://gist.github.com/k-ta-yamada/6d3d6c75980dee5cf1dd189e170badce
  4. If you don’t have Let’s Encrypt setup, it is possible to use a one-liner to get certificate manually:
    https://m-thirumal.github.io/installation_guide/#/TLS/let's_encrypt

--

--

No responses yet