Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save buntine/54d3d1d4ac6c0fd3a13f to your computer and use it in GitHub Desktop.
Save buntine/54d3d1d4ac6c0fd3a13f to your computer and use it in GitHub Desktop.
Building SSH Key management tool with Elixir

Building 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 and we've released the source code to the community at Github.

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 function composition operator |>, this makes the code much more readable, and elegant.
  • Pattern matching is great.
  • Documentation as 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 user into all the target machines.
  • Remove a user from all the target machines.
  • Add / remove a user from particular machine(s).

How Pubkeys work

PubKeys works by looking at a specified directory for the list of SSH key files. These files are named after the IP Address of a given server and contain all the user's SSH public keys that we want to sync to the remote machine. PubKeys parses those files to add / remove a 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
Depolying 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 the function to filter out any empty files, and then pass the result of this process to the next function that will sync the file to the 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 Elixir with functional programming style more elegant compared to 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 user

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 user:

  • 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 user

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 user's SSH keys across our servers. The experience building this tool with Elixir has been a delightful one and a fresh approach in developing in a functional style.

Through this journey, we've learnt the potency of Elixir as a programming language. Having said that, we've only scratched half of Elixir's surface here. Elixir comes out of the box with concurent, 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 how Elixir makes documentation as a first class citizen with Doctest makes testing and documenting code much easier. We have had a lot of fun building PubKeys with Elixir in 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