We hope you find this tutorial helpful. In addition to guides like this one, we provide simple cloud infrastructure for developers. Learn more →

How To Automatically Add New Droplets to your Configuration Management System

PostedFebruary 26, 2015 13.2k views API Chef Configuration Management

Introduction

Using the DigitalOcean metadata service, administrators can provide instructions that allow new servers to configure themselves automatically. While this is useful, many organizations like to handle all of their infrastructure configuration within a configuration management tool like Chef or Puppet.

In this guide, we will demonstrate how to bootstrap a DigitalOcean server using the metadata service and CloudInit to connect to an existing configuration management deployment. The actual configuration of the server can then be handled by the config management service. We will demonstrate how to bootstrap both Chef and Puppet nodes.

Prerequisites

In order to complete this guide, you will have to have some familiarity with the DigitalOcean metadata service. You can find out more about how to enter information into and retrieve information from the metadata service in this guide.

This guide will leverage a type of script called cloud-config that is consumed at first boot by the CloudInit service on your Droplet in order to perform first-run configuration. You should get some basic familiarity with cloud-config scripts, their syntax, and behavior in order to better understand how to modify the scripts presented in this guide. You can find an introduction to cloud-config scripting here. For a more practical example (along with some discussion on the limitations of the format), you can read our guide on performing some basic tasks using cloud-config here.

Using Cloud-Config Scripts to Bootstrap a Chef Node

Using the DigitalOcean metadata service, you can easily hook your new servers into an existing Chef-controlled infrastructure with cloud-config scripting.

To add your new server to this system, you must already have a Chef server configured that your new server can contact to receive configuration instructions. If you need help deploying a Chef server and management workstation, you can follow this guide to get started.

General Plan

When a new server is brought online, it must be brought under the control of the Chef server. Typically, this may be accomplished by connecting to the new server with the knife management command and using the bootstrap subcommand. This would connect to the new server, install the Chef client and the validation credentials that allow the new node to connect to the Chef server. Afterwards, the Chef client connects to the server, validates itself, receives new client credentials, pulls down its configuration from the server, and performs any actions necessary to bring itself into the desired state.

In this guide, we will use a cloud-config script to replace the manual bootstrapping step, allowing the new node to automatically connect to the Chef server, validate itself, receive client credentials, and perform an initial Chef client run. The server will do this automatically at first boot without any manual assistance from the administrator.

Gathering the Necessary Data from the Knife Config File

In order for our cloud-config script to successfully bootstrap, it will need access to the credentials typically available to the knife command. Specifically, we need the following pieces of information:

  • The Chef validation name
  • The validation key
  • The URL where the Chef server can be reached

All of this information is available, in the correct format, in the knife configuration file on workstation used to manage the Chef infrastructure. Inside of the Chef repo, there should be a hidden directory called .chef which contains this file.

Assuming that your Chef repo is located in your home directory on the workstation and is called chef-repo, you can output the contents of the file by typing:

cat ~/chef-repo/.chef/knife.rb

The pieces of information you need are highlighted below:

current_dir = File.dirname(__FILE__)
log_level                :info
log_location             STDOUT
node_name                "jellingwood"
client_key               "#{current_dir}/jellingwood.pem"
validation_client_name   "digitalocean-validator"
validation_key           "#{current_dir}/digitalocean-validator.pem"
chef_server_url          "https://your_server.com/organizations/digitalocean"
syntax_check_cache_path  "#{ENV['HOME']}/.chef/syntaxcache"
cookbook_path            ["#{current_dir}/../cookbooks"]

The validation name and the Chef server URL can be taken directly as-is from the file. Copy these values so that you can use them in the cloud-config file.

The validation_key points to the location where the actual key is kept. In the above example, this indicates that it is located in the same directory as the knife.rb file and is called digitalocean-validator.pem. This will likely be different for your configuration.

We need to contents of this file, so use the cat command again. Modify the command to point it to the location given for your validator key:

cat ~/chef-repo/.chef/digitalocean-validator.pem

