diff --git a/deploy_stack.sh b/deploy_stack.sh index a9ffd66c73..4bc5d51005 100755 --- a/deploy_stack.sh +++ b/deploy_stack.sh @@ -49,6 +49,20 @@ else echo "" fi +# Setup http signatures keys +rm signing.key > /dev/null 2>&1 || true && rm signing.key.pub > /dev/null 2>&1 || true +docker secret rm http-signing-private-key > /dev/null 2>&1 || true +docker secret rm http-signing-public-key > /dev/null 2>&1 || true + +ssh-keygen -t rsa -b 2048 -N "" -m PEM -f signing.key > /dev/null 2>&1 +openssl rsa -in ./signing.key -pubout -outform PEM -out signing.key.pub > /dev/null 2>&1 + +cat signing.key | docker secret create http-signing-private-key - > /dev/null 2>&1 || true +cat signing.key.pub | docker secret create http-signing-public-key - > /dev/null 2>&1 || true + +rm signing.key || true && rm signing.key.pub || true +echo "Http encryption settings enabled..\n" + arch=$(uname -m) case "$arch" in diff --git a/docker-compose.arm64.yml b/docker-compose.arm64.yml index 68446b15d6..ed4db928cc 100644 --- a/docker-compose.arm64.yml +++ b/docker-compose.arm64.yml @@ -38,6 +38,7 @@ services: secrets: - basic-auth-user - basic-auth-password + - http-signing-public-key # Docker Swarm provider faas-swarm: @@ -117,6 +118,7 @@ services: secrets: - basic-auth-user - basic-auth-password + - http-signing-private-key # End services @@ -199,3 +201,7 @@ secrets: external: true basic-auth-password: external: true + http-signing-public-key: + external: true + http-signing-private-key: + external: true diff --git a/docker-compose.armhf.yml b/docker-compose.armhf.yml index 1a72fcfa30..8727f412e0 100644 --- a/docker-compose.armhf.yml +++ b/docker-compose.armhf.yml @@ -38,6 +38,7 @@ services: secrets: - basic-auth-user - basic-auth-password + - http-signing-public-key # Docker Swarm provider faas-swarm: @@ -117,6 +118,7 @@ services: secrets: - basic-auth-user - basic-auth-password + - http-signing-private-key # End services @@ -199,3 +201,7 @@ secrets: external: true basic-auth-password: external: true + http-signing-public-key: + external: true + http-signing-private-key: + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 7834a509a5..24c34ad19a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: secrets: - basic-auth-user - basic-auth-password + - http-signing-public-key # Docker Swarm provider faas-swarm: @@ -117,6 +118,7 @@ services: secrets: - basic-auth-user - basic-auth-password + - http-signing-private-key # End services @@ -198,3 +200,7 @@ secrets: external: true basic-auth-password: external: true + http-signing-public-key: + external: true + http-signing-private-key: + external: true diff --git a/gateway/handlers/certificates_handler.go b/gateway/handlers/certificates_handler.go new file mode 100644 index 0000000000..d71d73e97e --- /dev/null +++ b/gateway/handlers/certificates_handler.go @@ -0,0 +1,89 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path" + + "github.com/gorilla/mux" +) + +var keyMap = map[string]string{ + "callback": "http-signing-public-key", +} + +type KeyType struct { + Id string `json:"id"` + PEM string `json:"pem"` +} + +type SecretsReader interface { + Read(key string) (string, error) +} + +type FileSecretsReader struct { + SecretMountPath string +} + +func (f *FileSecretsReader) Read(key string) (string, error) { + if len(f.SecretMountPath) == 0 { + return "", fmt.Errorf("invalid SecretMountPath specified for reading secrets used for certificates") + } + + certificatePath := path.Join(f.SecretMountPath, key) + if _, err := os.Stat(certificatePath); os.IsNotExist(err) { + return "", fmt.Errorf("unable to find secret %s", key) + } + + value, err := ioutil.ReadFile(certificatePath) + if err != nil { + return "", fmt.Errorf("error reading find secret %s", certificatePath) + } + + return string(value), nil +} + +func MakeCertificatesHandler(reader SecretsReader) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + keyID := vars["id"] + secretKey, ok := keyMap[keyID] + if !ok { + w.WriteHeader(http.StatusNotFound) + message := fmt.Sprintf("Unable to find certificate %s.", keyID) + w.Write([]byte(message)) + log.Println(message) + return + } + + publicKey, err := reader.Read(secretKey) + if err != nil { + w.WriteHeader(http.StatusNotFound) + message := fmt.Sprintf("Unable to find secret for key %s.", keyID) + w.Write([]byte(message)) + log.Println(message) + return + } + + key := &KeyType{ + Id: secretKey, + PEM: publicKey, + } + + bytesOut, marshalErr := json.Marshal(key) + if marshalErr != nil { + w.WriteHeader(http.StatusInternalServerError) + message := fmt.Sprintf("error marshalling json for key %s. %v", keyID, err) + w.Write([]byte(message)) + log.Println(message) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(bytesOut) + } +} diff --git a/gateway/handlers/certificates_handler_test.go b/gateway/handlers/certificates_handler_test.go new file mode 100644 index 0000000000..ff77412663 --- /dev/null +++ b/gateway/handlers/certificates_handler_test.go @@ -0,0 +1,98 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" +) + +func TestMakeCertificatesHandler(t *testing.T) { + tests := []struct { + name string + keyID string + eval func(r *http.Response) error + reader SecretsReader + }{ + { + name: "Get certificate", + keyID: "callback", + eval: func(r *http.Response) error { + if r.StatusCode != 200 { + return fmt.Errorf("expected 200") + } + + body, _ := ioutil.ReadAll(r.Body) + keyData := &KeyType{} + if err := json.Unmarshal(body, keyData); err != nil { + return fmt.Errorf("error unmarshalling result") + } + + if keyData.PEM != testPublicKey { + return fmt.Errorf("PEM want %s got %s", testPublicKey, keyData.PEM) + } + return nil + }, + reader: &TestSecretsReader{readCallBack: func(key string) (s string, e error) { + return testPublicKey, nil + }}, + }, + { + + name: "Unable to find certificate", + keyID: "missingID", + eval: func(r *http.Response) error { + if r.StatusCode != 404 { + return fmt.Errorf("expected 404") + } + + return nil + }, + reader: &TestSecretsReader{readCallBack: func(key string) (s string, e error) { + return "", fmt.Errorf("key not found") + }}, + }, + } + + r := mux.NewRouter() + ts := httptest.NewServer(r) + defer ts.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r.HandleFunc("/certificates/{id}", MakeCertificatesHandler(tt.reader)) + + url := ts.URL + "/certificates/" + tt.keyID + resp, err := http.Get(url) + if err != nil { + t.Errorf("MakeCertificatesHandler() call = %v", err) + } + + if err := tt.eval(resp); err != nil { + t.Errorf("MakeCertificatesHandler() eval = %v", err) + } + }) + } +} + +type TestSecretsReader struct { + readCallBack func(key string) (string, error) +} + +func (r *TestSecretsReader) Read(key string) (string, error) { + return r.readCallBack(key) +} + +const testPublicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7pEUKQ28pI5N3g/zG6OJ +100N/DV2Q8Ob+gzRjd7HjXgVgZyjS3nA8FAYrxTLSihcIhXuQrYxyk2vp6YMNmSB +fOptkdmj4UgLYskfeqEt8JjS6ExBxSWEDgr1IXOPPDP61on8F65/ZYGnp2JF2wHY +k0OeD4ppNUV+mIHj/wXf7VLHGflwFQH/+mfUn+tVQRgX7hTadcYmGJ+1XP0py4kU +gJDHfw8eBsFurHWr2mXu3BdraSKKf1G9i+SifmOUUul6mBONmlvzQdKtDCr48o1H +QndRHcMWjKhlBhKz4qrmqku8oGBh6iHhGVVYf8D3mU1nzyjH4rOUXZwzj+SaqgGk +vQIDAQAB +-----END PUBLIC KEY-----` diff --git a/gateway/server.go b/gateway/server.go index d0d87acd75..18f5088ecc 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -209,7 +209,7 @@ func main() { metricsHandler := metrics.PrometheusHandler() r.Handle("/metrics", metricsHandler) r.HandleFunc("/healthz", handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver, nilURLTransformer)).Methods(http.MethodGet) - + r.Handle("/certificates/{id}", handlers.MakeCertificatesHandler(&handlers.FileSecretsReader{SecretMountPath: config.SecretMountPath})) r.Handle("/", http.RedirectHandler("/ui/", http.StatusMovedPermanently)).Methods(http.MethodGet) tcpPort := 8080