In this blog post, we will learn how to control access to nomad.

Introduction

Nomad is an application scheduler, that helps you schedule application-processes efficiently, across multiple servers, and keep your infrastructure costs low. Nomad is capable of scheduling containers, virtual machines, as well as isolated forked processes.

There are other schedulers available, such as Kubernetes, Mesos or Docker Swarm, but each has different mechanisms for securing access. By following this post, you will understand the main components in securing your Nomad cluster, but the overall idea is valid across any of the other schedulers available.

One of Nomad's selling points, and why you could consider it over tools like Kubernetes, is that you can schedule not only containers, but also QEMU images, LXC, isolated fork/exec processes, and even Java applications in a chroot(!). All you need is a driver implemented for Nomad. On the other hand, its community is smaller than Kubernetes, so the tradeoffs have to be measured on a project-by-project basis.

We will start by deploying a test cluster and configuring access control lists (ACLs).

Overview

In this tutorial, we will:

  1. Setup our environment to run nomad inside a Vagrant virtual machine for running experiments
  2. We generate a root/admin token (usually known as the "management" token) and activate ACLs
  3. Using the management token, we add a new "non-admin" policy and create a token associated with this new policy
  4. Use the "non-admin" token to demonstrate access control.

Setup the environment

Pre-requisites:

We will run everything from within a virtual machine with all the necessary configuration and applications. Execute the following commands on your shell:

$ cd $(mktemp --directory)
$ curl -LO https://raw.githubusercontent.com/hashicorp/nomad/master/demo/vagrant/Vagrantfile
$ vagrant up
    ...
    lines and lines of Vagrant output
    this might take a while
    ...
$ vagrant ssh
    ...
    Message of the day greeting from VM
    Anything after this point is being executed inside the virtual machine
    ...
vagrant@nomad:~$ nomad version
Nomad vX.X.X
vagrant@nomad:~$ uname -n
nomad

Depending on your system and the version of Vagrantfile used, the prompt may be different.

Setup Nomad

We configure nomad to execute both as server and client for convenience, as opposed to a production environment where the server is remote and client is local to each machine or node. Create a nomad-agent.conf with the following contents:

bind_addr = "0.0.0.0"
data_dir = "/var/lib/nomad"
region = "global"
acl {
  enabled = true
}
server {
  enabled              = true
  bootstrap_expect     = 1
  authoritative_region = "global"
}
client {
  enabled = true
}

Then, execute:

vagrant@nomad:~$ sudo nomad agent -config=nomad-agent.conf # sudo is needed to run as a client

You should see output indicating that Nomad is running.

Clients need root access to be able to execute processes, while servers only communicate to synchronize state.

ACL Bootstrap

On another terminal, after running vagrant ssh from our temporary working directory, run the following command:

vagrant@nomad:~$ nomad acl bootstrap

Accessor ID  = 2f34299b-0403-074d-83e2-60511341a54c
Secret ID    = 9fff6a06-b991-22db-7fed-55f17918e846
Name         = Bootstrap Token
Type         = management
Global       = true
Policies     = n/a
Create Time  = 2018-02-14 19:09:23.424119008 +0000 UTC
Create Index = 13
Modify Index = 13

This Secret ID is our management (admin) token. This token is valid globally and all operations are permitted. No policies are necessary while authenticating with the management token, and so, none are configured by default.

It is important to copy the Accessor ID and Secret ID to some file, for safekeeping, as we will need these values later. For a production environment, it is safest to store these in a separate vault permanently.

Once ACLs are on, all operations are denied unless a valid token is provided with each request, and the operation we want is allowed by a policy associated with the provided token.

vagrant@nomad:~$ nomad node-status
Error querying node status: Unexpected response code: 403 (Permission denied)

vagrant@nomad:~$ export NOMAD_TOKEN='9fff6a06-b991-22db-7fed-55f17918e846' # Secret ID, above
vagrant@nomad:~$ nomad node-status

ID        DC   Name   Class   Drain  Status
1f638a17  dc1  nomad  <none>  false  ready

Designing policies

Policies are a collection of (ideally, non-overlapping) roles, that provide access to different operations. The table below shows typical users of a Nomad cluster.