You will see an RSA private key:

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA3O60HT5pwEo6xUwcZ8WtExBUhoL3bTjlsvHVXg1JVmBUES+f
V9jLu2N00uSZEDZneCIQyHLBXnqD/UNvWEPNvPzt1ecXzmw2BytB7lPDW4/F/8tJ
vAVrKqC7B04VFGmcFY2zC8gf8BWmX8CNRDQooM7UO5OWe/H6GDGPPRIITerO3GrU

. . .

sWyRAoGBAKNc/ZUM8ljRV0UJxQ9nbdozXRZjtUaNgXMNiw+oP2HYYdHrlkKnGHYJ
Js63rvjpq8pocjE8YI+2H0v4/4uWqW8GEBfrWbLMzGsYPnRyiHR5+hgjCUU50RB3
eFoNbURwLYcq2Z/IAQZpDpJWpofz3OVMpMXtei1cIflrAAd2wtWO
-----END RSA PRIVATE KEY-----

Copy the entirety of the validation key so that you can use it in the cloud-config script momentarily.

Basic Cloud-Config Chef Client Installation

Once you have the data above, you can build out the script. Chef configuration can be accomplished through a dedicated cloud-config module called chef. The cloud-config must contain valid YAML and must have #cloud-config as the first line of the script.

Starting off, your script will look like this:

#cloud-config
chef:

The cloud-config documentation claims to be able to install the Chef client either from a Ruby gem, a package, or using the traditional "omnibus" installation method. However, in practice, both the gem and package methods tend to fail, so we will use the "omnibus" method. Although it is usually not necessary, we will also explicitly list the location of the omnibus installer.

We will set force_install to "false". This way, if for some reason the Chef client is already installed on the image (for instance, if you are deploying from a snapshot), the client will not be reinstalled. So far, our script looks like this:

#cloud-config
chef:
  install_type: "omnibus"
  omnibus_url: "https://www.opscode.com/chef/install.sh"
  force_install: false

Next, we have the option of selecting a name for the new server within the Chef infrastructure by using the node_name directive. If you do not set this, Chef will use the server's hostname, so this is optional. However, this must be unique in your Chef environment.

Afterwards, we can add all of the connection information that we took from our Chef workstation. We will set the server_url option to the location of the Chef server exactly as it was in the knife.rb file. The same is true for the validation_name option.

For the validation key, we will use the YAML pipe symbol (|) to enter the entire validation key that we found on the workstation:

#cloud-config
chef:
  install_type: "omnibus"
  omnibus_url: "https://www.opscode.com/chef/install.sh"
  force_install: false
  node_name: "new_node"
  server_url: "https://your_server.com/organizations/digitalocean"
  validation_name: "digitalocean-validator"
  validation_key: |
    -----BEGIN RSA PRIVATE KEY-----
    MIIEowIBAAKCAQEA3O60HT5pwEo6xUwcZ8WtExBUhoL3bTjlsvHVXg1JVmBUES+f
    V9jLu2N00uSZEDZneCIQyHLBXnqD/UNvWEPNvPzt1ecXzmw2BytB7lPDW4/F/8tJ
    vAVrKqC7B04VFGmcFY2zC8gf8BWmX8CNRDQooM7UO5OWe/H6GDGPPRIITerO3GrU

    . . .

    sWyRAoGBAKNc/ZUM8ljRV0UJxQ9nbdozXRZjtUaNgXMNiw+oP2HYYdHrlkKnGHYJ
    Js63rvjpq8pocjE8YI+2H0v4/4uWqW8GEBfrWbLMzGsYPnRyiHR5+hgjCUU50RB3
    eFoNbURwLYcq2Z/IAQZpDpJWpofz3OVMpMXtei1cIflrAAd2wtWO
    -----END RSA PRIVATE KEY-----

At this point, your script has all of the authentication needed to connect to your Chef server and create client credentials.

Configuring Chef Environment, run_list, and Attributes

While the above details provides enough information for the client to connect to the Chef server, we haven't given the node any information about how to actually configure itself. We can provide this information in the cloud-config script as well.

