From 35f0e9e657f54c4450ca48e87769fac2aec8fc8a Mon Sep 17 00:00:00 2001 From: Lucas Roesler Date: Tue, 17 Oct 2017 11:33:31 +0200 Subject: [PATCH] Clean up docs and refactor createhandler **What** - Add a description for the secret key to the api swagger spec. - Remove optional examples from the secret management guide. - Update the ApiKeyProtected README to point at the new guide. - Refactor the `makeSpec` function to accept the already assembled secrets array because this should be easier to unit test. Signed-off-by: Lucas Roesler --- api-docs/swagger.yml | 1 + gateway/handlers/create_handler.go | 14 ++-- guide/secure_secret_management.md | 81 ++++------------------ sample-functions/ApiKeyProtected/README.md | 4 +- 4 files changed, 21 insertions(+), 79 deletions(-) diff --git a/api-docs/swagger.yml b/api-docs/swagger.yml index 2e6db16d..a412a469 100644 --- a/api-docs/swagger.yml +++ b/api-docs/swagger.yml @@ -229,6 +229,7 @@ definitions: type: array items: type: string + description: An array of names of secrets that are required to be loaded from the Docker Swarm. registryAuth: type: string description: >- diff --git a/gateway/handlers/create_handler.go b/gateway/handlers/create_handler.go index de4c13ab..31385da5 100644 --- a/gateway/handlers/create_handler.go +++ b/gateway/handlers/create_handler.go @@ -53,7 +53,7 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie options.EncodedRegistryAuth = auth } - spec, err := makeSpec(c, &request, maxRestarts, restartDelay) + secrets, err := makeSecretsArray(c, request.Secrets) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) @@ -61,6 +61,8 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie return } + spec := makeSpec(&request, maxRestarts, restartDelay, secrets) + response, err := c.ServiceCreate(context.Background(), spec, options) if err != nil { log.Println("Error creating service:", err) @@ -72,8 +74,7 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie } } -func makeSpec(c *client.Client, request *requests.CreateFunctionRequest, maxRestarts uint64, restartDelay time.Duration) (swarm.ServiceSpec, error) { - linuxOnlyConstraints := []string{"node.platform.os == linux"} +func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64, restartDelay time.Duration, secrets []*swarm.SecretReference) swarm.ServiceSpec { constraints := []string{} if request.Constraints != nil && len(request.Constraints) > 0 { @@ -101,11 +102,6 @@ func makeSpec(c *client.Client, request *requests.CreateFunctionRequest, maxRest }, } - secrets, err := makeSecretsArray(c, request.Secrets) - if err != nil { - return swarm.ServiceSpec{}, err - } - spec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: request.Service, @@ -142,7 +138,7 @@ func makeSpec(c *client.Client, request *requests.CreateFunctionRequest, maxRest spec.TaskTemplate.ContainerSpec.Env = env } - return spec, nil + return spec } func buildEnv(envProcess string, envVars map[string]string) []string { diff --git a/guide/secure_secret_management.md b/guide/secure_secret_management.md index 8967e75a..41e6baed 100644 --- a/guide/secure_secret_management.md +++ b/guide/secure_secret_management.md @@ -3,15 +3,7 @@ OpenFaaS deploys functions as Docker Swarm Services, as result there are several features that we can leverage to simplify the development and subsquent deployment of functions to hardened production environments. ## Using Environment Variables -First, and most simple, is the ability to set environment variables at deploy time. For example, you might want to set the `NODE_ENV` or `DEBUG` variable. Setting the `NODE_ENV` while using the `faas-cli` might look like this - -```sh -$ faas-cli deploy -f ./samples.yml -$ fass-cli invoke nodehelloenv -Hello from a production machine -``` - -Where your `samples.yml` stack file looks like this +First, and least secure, is the ability to set environment variables at deploy time. For example, you might want to set the `NODE_ENV` or `DEBUG` variable. Setting the `NODE_ENV` in the stack file `samples.yml` ```yaml provider: name: faas @@ -26,24 +18,18 @@ functions: NODE_ENV: production ``` - -(_Optional_) If you are directly using the OpenFaaS Gateway API, then it will look like this - +You can then deploy and invoke the function via the `faas-cli` using ```sh -$ curl -H "Content-Type: application/json" \ - -X POST \ - -d '{"service":"nodeinfo","network":"func", "image": "functions/nodehelloenv:latest", "envVars": {"NODE_ENV": "production"}}' \ - http://localhost:8080/system/functions -$ curl -X POST \ - -H 'Content-Type: text/plain' \ - -H 'Content-Length: 0' \ - http://localhost:8080/function/nodehelloenv +$ faas-cli deploy -f ./samples.yml +$ fass-cli invoke nodehelloenv Hello from a production machine ``` +Notice that it is using the value of `NODE_ENV` from the stack file, the default is is `dev`. + ## Using Swarm Secrets -A very tempting thing to do is to now add database password or api secrets as environment variables. However, this is not secure. Instead, we can leverage the [Docker Swarm Secrets](https://docs.docker.com/engine/swarm/secrets/) feature to safely store and give our functions access to the needed values. Using secrets is a two step process. Take the [ApiKeyProtected](../sample-functions/ApiKeyProtected) example function, when we deploy this function we provide a secret key that it uses to authenticate requests to it. First we must add a secret to the swarm +For sensitive value we can leverage the [Docker Swarm Secrets](https://docs.docker.com/engine/swarm/secrets/) feature to safely store and give our functions access to the needed values. Using secrets is a two step process. Take the [ApiKeyProtected](../sample-functions/ApiKeyProtected) example function, when we deploy this function we provide a secret key that it uses to authenticate requests to it. First we must add a secret to the swarm ```sh docker secret create secret_api_key ~/secrets/secret_api_key.txt @@ -52,24 +38,24 @@ docker secret create secret_api_key ~/secrets/secret_api_key.txt where `~/secrets/secret_api_key.txt` is a simple text file that might look like this ```txt -R^Y$qzKzSJw51K9zP$pQ3R3N +R^YqzKzSJw51K9zPpQ3R3N ``` Equivalently, you can pipe the value to docker via stdin like this ```sh -echo "R^Y$qzKzSJw51K9zP$pQ3R3N" | docker secret create secret_api_key - +echo "R^YqzKzSJw51K9zPpQ3R3N" | docker secret create secret_api_key - ``` Now, with the secret defined, we can deploy the function like this ```sh -$ echo "R^Y$qzKzSJw51K9zP$pQ3R3N" | docker secret create secret_api_key - +$ echo "R^YqzKzSJw51K9zPpQ3R3N" | docker secret create secret_api_key - $ faas-cli deploy -f ./samples.yml --secret secret_api_key $ curl -H "Content-Type: application/json" \ -X POST \ - -H "X-Api-Key: R^Y$qzKzSJw51K9zP$pQ3R3N" \ + -H "X-Api-Key: R^YqzKzSJw51K9zPpQ3R3N" \ -d '{}' \ http://localhost:8080/function/protectedapi @@ -99,40 +85,22 @@ functions: Note that unlike the `envVars` in the first example, we do not provide the secret value, just a list of names: `"secrets": ["secret_api_key"]`. The secret value has already been securely stored in the Docker swarm. One really great result of this type of configuration is that you can simplify your function code by always referencing the same secret name, no matter the environment, the only change is how the environments are configured. - -(_Optional_) If you are using the API directly, the above looks likes this - -```sh -$ curl -H "Content-Type: application/json" \ - -X POST \ - -d '{"service":"protectedapi","network":"func_functions", "image": "functions/api-key-protected:latest", "secrets": ["secret_api_key"]}' \ - http://localhost:8080/system/functions -$ curl -H "Content-Type: application/json" \ - -X POST \ - -H "X-Api-Key: R^Y$qzKzSJw51K9zP$pQ3R3N" \ - -d '{}' \ - http://localhost:8080/function/protectedapi - -Unlocked the function! -``` - ## Advanced Swarm Secrets For various reasons, you might add a secret to the Swarm under a different name than you want to us in your function, e.g. if you are rotating a secret key. The Docker Swarm secret specification allows us some advanced configuration of secrets [by supplying a comma-separated value specifying the secret](https://docs.docker.com/engine/reference/commandline/service_create/#create-a-service-with-secrets). The is best show in an example. Let's change the api key on our example function. First add a new secret key ```sh -echo "new$qzKzSJw51K9zP$pQ3R3N" | docker secret create secret_api_key_2 - +echo "newqzKzSJw51K9zPpQ3R3N" | docker secret create secret_api_key_2 - ``` Then, remove our old function and redeploy it with the new secret mounted in the same place as the old secret ```sh -$ echo "new$qzKzSJw51K9zP$pQ3R3N" | docker secret create secret_api_key_s - $ faas-cli deploy -f ./samples.yml --secret source=secret_api_key_2,target=secret_api_key --replace $ curl -H "Content-Type: application/json" \ -X POST \ - -H "X-Api-Key: new$qzKzSJw51K9zP$pQ3R3N" \ + -H "X-Api-Key: newqzKzSJw51K9zPpQ3R3N" \ -d '{}' \ http://localhost:8080/function/protectedapi @@ -140,26 +108,3 @@ Unlocked the function! ``` We reuse the sample stack file as in the previous section. - - -(_Optional_) Directly using the API, the above looks like this. - -```sh -$ curl -H "Content-Type: applicaiton/json" \ - -X DELETE \ - -d '{"functionName": "protectedapi"}' \ - http://localhost:8080/system/functions - -$ curl -H "Content-Type: application/json" \ - -X POST \ - -d '{"service":"protectedapi","network":"func_functions", "image": "functions/api-key-protected:latest", "secrets": ["source=secret_api_key_2,target=secret_api_key"]}' \ - http://localhost:8080/system/functions - -$ curl -H "Content-Type: application/json" \ - -X POST \ - -H "X-Api-Key: new$qzKzSJw51K9zP$pQ3R3N" \ - -d '{}' \ - http://localhost:8080/function/protectedapi - -Unlocked the function! -``` \ No newline at end of file diff --git a/sample-functions/ApiKeyProtected/README.md b/sample-functions/ApiKeyProtected/README.md index 0d5e029a..57cb2847 100644 --- a/sample-functions/ApiKeyProtected/README.md +++ b/sample-functions/ApiKeyProtected/README.md @@ -1,6 +1,6 @@ ### Api-Key-Protected sample -To use this sample provide a secret for the container/service in `secret_api_key` using [Docker Swarm Secret](https://docs.docker.com/engine/swarm/secrets/#defining-and-using-secrets-in-compose-files). +See the [secure secret management guide](../guide/secure_secret_management.md) for instructions on how to use this function. -Then when calling via the gateway pass the additional header "X-Api-Key", if it matches the `secret_api_key` value then the function will give access, otherwise access denied. +When calling via the gateway pass the additional header "X-Api-Key", if it matches the `secret_api_key` value then the function will give access, otherwise access denied.