RoleNamespaceAgentNodeRemarks
AnonymousdenydenydenyUnnecessary, as token-less requests are denied all operations.
DeveloperwritedenyreadDevelopers are permitted to debug their applications, but not to perform cluster management
Loggerlist-jobs, read-logsdenyreadAutomated log aggregators or analyzers that need read access to logs
Job requestersubmit-jobdenydenyCI systems create new jobs, but don't interact with running jobs.
InfrastructurereadwritewriteDevOps teams perform cluster management but seldom need to interact with running jobs.

For namespace access, read is equivalent to [read-job, list-jobs]. write is equivalent to [list-jobs, read-job, submit-job, read-logs, read-fs, dispatch-job].

In the event that operators do need to have access to namespaces, one can always create a token that has both Developer and Infrastructure policies attached. This is equivalent to having a management token.

We have left out multi-region and multi-namespace setups here. We have assumed everything to be running under the default namespace. It should be noted that on production deployments, with much larger needs, the policies could be designed per-namespace, and tracked between regions.

Policy specification

Policies are expressed by a combination of rules Note that the deny rule will preside over any conflicting capability.

Nomad accepts a JSON payload with the name and description of a policy, along with a quoted JSON or HCL document with rules, like the following.

{
  "Description": "Agent and node management",
  "Name": "infrastructure",
  "Rules": "{\"agent\":{\"policy\":\"write\"},\"node\":{\"policy\":\"write\"}}"
}

This policy matches what we have in the table above. Create an infrastructure.json with the content above for use in the next step.

TIP:

To avoid error-prone quoting, one could write the policies in YAML:

Name: infrastructure
Description: Agent and node management
Rules:
  agent:
    policy: write
  node:
    policy: write

And then, convert them to JSON with the necessary quoting, by:

$ yaml2json < infrastructure.yaml | jq '.Rules = (.Rules | @text)' > infrastructure.json

Adding a policy

To add the policy, simply make an HTTP POST request to the server. The NOMAD_TOKEN below is the "management" token that we first created.

vagrant@nomad:~$ curl \
    --request POST \
    --data @infrastructure.json \
    --header "X-Nomad-Token: ${NOMAD_TOKEN}" \
    https://127.0.0.1:4646/v1/acl/policy/infrastructure

vagrant@nomad:~$ nomad acl policy list
Name            Description
infrastructure  Agent and node management

vagrant@nomad:~$ nomad acl policy info infrastructure
Name        = infrastructure
Description = Agent and node management
Rules       = {"agent":{"policy":"write"},"node":{"policy":"write"}}
CreateIndex = 425
ModifyIndex = 425

Creating a token for a policy

We now create a token for the infrastructure policy, and attempt a few operations with it:

vagrant@nomad:~$ nomad acl token create \
    -name='devops-team' \
    -type='client' \
    -global='true' \
    -policy='infrastructure'

Accessor ID  = 927ea7a4-e689-037f-be89-54a2cdbd338c
Secret ID    = 26832c8d-9315-c1ef-aabf-2058c8632da8
Name         = devops-team
Type         = client
Global       = true
Policies     = [infrastructure]
Create Time  = 2018-02-15 19:53:59.97900843 +0000 UTC
Create Index = 432
Modify Index = 432

vagrant@nomad:~$ export NOMAD_TOKEN='26832c8d-9315-c1ef-aabf-2058c8632da8' # change the token to the new one with the "infrastructure" policy attached
vagrant@nomad:~$ nomad status
Error querying jobs: Unexpected response code: 403 (Permission denied)

vagrant@nomad:~$ nomad node-status
ID        DC   Name   Class   Drain  Status
1f638a17  dc1  nomad  <none>  false  ready

As you can see, anyone with the devops-team token will be allowed to run operations on nodes, but not on jobs -- i.e. on namespace resources.

Where to go next

The example above demonstrates adding one of the policies from our list at the beginning. Adding the rest of them and trying different commands could be a good exercise.

As a reference, the FP Complete team maintains a repository with policies ready for use.

Do you like this blog post and need help with DevOps, Rust or functional programming? Contact us.

Share this