To specify the environment that the new node should be placed in, use the environment option. If this is not set, the _default environment will be set, which is the generic default for Chef nodes that have not been given another environment.

chef:
  environment: "staging"

Our run_list can be specified as a simple list of items that the client should apply in order. These can be either recipes or roles.

chef:
  run_list:
    - "recipe[lamp]"
    - "role[backend-web]"

You can specify the new node's initial attributes using an initial_attributes hierarchy. This will set the initial attributes that will affect how the run_list is applied:

chef:
  initial_attributes:
    lamp:
      apache:
        port: 80
      mysql:
        username: webclient
        pass: $#fjeaiop34S

When hooked up to the previous cloud-config script, it might look something like this:

#cloud-config
chef:
  install_type: "omnibus"
  omnibus_url: "https://www.opscode.com/chef/install.sh"
  force_install: false
  node_name: "new_node"
  server_url: "https://your_server.com/organizations/digitalocean"
  validation_name: "digitalocean-validator"
  validation_key: |
    -----BEGIN RSA PRIVATE KEY-----
    MIIEowIBAAKCAQEA3O60HT5pwEo6xUwcZ8WtExBUhoL3bTjlsvHVXg1JVmBUES+f
    V9jLu2N00uSZEDZneCIQyHLBXnqD/UNvWEPNvPzt1ecXzmw2BytB7lPDW4/F/8tJ
    vAVrKqC7B04VFGmcFY2zC8gf8BWmX8CNRDQooM7UO5OWe/H6GDGPPRIITerO3GrU

    . . .

    sWyRAoGBAKNc/ZUM8ljRV0UJxQ9nbdozXRZjtUaNgXMNiw+oP2HYYdHrlkKnGHYJ
    Js63rvjpq8pocjE8YI+2H0v4/4uWqW8GEBfrWbLMzGsYPnRyiHR5+hgjCUU50RB3
    eFoNbURwLYcq2Z/IAQZpDpJWpofz3OVMpMXtei1cIflrAAd2wtWO
    -----END RSA PRIVATE KEY-----
  environment: "staging"
  run_list:
    - "recipe[lamp]"
    - "role[backend-web]"
  initial_attributes:
    lamp:
      apache:
        port: 80
      mysql:
        username: webclient
        pass: $#fjeaiop34S

Redirecting Output and Configuring Chef Client Run

The above script contains all of the information needed under the chef: section. However, there are a few other things we should do using some other cloud-config modules.

First, we should specify that we wish to redirect the output from every command and subcommand into the CloudInit process's output log. This is located at /var/log/cloud-init-output.log by default. We can do this with the output module like this:

output: {all: '| tee -a /var/log/cloud-init-output.log'}

The other thing we want to do is set the Chef client up to actually run once it has been installed and configured. At the time of this writing, the omnibus installation method does not do this automatically.

We can force this behavior by waiting until the chef-client executable is installed on the server before calling the command. Using a simple bash loop, we will check for the existence of this file every five seconds. When it is found, we will run chef-client in order to implement the initial configuration we have specified.

The runcmd module can be used to issue arbitrary commands. It is the ideal location for our bash loop:

runcmd:
  - while [ ! -e /usr/bin/chef-client ]; do sleep 5; done; chef-client

Also, optionally, you can add another cloud-config directive to null-route the metadata endpoint after the first boot. This is useful because we are putting a private key in our user data. Without null-routing the metadata endpoint, this would be accessible to any user on the server. Implement this by adding:

disable_ec2_metadata: true

Combining these with the script we've constructed thus far, we can get the complete script necessary to bootstrap our node and connect it to our Chef infrastructure:

#cloud-config
chef:
  install_type: "omnibus"
  omnibus_url: "https://www.opscode.com/chef/install.sh"
  force_install: false
  node_name: "new_node"
  server_url: "https://your_server.com/organizations/digitalocean"
  validation_name: "digitalocean-validator"
  validation_key: |
    -----BEGIN RSA PRIVATE KEY-----
    MIIEowIBAAKCAQEA3O60HT5pwEo6xUwcZ8WtExBUhoL3bTjlsvHVXg1JVmBUES+f
    V9jLu2N00uSZEDZneCIQyHLBXnqD/UNvWEPNvPzt1ecXzmw2BytB7lPDW4/F/8tJ
    vAVrKqC7B04VFGmcFY2zC8gf8BWmX8CNRDQooM7UO5OWe/H6GDGPPRIITerO3GrU

    . . .

    sWyRAoGBAKNc/ZUM8ljRV0UJxQ9nbdozXRZjtUaNgXMNiw+oP2HYYdHrlkKnGHYJ
    Js63rvjpq8pocjE8YI+2H0v4/4uWqW8GEBfrWbLMzGsYPnRyiHR5+hgjCUU50RB3
    eFoNbURwLYcq2Z/IAQZpDpJWpofz3OVMpMXtei1cIflrAAd2wtWO
    -----END RSA PRIVATE KEY-----
  environment: "staging"
  run_list:
    - "recipe[lamp]"
    - "role[backend-web]"
  initial_attributes:
    lamp:
      apache:
        port: 80
      mysql:
        username: webclient
        pass: $#fjeaiop34S
output: {all: '| tee -a /var/log/cloud-init-output.log'}
runcmd:
  - while [ ! -e /usr/bin/chef-client ]; do sleep 5; done; chef-client
disable_ec2_metadata: true

The above script can be tweaked as necessary for each new server in your infrastructure.

Using Cloud-Config Scripts to Bootstrap a Puppet Node

If your infrastructure relies on Puppet for configuration management, you can use the puppet module instead. Like the Chef example, bootstrapping a Puppet node involves using cloud-config to attach the new server to the existing configuration management infrastructure.

Before you get started, you should have a Puppet master server configured for your infrastructure. If you need help getting a Puppet server up and running, check out this guide.

General Plan

When a new Puppet server is brought online, a Puppet agent is installed so that it can communicate with the Puppet master server. This agent is responsible for receiving and applying the information that dictates the desired state of the node. To do this, the agent connects with the master, uploads data about itself, pulls down the current catalog describing its desired state, and performs the actions necessary to reach that state.

Before this happens though, on its first run, the agent must register itself with the master server. It creates a certificate signing request and sends it to the master to sign. Typically, the agent will reconnect to the master periodically until the certificate is signed, but you can configure your Puppet to automatically sign incoming requests with certain characteristics if that is suitable for your environment (we will cover this later).

Using our cloud-config script, we will configure our new server with the information that it needs to connect to the master for the first time. At that point, it can retrieve configuration details from the Puppet master server in the form of a catalog.

Gathering the Necessary Data from the Puppet Master

The first thing we need to do prior to building our cloud-config file is gather the data from our Puppet master server that we will need to connect. We only need a few pieces of information.

First, you need to get the Puppet master server's fully qualified domain name (FQDN). You can do this by typing:

hostname -f

In most cases, it should return something like this:

puppet.example.com

You can also check your Puppet master configuration file to see if the dns_alt_names option is set:

cat /etc/puppet/puppet.conf
. . .

dns_alt_names = puppet,puppet.example.com

. . .

If your Puppet master's SSL certificates were generated after setting these options, they may be usable as well.

The other item that we need to collect is the Puppet master's certificate authority certificate. This can be found in either /var/lib/puppet/ssl/certs/ca.pem or /var/lib/puppet/ssl/ca/ca_crt.pem:

sudo cat /var/lib/puppet/ssl/certs/ca.pem

The results will look something like this:

-----BEGIN CERTIFICATE-----
MIIFXjCCA0agAwIBAgIBATANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFQdXBw
ZXQgQ0E6IHB1cHBldDAeFw8xNTAyMTkxOTA0MzVaFw0yMDAyMTkxOTA0MzVaMBwx
GjAYBgNVBAMMEVB1cHBldCBDQTogcHVwcGV0MIICIjANBgkqhkiG9w0BAQEFAAOC

. . .

