Setup Ruby Sinatra + WEBrick with SSL
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"
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:
- WEBrick itself supports create self-signed certificate on-the-fly: https://gist.github.com/suruseas/3489236
- 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. - Most examples pass the WEBrick server options either via additional lines or via an extra
config.ru
. Simply using Sinatraserver_settings
idea is from here: https://gist.github.com/k-ta-yamada/6d3d6c75980dee5cf1dd189e170badce - 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