gifnoc

Configuration, turned on its head

gifnoc

Configuration, turned on its head

Overview

Gifnoc is a secure, cloud hosted configuration provider.

Terminology

Record

A single configuration item. A Record holds the following attributes:

Attribute Meaning
Token Record’s Token (see below)
Value Record’s value
UUID A unique identifier for the record
TTL The time to live for the Record in memory cache on the Proxy side
Secret Boolean: Is the Record holding an encrypted value or not
CreatedAt When the Record was created
UpdatedAt When the Record was last updated
LastAccess When the Record value was last read by a client

Token

The key to retrieve a Recrod. This could be something like eu.production.database.username.

Tokens are made up of 1 or more “segments” (eu,production,database and username are each a segment). A segement can be either a word (like production) or a wildcard *. Using a wildcard means the Record is returned as a match for a query with any value instead of that segment. Here is an example:

eu.*.database.username is a match for all of the following:

eu.production.database.username
eu.staging.database.username
eu.qa.database.username

A Token can have as many as 10 segments and as many wildcard segments.

Value

Payload of the configuration Record. If the Record is a secret (has Secret set to true), this value is encrypted and only decrypted at the Proxy when needed.

Namespace

Records are namespaced to allow multi-tenancy or testing with a single server.

Security Provider

Security Provider is a plugin that provides encryption and decryption of Values for Records at Proxy. Currently there is only support for GCP KMS. Code tests in gifnoc use an xor Security Provider that uses a boolean xor to “encrypt” and “decrypt” the values for test.

Architecture

Gifnoc has 3 pieces: Server, Proxy and Client. Client can be in any langugae. gifnoc comes with a command line interface (CLI) for use in POSIX shells.

Server

Server is a daemon that provides RESTful API backed by a cloud hosted database server like MySQL and is responsible for read and write of the Records into the database.

Proxy

Proxy is a daemon running at the point of consumption of configurations. It connects to the server from one side and gets the Records in bulk and serves them to any local caller over a variety of protocols including RESTful API or FUSE file system. Proxy allows offline work for development by caching the latest known Records.

Client

Is any client side code that calls the proxy to read and write the Records and Namespaces.

Security

Server doesn’t store any secrets unencrypted. It also doesn’t have the keys to decrypt any of the Values for the Records. All Values are trasnported to Proxy encrypted and the Proxy is responsible for decrypting them using a KMS (like Google or AWS).

Finding Record Matches

When a client calls gifnoc to get the value of a Record, it sends a specific query token. Queries cannot contain wildcards and should have specific values for each segment. For example eu.production.database.password is a valid query. gifnoc will try to find the closest single Token for this query. If it finds a specific Record then that is returned, otherwise it starts looking for a record that matches the query and has the least number of wildcard segments. If more than one Record is found with the same number of wildcard segments, then the query will fail with Not Found error.

Encryption and Decryption Keys

Each Namespace can have an Enc/Dec key (--keyuri option). This value is used as a key by the “Security Provider” to encrypt and decrypt the Value of a Record. The name of the Security Provider is in the scheme of the URI. For example the GCP KMS Security Provider uses Google Cloud’s KMS service for encryption. A Namespace using this Security Provider will have a keyuri of gcp://projects/my-project/locations/global/keyRings/foo/cryptoKeys/production. This is a key used by GCP KMS service. The gcp scheme tells gifnoc to use the Google KMS Security Provider and pass the key to it when needed. Currently only GCP KMS is supported.

Choosing the Encryption / Decryption Key

Sometimes you might want to use different keys for different Records. For example if you use the same Namespace for both staging and production environments of your application, you can include the environment name as a segment in the Tokens. Here is an example:

eu.production.database.name
eu.staging.database.name

In this example the second segment determins the application environment. To choose different keys for those 2 environments, you can use “segment placeholders” as the keyuri of a Namespace. Here is an example of a Key URI for this sample Namespace:

gcp://projects/my-project/locations/global/keyRings/foo/cryptoKeys/$2

Note the last part of the URI: The $2 at the end is a placeholder that is replaced with the Token’s second segment when the key is used for encryption and decryption. This means the key will be rendered as below:

Token Enc/Dec Key
eu.production.database.name gcp://projects/my-project/locations/global/keyRings/foo/cryptoKeys/production
eu.staging.database.name gcp://projects/my-project/locations/global/keyRings/foo/cryptoKeys/staging

Installation

The best way to run gifnoc server is in a container and preferrably using Kubernetes.

Server

  1. Clone the repository in your $GOPATH folder under $GOPATH//src/github.com/cloud66/gifnoc
  2. Build the gifnoc Docker image: docker build -t gifnoc .
  3. Push the image to your Docker repository (you might want to retag the image for this)
  4. Replace CHANGE_THIS in k8s.yml file with the appropriate values: your MySQL host address, the authorized email domain (see Security) and the Docker image name for gifnoc
  5. Add the TLS files to your cluster (see Security) with these commands:
$ kubectl create secret generic server-tls --from-file=ca.crt --from-file=server.crt --from-file=server.key -n gifnoc
$ kubectl create secret generic server-secrets --from-literal username=YOUR_MYSQL_USERNAME --from-literal password=YOUR_MYSQL_PASSWORD
  1. Apply the file to your cluster: kubectl apply -f k8s.yml

NOTE: The k8s.yml is an example for the setup and uses load balancers which means it works on AWS and Google Cloud but you might want to change this if you’re running gifnoc on other Kubernetes clusters.

Proxy

On servers, it’s best to run gifnoc Proxy inside of a container. If your application is running in a Kubernetes cluster, then you might want to run gifnoc Proxy as a “sidecar” container in the application pod for more security instead of an internal cluster service.

Offline Proxy mode

To allow developers use gifnoc when offline, the Proxy can run in offline mode. When started in offline mode, the Proxy will try to fetch the entire configuration set from the server and store it locally. It then serves all read requests from the local database and passes all the write requests up to the server. All encryption still happens on the Proxy so offline mode will not work with Security Providers that require online access like GCP KMS. It is however possible to use the Proxy caching to store the decrypted values in memory after online connection is lost.

Doing reads locally and writes remotely means the Proxy will not have any updated information in the local storage. This is to avoid complicating the system with synchronisation requirements when the only usecase is offline development mode. To refresh the local Proxy simply restart it. A restart will clean the local copy and repopulate it with data from the remote server.

Client / CLI

Currently there is only a Mac (OSX) native version of the client available. To install on a development machine (with offline support), use homebrew:

$ brew tap cloud66/tap
$ brew install gifnoc --token=cloud66

To start the proxy on every login, use brew services:

$ brew services start gifnoc

You need client authentication keys (ca.crt, client.crt and client.key) to use gifnoc.

To test the proxy:

$ gifnoc namespaces list

Configuration

Gifnoc uses MySQL as a backend for the Server and sqlite3 as the offline storage for the Proxy component. To configure the server, you can use any MySQL as a service provided by all major cloud providers (RDS on AWS or CloudSQL on GCP for example).

To opearate, Server requires the following:

By default, gifnoc looks in ~/.gifnoc/tls for the TLS CA, key and the certificate. The paths and filenames can be changed in the config file.

Here is an example for a Server config file:

apiconf:
  binding: 127.0.0.1
  port: 9810
  forceexternalbinding: false
serverconf:
  user: root
  password: ""
  dbhost: tcp(localhost:3306)
  database: gifnoc
  serverkeyfile: /Users/foo/server-key.pem
  servercertfile: /Users/foo/server.pem
  admindomain: acme.com
loglevel: debug
cacertfile: /Users/foo/ca.pem

user and password replace MYSQL_USERNAME and MYSQL_PASSWORD if present.

To pass the configuration file to the Server, use the --config option:

$ gifnoc --config server.yml start --runas server

To see a list of available configuration options, use the --help option:

$ gifnoc --help

Setup of Proxy

Once you have the server running, you can start the Proxy. Here is an example configuration file for the Proxy:

apiconf:
  binding: 127.0.0.1
  port: 9820
  forceexternalbinding: false
proxyconf:
  upstream: "https://acme.gifnoc.com:9810"
  dbfile: gifnoc.db
  backupdbfile: gifnoc.db.back
  forcesync: false
  ignorecertificateissues: true
  clientcertfile: /Users/foo/foo.pem
  clientkeyfile: /Users/foo/foo-key.pem
loglevel: debug
cacertfile: /Users/foo/ca.pem

upstream should point to the running Server.

TLS Files

gifnoc uses TLS for security of communication and client certificates for client authentication. As such a working solution will require a full set of certificates and keys to operate. These files can be generated using tools like openssl, easyrsa or cfssl (recommended).

cfssl

Gifnoc uses client certificates for authentication. While you can use any CA, Server and Client certificates for this purpose, I recommend using a tool like cfssl to generate all the needed files.

To install cfssl on OSX, use brew:

$ brew install cfssl

For a new setup, you can use a CSR file like the given csr.json (sample below):

{
	"CN": "cloud66.gifnoc.com",
	"hosts": [
		"127.0.0.1",
		"localhost",
		"cloud66.gifnoc.com"
	],
	"key": {
		"algo": "rsa",
		"size": 2048
	},
	"names": [
		{
			"C":  "US",
			"L":  "San Francisco",
			"O":  "Cloud66 Inc.",
			"OU": "Eng",
			"ST": "California"
		}
	]
}

$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca

This generates a set of files beginning with ca that are your Root Certificate files. Keep them safe!

Now you need to generate your server certificate files. You can use the given ca-config.json file:

cfssl gencert \
    -ca=ca.pem \
    -ca-key=ca-key.pem \
    -config=ca-config.json \
    -profile=server \
    apiserver-csr.json | cfssljson -bare server

This generates your server key and certificate files.

Using these files you can start your server.

Now you can generate client certificates. You can use the given client-csr.json after modifying the email address (sample below):

{
	"CN": "foo@cloud66.com",
	"hosts": [
		""
	],
	"key": {
		"algo": "rsa",
		"size": 2048
	},
	"names": [
		{
			"C": "US",
			"L": "San Francisco",
			"O": "Cloud66 Inc.",
			"OU": "Eng",
			"ST": "California"
		}
	]
}
cfssl gencert \
    -ca=ca.pem \
    -ca-key=ca-key.pem  \
    -config=ca-config.json \
    -profile=client \
    client-csr.json | cfssljson -bare user1

NOTE: Change the name! This generates a set of files for each client. Give it to each client securely.

This is taken from here

While authentication happens using the client certificates, authorization is based on the email domain used in the client certificate. This should match the admindomain attribute of your configuration file.

CLI

With the Server and Proxy setup, you can use gifnoc executable as a command line interface to work with the system. To get help and a list of supported commands, use gifnoc --help. Here is a summary of some of the commands you can use with gifnoc CLI:

List all namespaces

$ gifnoc namespaces list

This is equal to

$ gifnoc ns ls

List all records in a given namespace

$ gifnoc records list --namespace foo

This is equal to

$ gifnoc rec ls -n foo

Get more information on namespaces or records

$ gifnoc -o wide namespace list
$ gifnoc -o wide record list --namespace foo

The --output or -o option determines the output format and can be text (default), wide (verbose) and json.

Get details of a namepsace or record

$ gifnoc namespace get foo
$ gifnoc record get bar --namespace foo

Create a new namespace

$ gifnoc namespace add --name foo --keyuri xor://secret --delete-protection

Add a new record

$ gifnoc record add --namespace foo --token production.eu.some.item --secret=false --from-literal=$(echo -n 'hello' | base64)

As you can see, gifnoc requires the value to be base64 encoded.

You can also add the value for a record from the contents of a file. The content in this case will be converted to base64 encoding automatically by the CLI:

$ gifnoc record add --namespace foo --token production.eu.some.other.item --secret=true --from-file=/here/is/my/production/secret

Starting gifnoc in Server mode

To start gifnoc in Server mode, use the start command with server for --runas:

$ gifnoc start --runas server

Starting gifnoc in Proxy mode

To start gifnoc in Proxy mode, use start command with proxy for --runas:

$ gifnoc start --runas proxy

In Proxy mode, you have the following options:

Option Description
--force-external-binding By default gifnoc binds only to loopback (127.0.0.1). This overrides this behaviour
--upstream Which Server to use
--ignore-cert-errors Let’s Proxy ignore certificate issues related to the FQDN of the certificate not matching the Server name
--offline Run the Proxy in offline mode
--no-cache Do not hold any decrypted values in memory

API

Both Server and Proxy use the same API. Server expects values to be encrypted when received and sends them back as they are stored in the database. Proxy encrypts the values based on their Namespace’s Security Provider and decrypts them before sending them back to the final caller.

Endpoints

# General
GET /.ping
GET /.version
GET /.info

# Records
GET    /namespaces/:namespace/records
GET    /namespaces/:namespace/records/#id
POST   /namespaces/:namespace/records
DELETE /namespaces/:namespace/records/#id
PUT    /namespaces/:namespace/records/#id

# Namespace
GET    /namespaces
GET    /namespaces/:namespace
POST   /namespaces
DELETE /namespaces/:namespace
PUT    /namespaces/:namespace

Data Models

Namespace

{
	"uuid": "2aca8256-e0e5-41cb-b077-8d514fcdf859",
	"name": "app-secrets",
	"key_uri": "gcp://projects/my-project/locations/global/keyRings/acme/cryptoKeys/$3",
	"delete_protection": true,
	"record_count": 59,
	"created_at": "Jan 11 09:10",
	"updated_at": "Jan 11 09:10"
}

Record

{
	"token": "foo.bar.production",
	"raw_value": "Zm9v",
	"uuid": "42d6c2b4-a3bb-4757-bc0d-7d00fd05bc9f",
	"ttl": "3600",
	"key_uri": "gcp://projects/my-project/locations/global/keyRings/acme/cryptoKeys/production",
	"value": "Zm9v",
	"secret": false,
	"created_at": "Jan 11 09:10",
	"updated_at": "Jan 11 09:10",
	"last_access": "Jan 11 09:10"
}

Sample Clients

Go

gifnoc is written in Go and the codebase contains a Go client for it which can be used by importing it as a package.

Ruby

Here is a simple gifnoc client in Ruby, using the HTTParty gem:

require 'httparty'

module Gifnoc

	class Client
		include HTTParty
		base_uri "http://#{ENV["GIFNOC_HOST"] || "localhost"}:9820" # on almost all cases and envs, gifnoc proxy is running on localhost
		NS_UUID = ENV['GIFNOC_NAMESPACE']

		def self.namespaces
			self.get("/namespaces")
		end

		def self.namespace(name)
			ns = self.namespaces
			ns.select { |x| x['name'] == name }
		end

		def self.records(namespace_uuid = Gifnoc::Client::NS_UUID)
			self.get("/namespaces/#{namespace_uuid}/records")
		end

		def self.record(namespace_uuid = Gifnoc::Client::NS_UUID, token)
			self.get("/namespaces/#{namespace_uuid}/records/#{token}")
		end

		def self.value(namespace_uuid = Gifnoc::Client::NS_UUID, token)
			rec = self.record(namespace_uuid, token)
			if rec
				rec['value']
			else
				nil
			end
		end

	end

end

CURL

$ curl http://localhost:9820/namespaces
$ curl http://localhost:9820/namespaces/3aca8296-e015-41db-b087-8d590fcda859