arsjZT5/CtIhtP33Jl3mCp7U2F6bsk4/GDGRaAsFXjJHvBbL93NzgpkZ7elf0zUP
rOcSGrDrUuzuJk8lEAtrZr/IfAgfKKXPqbyYF95V1qN3OMY+aTcrK20XTydKVWSe
l5UfYGY3S9UJFrSn9aBsZzN+10HXPkaFKo7HxpztlYyJNI8UVSatcRF4aYYqt9KR
UClnR+2WxK5v7ix0CVd4/KpYH/6YivvyTwxrhjF2AksZKg==
-----END CERTIFICATE-----

Copy the certificate in its entirety. We will be including this in our cloud-config file so that our new servers can verify that they are connecting to the correct Puppet master.

Once you have these pieces of information, you can begin building the cloud-config file so that the new server can plug itself into the existing Puppet infrastructure.

Basic Cloud-Config Puppet Node Installation

The cloud-config configuration for new Puppet nodes is fairly simple. All Puppet-specific configuration is located within the puppet: section of the file. As with every cloud-config file, the very first line must contain #cloud-config on its own:

#cloud-config
puppet:

Beneath this, there are only two subsections. The first is the ca_cert key. This will use the pipe character to start a YAML text block so that the CA certificate can be given in its entirety as an indented block:

#cloud-config
puppet:
  ca_cert: |
    -----BEGIN CERTIFICATE-----
    MIIFXjCCA0agAwIBAgIBATANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFQdXBw
    ZXQgQ0E6IHB1cHBldDAeFw8xNTAyMTkxOTA0MzVaFw0yMDAyMTkxOTA0MzVaMBwx
    GjAYBgNVBAMMEVB1cHBldCBDQTogcHVwcGV0MIICIjANBgkqhkiG9w0BAQEFAAOC

    . . .

    arsjZT5/CtIhtP33Jl3mCp7U2F6bsk4/GDGRaAsFXjJHvBbL93NzgpkZ7elf0zUP
    rOcSGrDrUuzuJk8lEAtrZr/IfAgfKKXPqbyYF95V1qN3OMY+aTcrK20XTydKVWSe
    l5UfYGY3S9UJFrSn9aBsZzN+10HXPkaFKo7HxpztlYyJNI8UVSatcRF4aYYqt9KR
    UClnR+2WxK5v7ix0CVd4/KpYH/6YivvyTwxrhjF2AksZKg==
    -----END CERTIFICATE-----

Be sure to include the entire certificate along with the beginning and ending markers and to indent it appropriately.

The second section under the puppet: umbrella is the conf: section. This is used to specify key-value pairs that will be appended to a generic puppet.conf file. The key-value pairs should be placed under section headers as they would be in the puppet.conf file.

For instance, at the very least, the new server will need to know the address of the Puppet master server. In the puppet.conf file, this is found under the [agent] section, like this:

. . .

[agent]
server = puppet.example.com

. . .

To specify this in the cloud-config syntax, you would add this to what we have so far:

#cloud-config
puppet:
  ca_cert: |
    -----BEGIN CERTIFICATE-----
    MIIFXjCCA0agAwIBAgIBATANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFQdXBw
    ZXQgQ0E6IHB1cHBldDAeFw8xNTAyMTkxOTA0MzVaFw0yMDAyMTkxOTA0MzVaMBwx
    GjAYBgNVBAMMEVB1cHBldCBDQTogcHVwcGV0MIICIjANBgkqhkiG9w0BAQEFAAOC

    . . .

    arsjZT5/CtIhtP33Jl3mCp7U2F6bsk4/GDGRaAsFXjJHvBbL93NzgpkZ7elf0zUP
    rOcSGrDrUuzuJk8lEAtrZr/IfAgfKKXPqbyYF95V1qN3OMY+aTcrK20XTydKVWSe
    l5UfYGY3S9UJFrSn9aBsZzN+10HXPkaFKo7HxpztlYyJNI8UVSatcRF4aYYqt9KR
    UClnR+2WxK5v7ix0CVd4/KpYH/6YivvyTwxrhjF2AksZKg==
    -----END CERTIFICATE-----
  conf:
    agent:
      server: "puppet.example.com"

