Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jpartain89/6759f0cc9a0cc42f635a7de3eaa6b9d9 to your computer and use it in GitHub Desktop.
Save jpartain89/6759f0cc9a0cc42f635a7de3eaa6b9d9 to your computer and use it in GitHub Desktop.
Building SSH Key management tool with Elixir

Building an SSH key management tool with Elixir

Sipping the Elixir

After being inspired by David's talk on Elixir at Hardhat, we decided to try and build our own SSH public key management tool, PubKeys, in Elixir. We're delighted to report back that Elixir is amazing.

Things we love about Elixir:

  • Writing code in a functional style encourages us to break the code into small chunks of functions.
  • Through the use of the function composition operator |>, the code is readable and elegant.
  • Pattern matching is great.
  • Documentation is a first-class citizen.

Introducing PubKeys

The purpose of PubKeys is to enable the developers at Hardhat to easily manage public key access to various servers from one central machine.

The following are key functionalities of PubKeys:

  • add a key to all machines
  • remove a key from all machines
  • add/remove a key from one or more machines.

How PubKeys work

PubKeys works by looking at a specified directory for a list of files containing public keys. These files are named after the IP address of a given server and they contain the public keys that we want to sync to the remote machine. PubKeys parses those files, modifies them by adding/removing the specified key, and then performs a secure copy via scp to syncrhonize the keys on the central server with the remote server.

concept

The potent Elixir

Main function

Peeking inside the code, we can see that the main function parses command-line arguments. Using Elixir's pattern matching, we can match the input arguments and execute appropriate functions.

input

def main(options) do
    path = files_path(Mix.env)
    files_list = System.cmd("ls", [path]) |> elem(0) |> String.split("\n")
    
    case options do
      ["--help"] -> IO.puts @instructions
      ["--deploy-all"] -> deploy_all(files_list, path)
      ["--add", ssh_key] -> add_user_key(ssh_key, files_list, path)
      ["--remove", ssh_key] -> remove_user_key(ssh_key, files_list, path)
      _ -> IO.puts "Unrecognized input.\n#{@instructions}"
    end
  end
Deploying to remote servers

This function takes a list of ssh key files and a remote target path as argument inputs. Through the use of the pipe operator |>, which allows for function composition, we can pass in the files list to a function that filters out any empty files. The result is then passed to the next function, which syncs each file to the appropriate remote server.

You can see that this process flow is akin to the process in an assembly line. A collection of objects is being passed to a function, which in return passes the results to the next function. This is what makes using Elixir in a functional programming style more elegant when compared to the traditional imperative programming style.

deploy-all

def deploy_all(files_list, path) do
  files_list
  |> Enum.reject(&empty?(&1))
  |> Enum.map(&scp_to_server(&1, path))
end
Adding/removing a key

The add_user_key and remove_user_key function takes 3 arguments: ssh_key, files_list, and path. Like the function deploy_all, this function passes the input from one function into another until finally the last function syncs the file with the remote server.

These are functions in PubKeys for adding or removing a key:

  • Adding a user

add key

def add_user_key(ssh_key, files_list, path) do
    files_list
    |> Enum.reject(&empty?(&1))
    |> Enum.map(&read_keys(&1, path))
    |> Enum.reject(&should_skip?(&1, ssh_key))
    |> Enum.map(&prepend_key(&1, ssh_key, path))
    |> Enum.map(&scp_to_server(&1, path))
  end
  • Removing a key

delete key

def remove_user_key(ssh_key, files_list, path) do
    files_list
    |> Enum.reject(&empty?(&1))
    |> Enum.map(&read_keys(&1, path))
    |> Enum.reject(&should_skip?(&1, ssh_key, :remove))
    |> Enum.map(&remove_key(&1, ssh_key, path))
    |> Enum.map(&scp_to_server(&1, path))
  end

Documentation

Elixir promotes documentation to a first-class citizen. With Doctest, one can easily write a comment that documents what a function does. One can also provide a test within the example. This test can be integrated as part of the test suite when running mix test. The following code snippet displays an example of doctest implementation.

@doc """
  Given an input of remote server's IP address, and the directory path to ssh key files, returns a tuple containing the ip address and a list of the ssh_keys
  ## Examples
      iex> path = PubKeys.Helper.files_path(:test)
      iex> PubKeys.read_keys("192.168.0.10", path)
      {"192.168.0.10", ["ssh-rsa ABCD123abcd test1@work", "ssh-rsa XYZ987abcd test2@work"]}
  """
 def read_keys(ip, path) do
  {:ok, content} = File.read("#{path}/#{ip}")
  filtered_keys = (String.split(content, "\n") |> Enum.reject(&empty?/1))
  {ip, filtered_keys}
end

With Ex_doc, we can easily generate documentation by running mix docs. The generated documenation is a very neat addition to doctest. An example of PubKeys generated documentation can be viewed here : PubKeys Doc

Conclusion

PubKeys is now deployed live in Hardhat's environment as our internal tool for managing public keys across our servers. The experience building this tool with Elixir has been a delightful one; it's provided a fresh approach to developing using a functional style.

Through this journey, we've learnt the potency of Elixir as a programming language. Having said that, we've only scratched Elixir's surface here. Elixir comes out of the box with concurrent, distributed, and fault-tolerant properties. If you're interested in learning more about Elixir, we highly recommend you check out this guide.

Elixir, with it's functional programming style, is very elegant and fun to write. The way Elixir makes documentation a first-class citizen with Doctest makes testing and documenting code much easier. We have had a lot of fun building PubKeys with Elixir at Hardhat. Next we're looking at Phoenix, a web framework written in Elixir. Stay tuned for the next post!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment