The latest Ruby runtime for AWS Lambda runs Ruby 2.7. Though this version of ruby is only 6 months old, the version of OpenSSL that Lambdas instance of Ruby was compiled with is over 3 years old. You can verify that by running the function below and seeing what it returns:
require 'openssl' def lambda_handler(event:, context:) return OpenSSL::OPENSSL_VERSION end # OpenSSL 1.0.2k 26 Jan 2017
That's Old! That means that Ruby's OpenSSL library is missing some key features like
TLS 1.3, and the
I wanted to see if I could load in a newer version of the OpenSSL shared library ruby loads so I could leverage some of these shiny new features. Well, it turns out, AWS Lambda Layers was a big part of the answer here. In the documentation, a Lambda Layer is available to your Lambda code via the
/opt directory. Now anyone who uses a lot of gem dependencies might have already come across this feature as it's a great way to share gems across different functions while keeping the function size itself fairly small.
But interestingly enough, it's not just a place to load gems. Lambda also adds to the
RUBYLIB environment variable with a path you can fill with a Lambda Layer (specifically
/opt/ruby/lib). This path is also prefixed to the
LOAD_PATH variable. This is where things get interesting.
Now that we know we can load up a Lambda Layer with a shared library that will be part of the auto searched
LOAD_PATH, we can construct a Lambda Layer with the necessary files to load our own version of OpenSSL. To do this, we need a newer instance of
openssl.so that was compiled with Ruby and we also need the
libcrypto.so.1.1 files to support the shared library.
I was able to extract a copy of these files by installing the latest version of OpenSSL from my package manager (pacman), and installing Ruby 2.7 from RVM so it re-compiled on my machine. In the end, I constructed a directory structure that looked like this:
. ├── lib │ ├── libcrypto.so -> libcrypto.so.1.1 │ ├── libcrypto.so.1.1 │ ├── libssl.so -> libssl.so.1.1 │ └── libssl.so.1.1 └── ruby └── lib └── openssl.so
I then zipped that up and uploaded that zip to a new Lambda Layer destined for my function. Upon running the below function, we can see that my OpenSSL version is now nice and new and should include the features I want! Running the original function above, I now see
OpenSSL 1.1.1d 10 Sep 2019. Excellent! Now I can go generate all the
scrypt keys and initiate all the
TLS 1.3 connections I want right?
Not exactly. It turns out, Ruby has a fun little behavior when it sees it needs to load some files. when calling
require, ruby will search through the
LOAD_PATH for the code you are trying to load, but specifically with
require, it will load .rb files and shared libraries with the
.so extension. So When I tried to create a new
SHA-256 digest, I was met with an unexpected error:
require 'openssl' def lambda_handler(event:, context:) return OpenSSL::Digest::SHA256.new end # uninitialized constant OpenSSL::Digest::SHA256
What happened? Well it turns out, because my
openssl.so file is now ahead of Ruby's built-in
openssl.rb code, I am only loading the shared library which comes with some classes, but not all the classes I expect. To get around this, it's quite simple:
require 'openssl.rb' def lambda_handler(event:, context:) return OpenSSL::Digest::SHA256.new end # #<OpenSSL::Digest::SHA256: ...>
By specifying the
.rb extension, I am now instructing Ruby to look through its
LOAD_PATH until it finds the first instance of a file called
openssl.rb. This is included with ruby and is the code that loads in all of the classes I expect to see, as well as an explicit call to load
openssl.so. This now allows me to use all of the shiny new features that OpenSSL 1.1.1(x) provides without having to use a Custom Runtime.