Note that the conf: section is in-line with the ca_cert section and not a child element. This is the bare minimum needed to connect to the Puppet master. Any additional configuration items found in puppet.conf can be added in a similar way by first creating a level for the section name and then defining the key-value pair.

After this, we should redirect all future output to the cloud-init-output.log file and add a runcmd line comparable to the one we added for the Chef config. This will wait until the Puppet agent is installed and then enable and restart it. We can also null-route the metadata endpoint after the first run like we did in the Chef section. These lines cloud-config directives should be placed outside of any other module sections:

. . .

  conf:
    agent:
      server: "puppet.example.com"
output: {all: '| tee -a /var/log/cloud-init-output.log'}
runcmd:
  - while [ ! -e /usr/bin/puppet ]; do sleep 5; done; puppet agent --enable; service puppet restart
disable_ec2_metadata: true

With this information, the new server can connect to the Puppet master server and then generate a client certificate signing request to transfer to the master. By default, client certificates must be manually signed on the Puppet master. Once this is done, at the next Puppet agent update interval (every 30 minutes by default), the node will pull down its configuration from the Puppet master. We will demonstrate a bit later how to implement a relatively secure auto-signing mechanism to avoid this delay.

Defining the Certname for the Node

One of the values that can be placed into the new server's puppet.conf file is a unique case. In the cloud-config file, the certname option can substitute values from the environment if certain variables are given. The following variables are recognized:

  • %i: The instance ID of the server. This will be taken from http://169.254.169.254/metadata/v1/id when the server is created. It corresponds to the Droplet ID used to uniquely identify Droplets.
  • %f: The FQDN of the server.

With this in mind, a common certname setting would look like this:

#cloud-config
puppet:

. . .

  conf:
    agent:
      server: "puppet.example.com"
      certname: "%i.%f"

This would produce a certname with a pattern similar to this:

   |-Droplet ID
   |
   |            |-Fully Qualified Domain Name
   |            |
|-----||-------------------|
123456.testnode.example.com

Having Droplet ID as part of the certname can be useful for configuring secure Puppet auto-signing as we will see in the next section.

Implement Puppet Certificate Auto Signing

If you wish to implement a certificate auto-signing system to avoid the need for administrator intervention, there are a few options. You must set this up on your Puppet master server first.

In the puppet.conf file on the Puppet master server, you can set the autosign option under the [master] section of the file. This can take a few different values:

  • true: This will tell the Puppet master server to sign every certificate request that comes in, without doing any checks. This is extremely dangerous in a real environment because any host can get a CSR signed and enter your infrastructure.
  • <whitelist_filename>: The second option is to specify a file that will function as a whitelist of hosts or host regular expressions. The Puppet master will check certificate signing requests against this list to see if the certificate should be signed. This is again not recommended as the certificate names can be spoofed easily.
  • <policy_executable>: The third option is to specify a script or executable that can be run to determine whether the certificate signing request should be signed. Puppet will pass the certname in as an argument and the entire CSR in through standard input. If an exit status of 0 is returned, the certificate is signed. If another status is given, the certificate will not be signed.

Policy-based auto-signing is the most secure way to implement automatic key signing because it allows you to be arbitrarily complex in how you distinguish between legitimate and non-legitimate requests.

To demonstrate policy-based auto-signing, you can add the certname variable to your cloud-config that includes the %i instance ID variable. We will use %i.%f so that it also includes the hostname selected as well:

#cloud-config
puppet:
  conf:
    agent:
      server: "puppet.example.com"
      certname: "%i.%f"
  ca_cert: |

   . . .

Your complete cloud-config may now look something like this:

