From 5c48ac1a706704fa3913969c4dd98b4653553124 Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Mon, 27 Jan 2020 19:03:47 +0000 Subject: [PATCH] Add secrets support Adds secrets support and binding of secrets at runtime to functions. Files are written in plain-text to a 0644 permission folder which can only be read by root and the containers requesting the secret through the OpenFaaS API. Tested by deploying an alpine function using "cat" as its fprocess. Happy to revisit at a later date and look into encryption at rest. This should be on-par with using Kubernetes in its default unencrypted state. Fixes: #29 Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- .gitignore | 1 + cmd/provider.go | 7 +- pkg/provider/handlers/deploy.go | 30 +++++++-- pkg/provider/handlers/secret.go | 111 ++++++++++++++++++++++++++++++++ pkg/provider/handlers/update.go | 9 ++- 5 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 pkg/provider/handlers/secret.go diff --git a/.gitignore b/.gitignore index 6975047..982043a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ hosts basic-auth-user basic-auth-password /bin +/secrets diff --git a/cmd/provider.go b/cmd/provider.go index 89b0b39..4acf673 100644 --- a/cmd/provider.go +++ b/cmd/provider.go @@ -66,17 +66,20 @@ func runProvider(_ *cobra.Command, _ []string) error { invokeResolver := handlers.NewInvokeResolver(client) + userSecretPath := path.Join(wd, "secrets") + bootstrapHandlers := types.FaaSHandlers{ FunctionProxy: proxy.NewHandlerFunc(*config, invokeResolver), DeleteHandler: handlers.MakeDeleteHandler(client, cni), - DeployHandler: handlers.MakeDeployHandler(client, cni), + DeployHandler: handlers.MakeDeployHandler(client, cni, userSecretPath), FunctionReader: handlers.MakeReadHandler(client), ReplicaReader: handlers.MakeReplicaReaderHandler(client), ReplicaUpdater: handlers.MakeReplicaUpdateHandler(client, cni), - UpdateHandler: handlers.MakeUpdateHandler(client, cni), + UpdateHandler: handlers.MakeUpdateHandler(client, cni, userSecretPath), HealthHandler: func(w http.ResponseWriter, r *http.Request) {}, InfoHandler: handlers.MakeInfoHandler(Version, GitCommit), ListNamespaceHandler: listNamespaces(), + SecretHandler: handlers.MakeSecretHandler(client, userSecretPath), } log.Printf("Listening on TCP port: %d\n", *config.TCPPort) diff --git a/pkg/provider/handlers/deploy.go b/pkg/provider/handlers/deploy.go index 36e0828..4e0a8f3 100644 --- a/pkg/provider/handlers/deploy.go +++ b/pkg/provider/handlers/deploy.go @@ -22,7 +22,7 @@ import ( "github.com/pkg/errors" ) -func MakeDeployHandler(client *containerd.Client, cni gocni.CNI) func(w http.ResponseWriter, r *http.Request) { +func MakeDeployHandler(client *containerd.Client, cni gocni.CNI, secretMountPath string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -45,11 +45,15 @@ func MakeDeployHandler(client *containerd.Client, cni gocni.CNI) func(w http.Res return } - name := req.Service + err = validateSecrets(secretMountPath, req.Secrets) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + name := req.Service ctx := namespaces.WithNamespace(context.Background(), FunctionNamespace) - deployErr := deploy(ctx, req, client, cni) + deployErr := deploy(ctx, req, client, cni, secretMountPath) if deployErr != nil { log.Printf("[Deploy] error deploying %s, error: %s\n", name, deployErr) http.Error(w, deployErr.Error(), http.StatusBadRequest) @@ -58,7 +62,7 @@ func MakeDeployHandler(client *containerd.Client, cni gocni.CNI) func(w http.Res } } -func deploy(ctx context.Context, req types.FunctionDeployment, client *containerd.Client, cni gocni.CNI) error { +func deploy(ctx context.Context, req types.FunctionDeployment, client *containerd.Client, cni gocni.CNI, secretMountPath string) error { imgRef := "docker.io/" + req.Image if strings.Index(req.Image, ":") == -1 { @@ -81,6 +85,15 @@ func deploy(ctx context.Context, req types.FunctionDeployment, client *container envs := prepareEnv(req.EnvProcess, req.EnvVars) mounts := getMounts() + for _, secret := range req.Secrets { + mounts = append(mounts, specs.Mount{ + Destination: path.Join("/var/openfaas/secrets", secret), + Type: "bind", + Source: path.Join(secretMountPath, secret), + Options: []string{"rbind", "ro"}, + }) + } + name := req.Service container, err := client.NewContainer( @@ -177,3 +190,12 @@ func getMounts() []specs.Mount { }) return mounts } + +func validateSecrets(secretMountPath string, secrets []string) error { + for _, secret := range secrets { + if _, err := os.Stat(path.Join(secretMountPath, secret)); err != nil { + return fmt.Errorf("unable to find secret: %s", secret) + } + } + return nil +} diff --git a/pkg/provider/handlers/secret.go b/pkg/provider/handlers/secret.go new file mode 100644 index 0000000..e1e42ee --- /dev/null +++ b/pkg/provider/handlers/secret.go @@ -0,0 +1,111 @@ +package handlers + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "os" + "path" + + "github.com/containerd/containerd" + "github.com/openfaas/faas-provider/types" +) + +const secretFilePermission = 0644 + +func MakeSecretHandler(c *containerd.Client, mountPath string) func(w http.ResponseWriter, r *http.Request) { + + err := os.MkdirAll(mountPath, secretFilePermission) + if err != nil { + log.Printf("Creating path: %s, error: %s\n", mountPath, err) + } + + return func(w http.ResponseWriter, r *http.Request) { + if r.Body != nil { + defer r.Body.Close() + } + + switch r.Method { + case http.MethodGet: + listSecrets(c, w, r, mountPath) + case http.MethodPost: + createSecret(c, w, r, mountPath) + case http.MethodPut: + createSecret(c, w, r, mountPath) + case http.MethodDelete: + deleteSecret(c, w, r, mountPath) + default: + w.WriteHeader(http.StatusBadRequest) + return + } + + } +} + +func listSecrets(c *containerd.Client, w http.ResponseWriter, r *http.Request, mountPath string) { + files, err := ioutil.ReadDir(mountPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + secrets := []types.Secret{} + for _, f := range files { + secrets = append(secrets, types.Secret{Name: f.Name()}) + } + + bytesOut, _ := json.Marshal(secrets) + w.Write(bytesOut) +} + +func createSecret(c *containerd.Client, w http.ResponseWriter, r *http.Request, mountPath string) { + secret, err := parseSecret(r) + if err != nil { + log.Printf("[secret] error %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = ioutil.WriteFile(path.Join(mountPath, secret.Name), []byte(secret.Value), secretFilePermission) + + if err != nil { + log.Printf("[secret] error %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func parseSecret(r *http.Request) (types.Secret, error) { + secret := types.Secret{} + bytesOut, err := ioutil.ReadAll(r.Body) + if err != nil { + return secret, err + } + + err = json.Unmarshal(bytesOut, &secret) + return secret, err +} + +func deleteSecret(c *containerd.Client, w http.ResponseWriter, r *http.Request, mountPath string) { + secret, err := parseSecret(r) + if err != nil { + log.Printf("[secret] error %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err != nil { + log.Printf("[secret] error %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = os.Remove(path.Join(mountPath, secret.Name)) + + if err != nil { + log.Printf("[secret] error %s", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/pkg/provider/handlers/update.go b/pkg/provider/handlers/update.go index bf2de43..5ef9f98 100644 --- a/pkg/provider/handlers/update.go +++ b/pkg/provider/handlers/update.go @@ -15,7 +15,7 @@ import ( "github.com/openfaas/faas-provider/types" ) -func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w http.ResponseWriter, r *http.Request) { +func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI, secretMountPath string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -47,6 +47,11 @@ func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w http.Res return } + err = validateSecrets(secretMountPath, req.Secrets) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + ctx := namespaces.WithNamespace(context.Background(), FunctionNamespace) if function.replicas != 0 { err = DeleteCNINetwork(ctx, cni, client, name) @@ -62,7 +67,7 @@ func MakeUpdateHandler(client *containerd.Client, cni gocni.CNI) func(w http.Res return } - deployErr := deploy(ctx, req, client, cni) + deployErr := deploy(ctx, req, client, cni, secretMountPath) if deployErr != nil { log.Printf("[Update] error deploying %s, error: %s\n", name, deployErr) http.Error(w, deployErr.Error(), http.StatusBadRequest)