mirror of
https://github.com/openfaas/faas.git
synced 2025-06-09 16:56:47 +00:00
Add support to specify secrets in services
**What** - During function creation, accept an array of strings defining swarm secrets that are required for the service - Update docs - Add new guide on using the secrets capability - Add new sample function to highlight using environment variables - Update `ApiKeyProtected` sample function to utilize the new secrets capabilities **Why** - This allows secrets to remain encrypted at rest instead of being unencrypted in environment variables and yaml files. Fixes #285 Signed-off-by: Lucas Roesler <lucas.roesler@gmail.com>
This commit is contained in:
parent
cc103ada94
commit
0fef825fb4
@ -225,6 +225,10 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
description: Overrides to environmental variables
|
description: Overrides to environmental variables
|
||||||
|
secrets:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
registryAuth:
|
registryAuth:
|
||||||
type: string
|
type: string
|
||||||
description: >-
|
description: >-
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/registry"
|
"github.com/docker/docker/registry"
|
||||||
@ -51,7 +52,13 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie
|
|||||||
options.EncodedRegistryAuth = auth
|
options.EncodedRegistryAuth = auth
|
||||||
}
|
}
|
||||||
|
|
||||||
spec := makeSpec(&request, maxRestarts, restartDelay)
|
spec, err := makeSpec(c, &request, maxRestarts, restartDelay)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("Deployment error: " + err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
response, err := c.ServiceCreate(context.Background(), spec, options)
|
response, err := c.ServiceCreate(context.Background(), spec, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -64,7 +71,8 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64, restartDelay time.Duration) swarm.ServiceSpec {
|
func makeSpec(c *client.Client, request *requests.CreateFunctionRequest, maxRestarts uint64, restartDelay time.Duration) (swarm.ServiceSpec, error) {
|
||||||
|
linuxOnlyConstraints := []string{"node.platform.os == linux"}
|
||||||
constraints := []string{}
|
constraints := []string{}
|
||||||
|
|
||||||
if request.Constraints != nil && len(request.Constraints) > 0 {
|
if request.Constraints != nil && len(request.Constraints) > 0 {
|
||||||
@ -92,6 +100,11 @@ func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64, resta
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secrets, err := makeSecretsArray(c, request.Secrets)
|
||||||
|
if err != nil {
|
||||||
|
return swarm.ServiceSpec{}, err
|
||||||
|
}
|
||||||
|
|
||||||
spec := swarm.ServiceSpec{
|
spec := swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{
|
Annotations: swarm.Annotations{
|
||||||
Name: request.Service,
|
Name: request.Service,
|
||||||
@ -104,8 +117,9 @@ func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64, resta
|
|||||||
Delay: &restartDelay,
|
Delay: &restartDelay,
|
||||||
},
|
},
|
||||||
ContainerSpec: swarm.ContainerSpec{
|
ContainerSpec: swarm.ContainerSpec{
|
||||||
Image: request.Image,
|
Image: request.Image,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
|
Secrets: secrets,
|
||||||
},
|
},
|
||||||
Networks: nets,
|
Networks: nets,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
@ -127,7 +141,7 @@ func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64, resta
|
|||||||
spec.TaskTemplate.ContainerSpec.Env = env
|
spec.TaskTemplate.ContainerSpec.Env = env
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec
|
return spec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildEnv(envProcess string, envVars map[string]string) []string {
|
func buildEnv(envProcess string, envVars map[string]string) []string {
|
||||||
@ -233,3 +247,63 @@ func getMinReplicas(request *requests.CreateFunctionRequest) *uint64 {
|
|||||||
}
|
}
|
||||||
return &replicas
|
return &replicas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSecretsArray(c *client.Client, secretNames []string) ([]*swarm.SecretReference, error) {
|
||||||
|
values := []*swarm.SecretReference{}
|
||||||
|
|
||||||
|
if len(secretNames) == 0 {
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedSecrets := make(map[string]bool)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// query the Swarm for the requested secret ids, these are required to complete
|
||||||
|
// the spec
|
||||||
|
args := filters.NewArgs()
|
||||||
|
for _, name := range secretNames {
|
||||||
|
args.Add("name", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := c.SecretList(ctx, types.SecretListOptions{
|
||||||
|
Filters: args,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create map of matching secrets for easy lookup
|
||||||
|
foundSecrets := make(map[string]string)
|
||||||
|
for _, secret := range secrets {
|
||||||
|
foundSecrets[secret.Spec.Annotations.Name] = secret.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// mimics the simple syntax for `docker service create --secret foo`
|
||||||
|
// and the code is based on the docker cli
|
||||||
|
for _, secretName := range secretNames {
|
||||||
|
if _, exists := requestedSecrets[secretName]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate secret target for %s not allowed", secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := foundSecrets[secretName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("secret not found: %s", secretName)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &swarm.SecretReference{
|
||||||
|
File: &swarm.SecretReferenceFileTarget{
|
||||||
|
UID: "0",
|
||||||
|
GID: "0",
|
||||||
|
Mode: 0444,
|
||||||
|
Name: secretName,
|
||||||
|
},
|
||||||
|
SecretID: id,
|
||||||
|
SecretName: secretName,
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedSecrets[secretName] = true
|
||||||
|
values = append(values, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
@ -21,6 +21,9 @@ type CreateFunctionRequest struct {
|
|||||||
// EnvVars provides overrides for functions.
|
// EnvVars provides overrides for functions.
|
||||||
EnvVars map[string]string `json:"envVars"`
|
EnvVars map[string]string `json:"envVars"`
|
||||||
|
|
||||||
|
// Secrets is a list of secrets required for the orchestration layer to provide
|
||||||
|
Secrets []string `json:"secrets"`
|
||||||
|
|
||||||
// RegistryAuth is the registry authentication (optional)
|
// RegistryAuth is the registry authentication (optional)
|
||||||
// in the same encoded format as Docker native credentials
|
// in the same encoded format as Docker native credentials
|
||||||
// (see ~/.docker/config.json)
|
// (see ~/.docker/config.json)
|
||||||
|
75
guide/secure_secret_management.md
Normal file
75
guide/secure_secret_management.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Guide on using Docker Swarm Secrets with OpenFaaS
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
First an 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. If you are interacting with the OpenFaaS gateway via the api, seeting the `NODE_ENV` might look like this
|
||||||
|
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
This particular function returns a simple sentence that contains the `NODE_ENV` in it.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl -X POST \
|
||||||
|
-H 'Content-Type: text/plain' \
|
||||||
|
-H 'Content-Length: 0' \
|
||||||
|
http://localhost:8080/function/nodehelloenv
|
||||||
|
Hello from a production machine
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalently, you can pipe the value to docker via stdin like this
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "R^Y$qzKzSJw51K9zP$pQ3R3N" | docker secret create secret_api_key -
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, with the secret defined, we can deploy the function like 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can test the function with these commands
|
||||||
|
```sh
|
||||||
|
$ 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!
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl -H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-H "X-Api-Key: wrong_key" \
|
||||||
|
-d '{}' \
|
||||||
|
http://localhost:8080/function/protectedapi
|
||||||
|
|
||||||
|
Access denied!
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
@ -1,6 +1,6 @@
|
|||||||
### Api-Key-Protected sample
|
### Api-Key-Protected sample
|
||||||
|
|
||||||
To use this sample provide an env variable for the container/service in `secret_api_key`.
|
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).
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
BIN
sample-functions/ApiKeyProtected/app
Executable file
BIN
sample-functions/ApiKeyProtected/app
Executable file
Binary file not shown.
@ -4,15 +4,21 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"github.com/openfaas/faas/watchdog/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handle(header http.Header, body []byte) {
|
func handle(body []byte) {
|
||||||
key := header.Get("X-Api-Key")
|
key := os.Getenv("Http_X_Api_Key")
|
||||||
if key == os.Getenv("secret_api_key") {
|
|
||||||
|
secretBytes, err := ioutil.ReadFile("/run/secrets/secret_api_key")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := strings.TrimSpace(string(secretBytes))
|
||||||
|
|
||||||
|
if key == secret {
|
||||||
fmt.Println("Unlocked the function!")
|
fmt.Println("Unlocked the function!")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Access denied!")
|
fmt.Println("Access denied!")
|
||||||
@ -21,9 +27,5 @@ func handle(header http.Header, body []byte) {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
bytes, _ := ioutil.ReadAll(os.Stdin)
|
bytes, _ := ioutil.ReadAll(os.Stdin)
|
||||||
req, err := types.UnmarshalRequest(bytes)
|
handle(bytes)
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
handle(req.Header, req.Body.Raw)
|
|
||||||
}
|
}
|
||||||
|
5
sample-functions/NodeHelloEnv/Dockerfile
Normal file
5
sample-functions/NodeHelloEnv/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
FROM functions/nodebase:alpine-6.9.1
|
||||||
|
|
||||||
|
ENV NODE_ENV=dev
|
||||||
|
|
||||||
|
COPY handler.js .
|
3
sample-functions/NodeHelloEnv/build.sh
Executable file
3
sample-functions/NodeHelloEnv/build.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker build --squash -t functions/nodehelloenv .
|
13
sample-functions/NodeHelloEnv/handler.js
Normal file
13
sample-functions/NodeHelloEnv/handler.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"use strict"
|
||||||
|
|
||||||
|
let getStdin = require('get-stdin');
|
||||||
|
|
||||||
|
let handle = (req) => {
|
||||||
|
console.log("Hello from a " + process.env.NODE_ENV + " machine")
|
||||||
|
};
|
||||||
|
|
||||||
|
getStdin().then(val => {
|
||||||
|
handle(val);
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e.stack);
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user