#cloud-config
puppet:
  conf:
    agent:
      server: "puppet.example.com"
      certname: "%i.%f"
  ca_cert: |
    -----BEGIN CERTIFICATE-----
    MIIFXjCCA0agAwIBAgIBATANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFQdXBw
    ZXQgQ0E6IHB1cHBldDAeFw8xNTAyMTkxOTA0MzVaFw0yMDAyMTkxOTA0MzVaMBwx
    GjAYBgNVBAMMEVB1cHBldCBDQTogcHVwcGV0MIICIjANBgkqhkiG9w0BAQEFAAOC

    . . .

    arsjZT5/CtIhtP33Jl3mCp7U2F6bsk4/GDGRaAsFXjJHvBbL93NzgpkZ7elf0zUP
    rOcSGrDrUuzuJk8lEAtrZr/IfAgfKKXPqbyYF95V1qN3OMY+aTcrK20XTydKVWSe
    l5UfYGY3S9UJFrSn9aBsZzN+10HXPkaFKo7HxpztlYyJNI8UVSatcRF4aYYqt9KR
    UClnR+2WxK5v7ix0CVd4/KpYH/6YivvyTwxrhjF2AksZKg==
    -----END CERTIFICATE-----
output: {all: '| tee -a /var/log/cloud-init-output.log'}
runcmd:
  - while [ ! -e /usr/bin/puppet ]; do sleep 5; done; puppet agent --enable; service puppet restart
disable_ec2_metadata: true

On the Puppet master server, we will have to set up a validation script. Since Ruby is already installed for Puppet, we can make a simple Ruby script.

Because we are using the %i.%f format for the certname, we can check whether the first part of the certname (the part before the first dot) corresponds with a valid Droplet ID for our account. This is just a simple check which, in practice doesn't do much more than the white list file. However, you can adapt this idea to be much more complex if you wish.

To do this, we will need a personal access token from the "Apps & API" section of the DigitalOcean control panel. You will also need to install one of the DigitalOcean Ruby libraries. Below, we will show you some simplified scripts that use the Barge and DropletKit DigitalOcean Ruby clients.

If you wish to use the Barge client, install the gem on your Puppet master by typing:

sudo gem install barge

The following script can be used to check whether the first portion of the certname in the certificate signing request corresponds with a valid Droplet ID:

#!/usr/bin/env ruby

require 'barge'

TOKEN = 'YOUR_DIGITALOCEAN_API_TOKEN'

droplet_ids = []
certname = ARGV[0]
id_string = certname.slice(0...(certname.index('.')))
id_to_check = id_string.to_i

client = Barge::Client.new(access_token: TOKEN)
droplets = client.droplet.all

droplets.droplets.each do |droplet|
        droplet_ids << droplet.id
end

Kernel.exit(droplet_ids.include?(id_to_check))

If you instead wish to use DropletKit, the official DigitalOcean Ruby client, you can install the gem by typing:

sudo gem install droplet_kit

Note that DropletKit gem is only valid for Ruby 2.0 and above, so this might not be a possibility when using the version of Ruby that comes with Puppet.

The script for DropletKit can be adapted like this:

#!/usr/bin/env ruby

require 'droplet_kit'

TOKEN = 'YOUR_DIGITALOCEAN_API_TOKEN'

droplet_ids = []
certname = ARGV[0]
id_string = certname.slice(0...(certname.index('.')))
id_to_check = id_string.to_i

client = DropletKit::Client.new(access_token: TOKEN)
droplets = client.droplets.all

droplets.each do |droplet|
        droplet_ids << droplet.id
end

Kernel.exit(droplet_ids.include?(id_to_check))

You can place the script that corresponds to the gem you installed in a file called /etc/puppet/validate.rb and mark it as executable by typing:

sudo chmod +x /etc/puppet/validate.rb

You can then add the following to your puppet.conf file (located at /etc/puppet/puppet.conf if using Open Source Puppet):

. . .

[master]
autosign = /etc/puppet/validate.rb

. . .

Restart the Apache service to implement the new signing policy:

sudo service apache2 restart

Now, when certificate signing requests are received by your Puppet master, it will check whether the first part of the certificate name corresponds with a valid Droplet name in your account. This is a rough example of how you can validate requests using an executable.

Conclusion

By leveraging cloud-config scripts, you can easily bootstrap your new servers and hand them off to your existing configuration management systems. This allows you to control your infrastructure immediately through your existing tools prior to making important changes outside of scope of your management solution.

Creative Commons License