Notes to self

Secure Azure blobs pre-signing in Elixir

If you use Azure block storage, chances are you want to take advantage of a shared access signature (SAS), a feature usually known as pre-signing URLs. Shared access signatures allow you to share access to your blobs with a given time limit.

The Elixir project I am on is hosted on Azure and so it’s to nobody surprise that we are using the Azure Blob Storage for our uploads. We started with ExAzure, a wrapper for Erlang’s :erlazure, to do the uploads from Elixir.

However, when it comes to pre-signing, we are on our own.

I am including an example of a minimal module that can pre-sign URLs, but make sure you go through and understand the documentation on SAS and official examples.

Below is a MyApp.Storage.Blob module that can pre-sign given paths using the shared access signature.

As a configuration you will need to provide a hostname, account_name and access_key:

defmodule MyApp.Storage.Blob do
  @hostname Application.get_env(:digi, MyApp.Storage)[:hostname]
  @account_name Application.get_env(:digi, MyApp.Storage)[:account_name]
  @access_key Application.get_env(:digi, MyApp.Storage)[:access_key]
  ...

Thesigned_uri/2 function is the main function of the module. It accepts the path to sign and options.

It gives read-only access for fixed 30 minutes by default, but options let you adjust permissions and exact start and expiry datetimes:

  ...
  def signed_uri(path, options \\ %{}) do
    now = DateTime.truncate(DateTime.utc_now(), :second)
    expiry = Timex.shift(now, minutes: +30)

    default_options = %{
      path: path,
      permissions: "r",
      start: DateTime.to_iso8601(now),
      expiry: DateTime.to_iso8601(expiry),
      stg_version: "2017-11-09",
      protocol: ""
    }

    opts = Map.merge(default_options, options)
    signable_string = signable_string_for_blob(opts)
    signature = sign(signable_string)

    query_hash = %{
      "sp" => opts[:permissions],
      "sv" => opts[:stg_version],
      "sr" => "b",
      "st" => opts[:start],
      "se" => opts[:expiry],
      "sig" => signature
    }

    "https://#{@hostname}/#{path}?" <> URI.encode_query(query_hash)
  end
  ...

As you can see, the main purpose is to build an actual URI.

We also needed to prepare a signable string using signable_string_for_blob/1 and generate HMAC signature with sign/1 functions:

  ...
  defp sign(body) do
    {:ok, key} = @access_key |> Base.decode64()

    :crypto.hmac(:sha256, key, body)
    |> Base.encode64()
  end

  defp signable_string_for_blob(options) do
    signable_opts = [
      options[:permissions],
      options[:start],
      options[:expiry],
      "/blob/#{@account_name}/#{options[:path]}",
      options[:identifier],
      options[:ip_range],
      options[:protocol],
      options[:stg_version],
      options[:cache_control],
      options[:content_disposition],
      options[:content_encoding],
      options[:content_language],
      options[:content_type]
    ]

    Enum.join(signable_opts, "\n")
  end
end

And that’s pretty much everything to get you started.

The usage of the signed_uri/2 function is simple (paths depend on your storage):

  ...
  def get_tmp_url(id) do
    MyApp.Storage.Blob.signed_uri("#{@container}/tmp/#{id}.png")
  end
  ...

Work with me

I have some availability for contract work. I can be your fractional CTO, a Ruby on Rails engineer, or consultant. Write me at strzibny@strzibny.name.

RSS