diff --git a/.gitignore b/.gitignore index 6716816..d79e487 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ hosts /resolv.conf .idea/ + +basic-auth-user +basic-auth-password diff --git a/Gopkg.lock b/Gopkg.lock index 5750a19..e10f1df 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -273,6 +273,14 @@ revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" version = "v0.8.1" +[[projects]] + digest = "1:044c51736e2688a3e4f28f72537f8a7b3f9c188fab4477d5334d92dfe2c07ed5" + name = "github.com/sethvargo/go-password" + packages = ["password"] + pruneopts = "UT" + revision = "07c3d521e892540e71469bb0312866130714c038" + version = "v0.1.3" + [[projects]] digest = "1:fd61cf4ae1953d55df708acb6b91492d538f49c305b364a014049914495db426" name = "github.com/sirupsen/logrus" @@ -466,6 +474,7 @@ "github.com/containerd/containerd/oci", "github.com/morikuni/aec", "github.com/opencontainers/runtime-spec/specs-go", + "github.com/sethvargo/go-password/password", "github.com/spf13/cobra", "github.com/vishvananda/netlink", "github.com/vishvananda/netns", diff --git a/Gopkg.toml b/Gopkg.toml index 8e2cd10..fe5130e 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -21,3 +21,7 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/sethvargo/go-password" + version = "0.1.3" diff --git a/README.md b/README.md index 85e1010..4ac2702 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ Other operations are pending development in the provider. Pending: -* [ ] Configure `basic_auth` to protect the OpenFaaS gateway and faas-containerd HTTP API * [ ] Use CNI to create network namespaces and adapters * [ ] Monitor and restart any of the core components at runtime if the container stops * [ ] Bundle/package/automate installation of containerd - [see bootstrap from k3s](https://github.com/rancher/k3s) @@ -55,6 +54,7 @@ Done: * [x] Restart containers upon restart of faasd * [x] Clear / remove containers and tasks with SIGTERM / SIGINT * [x] Determine armhf/arm64 containers to run for gateway +* [x] Configure `basic_auth` to protect the OpenFaaS gateway and faas-containerd HTTP API ## Hacking (build from source) @@ -82,6 +82,8 @@ go build # sudo ./faasd up ``` +You will then need to get the basic-auth password, it is written to `$GOPATH/src/github.com/alexellis/faasd/basic-auth-password` if you followed the above instructions. +The default Basic Auth username is `admin`, which is written to `$GOPATH/src/github.com/alexellis/faasd/basic-auth-user`, if you wish to use a non-standard user then create this file and add your username (no newlines or other characters) ### Build and run (binaries) ```sh diff --git a/cmd/up.go b/cmd/up.go index c1c37d5..a5d7c2b 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -2,6 +2,8 @@ package cmd import ( "fmt" + "github.com/pkg/errors" + "io/ioutil" "log" "os" "os/signal" @@ -12,6 +14,7 @@ import ( "github.com/alexellis/faasd/pkg" "github.com/alexellis/k3sup/pkg/env" + "github.com/sethvargo/go-password/password" "github.com/spf13/cobra" ) @@ -42,6 +45,11 @@ func runUp(_ *cobra.Command, _ []string) error { clientSuffix = "-arm64" } + authFileErr := errors.Wrap(makeBasicAuthFiles(), "Could not create gateway auth files") + if authFileErr != nil { + return authFileErr + } + services := makeServiceDefinitions(clientSuffix) start := time.Now() @@ -96,10 +104,70 @@ func runUp(_ *cobra.Command, _ []string) error { return nil } +func makeBasicAuthFiles() error { + wd, _ := os.Getwd() + pwdFile := wd + "/basic-auth-password" + authPassword, err := password.Generate(63, 10, 0, false, true) + + if err != nil { + return err + } + + err = makeFile(pwdFile, authPassword) + if err != nil { + return err + } + + userFile := wd + "/basic-auth-user" + err = makeFile(userFile, "admin") + if err != nil { + return err + } + + return nil +} + +func makeFile(filePath, fileContents string) error { + _, err := os.Stat(filePath) + if err == nil { + log.Printf("File exists: %q, Using data from this file",filePath) + return nil + } else if os.IsNotExist(err) { + log.Printf("Writing file: %q", filePath) + return ioutil.WriteFile(filePath, []byte(fileContents), 0644) + } else { + return err + } +} + func makeServiceDefinitions(archSuffix string) []pkg.Service { wd, _ := os.Getwd() + secretMountDir := "/run/secrets/" + return []pkg.Service{ + pkg.Service{ + Name: "basic-auth-plugin", + Image: "docker.io/openfaas/basic-auth-plugin:0.18.10" + archSuffix, + Env: []string{ + "port=8080", + "secret_mount_path=" + secretMountDir, + "user_filename=basic-auth-user", + "pass_filename=basic-auth-password", + }, + Mounts: []pkg.Mount{ + pkg.Mount{ + Src:path.Join(wd, "/basic-auth-password"), + Dest:secretMountDir + "basic-auth-password", + }, + pkg.Mount{ + Src: path.Join(wd, "/basic-auth-user"), + Dest: secretMountDir + "basic-auth-user", + }, + }, + Caps: []string{"CAP_NET_RAW"}, + Args: nil, + }, pkg.Service{ Name: "nats", Env: []string{""}, @@ -122,7 +190,7 @@ func makeServiceDefinitions(archSuffix string) []pkg.Service { pkg.Service{ Name: "gateway", Env: []string{ - "basic_auth=false", + "basic_auth=true", "functions_provider_url=http://faas-containerd:8081/", "direct_functions=false", "read_timeout=60s", @@ -130,9 +198,21 @@ func makeServiceDefinitions(archSuffix string) []pkg.Service { "upstream_timeout=65s", "faas_nats_address=nats", "faas_nats_port=4222", + "auth_proxy_url=http://basic-auth-plugin:8080/validate", + "auth_proxy_pass_body=false", + "secret_mount_path=" + secretMountDir, }, Image: "docker.io/openfaas/gateway:0.18.8" + archSuffix, - Mounts: []pkg.Mount{}, + Mounts: []pkg.Mount{ + pkg.Mount{ + Src:path.Join(wd, "/basic-auth-password"), + Dest:secretMountDir + "basic-auth-password", + }, + pkg.Mount{ + Src: path.Join(wd, "/basic-auth-user"), + Dest: secretMountDir + "basic-auth-user", + }, + }, Caps: []string{"CAP_NET_RAW"}, }, pkg.Service{ @@ -145,9 +225,20 @@ func makeServiceDefinitions(archSuffix string) []pkg.Service { "ack_wait=5m5s", "max_inflight=1", "write_debug=false", + "basic_auth=true", + "secret_mount_path=" + secretMountDir, }, Image: "docker.io/openfaas/queue-worker:0.9.0", - Mounts: []pkg.Mount{}, + Mounts: []pkg.Mount{ + pkg.Mount{ + Src:path.Join(wd, "/basic-auth-password"), + Dest:secretMountDir + "basic-auth-password", + }, + pkg.Mount{ + Src: path.Join(wd, "/basic-auth-user"), + Dest: secretMountDir + "basic-auth-user", + }, + }, Caps: []string{"CAP_NET_RAW"}, }, } diff --git a/vendor/github.com/sethvargo/go-password/LICENSE b/vendor/github.com/sethvargo/go-password/LICENSE new file mode 100644 index 0000000..c62ec3d --- /dev/null +++ b/vendor/github.com/sethvargo/go-password/LICENSE @@ -0,0 +1,20 @@ +Copyright 2017 Seth Vargo + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/sethvargo/go-password/password/generate.go b/vendor/github.com/sethvargo/go-password/password/generate.go new file mode 100644 index 0000000..4d2fe8b --- /dev/null +++ b/vendor/github.com/sethvargo/go-password/password/generate.go @@ -0,0 +1,253 @@ +// Package password provides a library for generating high-entropy random +// password strings via the crypto/rand package. +// +// res, err := Generate(64, 10, 10, false, false) +// if err != nil { +// log.Fatal(err) +// } +// log.Printf(res) +// +// Most functions are safe for concurrent use. +package password + +import ( + "crypto/rand" + "errors" + "math/big" + "strings" +) + +// Built-time checks that the generators implement the interface. +var _ PasswordGenerator = (*Generator)(nil) + +// PasswordGenerator is an interface that implements the Generate function. This +// is useful for testing where you can pass this interface instead of a real +// password generator to mock responses for predicability. +type PasswordGenerator interface { + Generate(int, int, int, bool, bool) (string, error) + MustGenerate(int, int, int, bool, bool) string +} + +const ( + // LowerLetters is the list of lowercase letters. + LowerLetters = "abcdefghijklmnopqrstuvwxyz" + + // UpperLetters is the list of uppercase letters. + UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + // Digits is the list of permitted digits. + Digits = "0123456789" + + // Symbols is the list of symbols. + Symbols = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./" +) + +var ( + // ErrExceedsTotalLength is the error returned with the number of digits and + // symbols is greater than the total length. + ErrExceedsTotalLength = errors.New("number of digits and symbols must be less than total length") + + // ErrLettersExceedsAvailable is the error returned with the number of letters + // exceeds the number of available letters and repeats are not allowed. + ErrLettersExceedsAvailable = errors.New("number of letters exceeds available letters and repeats are not allowed") + + // ErrDigitsExceedsAvailable is the error returned with the number of digits + // exceeds the number of available digits and repeats are not allowed. + ErrDigitsExceedsAvailable = errors.New("number of digits exceeds available digits and repeats are not allowed") + + // ErrSymbolsExceedsAvailable is the error returned with the number of symbols + // exceeds the number of available symbols and repeats are not allowed. + ErrSymbolsExceedsAvailable = errors.New("number of symbols exceeds available symbols and repeats are not allowed") +) + +// Generator is the stateful generator which can be used to customize the list +// of letters, digits, and/or symbols. +type Generator struct { + lowerLetters string + upperLetters string + digits string + symbols string +} + +// GeneratorInput is used as input to the NewGenerator function. +type GeneratorInput struct { + LowerLetters string + UpperLetters string + Digits string + Symbols string +} + +// NewGenerator creates a new Generator from the specified configuration. If no +// input is given, all the default values are used. This function is safe for +// concurrent use. +func NewGenerator(i *GeneratorInput) (*Generator, error) { + if i == nil { + i = new(GeneratorInput) + } + + g := &Generator{ + lowerLetters: i.LowerLetters, + upperLetters: i.UpperLetters, + digits: i.Digits, + symbols: i.Symbols, + } + + if g.lowerLetters == "" { + g.lowerLetters = LowerLetters + } + + if g.upperLetters == "" { + g.upperLetters = UpperLetters + } + + if g.digits == "" { + g.digits = Digits + } + + if g.symbols == "" { + g.symbols = Symbols + } + + return g, nil +} + +// Generate generates a password with the given requirements. length is the +// total number of characters in the password. numDigits is the number of digits +// to include in the result. numSymbols is the number of symbols to include in +// the result. noUpper excludes uppercase letters from the results. allowRepeat +// allows characters to repeat. +// +// The algorithm is fast, but it's not designed to be performant; it favors +// entropy over speed. This function is safe for concurrent use. +func (g *Generator) Generate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) (string, error) { + letters := g.lowerLetters + if !noUpper { + letters += g.upperLetters + } + + chars := length - numDigits - numSymbols + if chars < 0 { + return "", ErrExceedsTotalLength + } + + if !allowRepeat && chars > len(letters) { + return "", ErrLettersExceedsAvailable + } + + if !allowRepeat && numDigits > len(g.digits) { + return "", ErrDigitsExceedsAvailable + } + + if !allowRepeat && numSymbols > len(g.symbols) { + return "", ErrSymbolsExceedsAvailable + } + + var result string + + // Characters + for i := 0; i < chars; i++ { + ch, err := randomElement(letters) + if err != nil { + return "", err + } + + if !allowRepeat && strings.Contains(result, ch) { + i-- + continue + } + + result, err = randomInsert(result, ch) + if err != nil { + return "", err + } + } + + // Digits + for i := 0; i < numDigits; i++ { + d, err := randomElement(g.digits) + if err != nil { + return "", err + } + + if !allowRepeat && strings.Contains(result, d) { + i-- + continue + } + + result, err = randomInsert(result, d) + if err != nil { + return "", err + } + } + + // Symbols + for i := 0; i < numSymbols; i++ { + sym, err := randomElement(g.symbols) + if err != nil { + return "", err + } + + if !allowRepeat && strings.Contains(result, sym) { + i-- + continue + } + + result, err = randomInsert(result, sym) + if err != nil { + return "", err + } + } + + return result, nil +} + +// MustGenerate is the same as Generate, but panics on error. +func (g *Generator) MustGenerate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) string { + res, err := g.Generate(length, numDigits, numSymbols, noUpper, allowRepeat) + if err != nil { + panic(err) + } + return res +} + +// Generate is the package shortcut for Generator.Generate. +func Generate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) (string, error) { + gen, err := NewGenerator(nil) + if err != nil { + return "", err + } + + return gen.Generate(length, numDigits, numSymbols, noUpper, allowRepeat) +} + +// MustGenerate is the package shortcut for Generator.MustGenerate. +func MustGenerate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) string { + res, err := Generate(length, numDigits, numSymbols, noUpper, allowRepeat) + if err != nil { + panic(err) + } + return res +} + +// randomInsert randomly inserts the given value into the given string. +func randomInsert(s, val string) (string, error) { + if s == "" { + return val, nil + } + + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(s)+1))) + if err != nil { + return "", err + } + i := n.Int64() + return s[0:i] + val + s[i:], nil +} + +// randomElement extracts a random element from the given string. +func randomElement(s string) (string, error) { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(s)))) + if err != nil { + return "", err + } + return string(s[n.Int64()]), nil +} diff --git a/vendor/github.com/sethvargo/go-password/password/mock.go b/vendor/github.com/sethvargo/go-password/password/mock.go new file mode 100644 index 0000000..07d4734 --- /dev/null +++ b/vendor/github.com/sethvargo/go-password/password/mock.go @@ -0,0 +1,39 @@ +package password + +// Built-time checks that the generators implement the interface. +var _ PasswordGenerator = (*mockGenerator)(nil) + +type mockGenerator struct { + result string + err error +} + +// NewMockGenerator creates a new generator that satisfies the PasswordGenerator +// interface. If an error is provided, the error is returned. If a result if +// provided, the result is always returned, regardless of what parameters are +// passed into the Generate or MustGenerate methods. +// +// This function is most useful for tests where you want to have predicable +// results for a transitive resource that depends on go-password. +func NewMockGenerator(result string, err error) *mockGenerator { + return &mockGenerator{ + result: result, + err: err, + } +} + +// Generate returns the mocked result or error. +func (g *mockGenerator) Generate(int, int, int, bool, bool) (string, error) { + if g.err != nil { + return "", g.err + } + return g.result, nil +} + +// MustGenerate returns the mocked result or panics if an error was given. +func (g *mockGenerator) MustGenerate(int, int, int, bool, bool) string { + if g.err != nil { + panic(g.err) + } + return g.result +}