From 0bce58e6e48b0cec79b1d5e67b1a7e7df497a048 Mon Sep 17 00:00:00 2001 From: Dee Kryvenko Date: Sat, 14 May 2022 21:55:31 -0700 Subject: [PATCH] Implemented TLS --- CHANGELOG.md | 4 ++++ README.md | 13 ++++++++++++- cmd/git_backend.go | 27 +++++++++++++++++++++------ server/server.go | 22 ++++++++++++++-------- test/tf/localhost.crt | 18 ++++++++++++++++++ test/tf/localhost.key | 28 ++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 test/tf/localhost.crt create mode 100644 test/tf/localhost.key diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c2a013..1e0a5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.0.19] - 2022-05-14 +### Added + +- Implemented TLS mode + ### Changed - Introduced `--dir` option under `git` backend - now current working directory can be changed dynamically diff --git a/README.md b/README.md index fceddf3..f84357b 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,21 @@ Git as Terraform backend? Seriously? I know, might sound like a stupid idea at f - [From Sources](#from-sources) - [Usage](#usage) - [As wrapper](#as-wrapper) - - [with Hashicorp Configuration Language (HCL)](#with-hashicorp-configuration-language-(hcl)) + - [with Hashicorp Configuration Language (HCL)](#with-hashicorp-configuration-language-hcl) - [as Terraform HTTP backend](#as-terraform-http-backend) - [As Github Action](#as-github-action) + - [Setup action](#setup-action) + - [Inputs](#inputs) + - [`version`](#version) + - [Outputs](#outputs) + - [`version`](#version-1) + - [Example usage](#example-usage) - [Wrappers CLI](#wrappers-cli) - [Configuration](#configuration) - [Git Credentials](#git-credentials) - [State Encryption](#state-encryption) - [Running backend remotely](#running-backend-remotely) + - [TLS](#tls) - [Basic HTTP Authentication](#basic-http-authentication) - [Why not native Terraform Backend](#why-not-native-terraform-backend) - [Why storing state in Git](#why-storing-state-in-git) @@ -234,6 +241,10 @@ Make sure you do not open the port in your firewall for remote connections. By d You may get creative and use something like K8s Network Policies like `calico`, or wrap backend traffic into API Gateway or ServiceMesh like Istio to add external layer of encryption and authentication, and then at your discretion you may run it with `--address=:6061` argument so the backend will bind to `0.0.0.0` and become remotely accessible. +### TLS + +You can set `TF_BACKEND_GIT_HTTPS_CERT` and `TF_BACKEND_GIT_HTTPS_KEY` pointing to your cert and a key files. This will make HTTP backend to start in TLS mode. If you are using self-signed certificate - you can also set `TF_BACKEND_GIT_HTTPS_SKIP_VERIFICATION=true` and that will enable `skip_cert_verification` in terraform config. + ### Basic HTTP Authentication You can use `TF_BACKEND_GIT_HTTP_USERNAME` and `TF_BACKEND_GIT_HTTP_PASSWORD` environment variables to add an extra layer of protection. In `wrapper` mode, same environment variables will be used to render `*.auto.tf` config for Terraform, but if you are using backend in standalone mode - you will have to tell these credentials to the Terraform explicitly: diff --git a/cmd/git_backend.go b/cmd/git_backend.go index e1e3096..f08d687 100644 --- a/cmd/git_backend.go +++ b/cmd/git_backend.go @@ -34,9 +34,10 @@ var gitBackendCmd = &cobra.Command{ t, err := template.New(gitHTTPBackendConfigPath).Parse(` terraform { backend "http" { - address = "http://localhost:{{ .port }}/?type=git&repository={{ .repository }}&ref={{ .ref }}&state={{ .state }}" - lock_address = "http://localhost:{{ .port }}/?type=git&repository={{ .repository }}&ref={{ .ref }}&state={{ .state }}" - unlock_address = "http://localhost:{{ .port }}/?type=git&repository={{ .repository }}&ref={{ .ref }}&state={{ .state }}" + address = "{{ .protocol }}://localhost:{{ .port }}/?type=git&repository={{ .repository }}&ref={{ .ref }}&state={{ .state }}" + lock_address = "{{ .protocol }}://localhost:{{ .port }}/?type=git&repository={{ .repository }}&ref={{ .ref }}&state={{ .state }}" + unlock_address = "{{ .protocol }}://localhost:{{ .port }}/?type=git&repository={{ .repository }}&ref={{ .ref }}&state={{ .state }}" + skip_cert_verification = {{ .skipHttpsVerification }} username = "{{ .username }}" password = "{{ .password }}" } @@ -46,14 +47,28 @@ terraform { log.Fatal(err) } + _, okHttpCert := os.LookupEnv("TF_BACKEND_GIT_HTTPS_CERT") + _, okHttpKey := os.LookupEnv("TF_BACKEND_GIT_HTTPS_KEY") + protocol := "http" + if okHttpCert && okHttpKey { + protocol = "https" + } + + skipHttpsVerification, okSkipHttpsVerification := os.LookupEnv("TF_BACKEND_GIT_HTTPS_SKIP_VERIFICATION") + if !okSkipHttpsVerification { + skipHttpsVerification = "false" + } + username, _ := os.LookupEnv("TF_BACKEND_GIT_HTTP_USERNAME") password, _ := os.LookupEnv("TF_BACKEND_GIT_HTTP_PASSWORD") addr := strings.Split(viper.GetString("address"), ":") p := map[string]string{ - "port": addr[len(addr)-1], - "username": username, - "password": password, + "port": addr[len(addr)-1], + "protocol": protocol, + "skipHttpsVerification": skipHttpsVerification, + "username": username, + "password": password, } for _, flag := range []string{"repository", "ref", "state"} { diff --git a/server/server.go b/server/server.go index 1744206..c216d83 100644 --- a/server/server.go +++ b/server/server.go @@ -34,7 +34,14 @@ func Start() { address := viper.GetString("address") log.Println("listen on", address) - log.Fatal(http.ListenAndServe(address, mux)) + + httpCert, okHttpCert := os.LookupEnv("TF_BACKEND_GIT_HTTPS_CERT") + httpKey, okHttpKey := os.LookupEnv("TF_BACKEND_GIT_HTTPS_KEY") + if okHttpCert && okHttpKey { + log.Fatal(http.ListenAndServeTLS(address, httpCert, httpKey, mux)) + } else { + log.Fatal(http.ListenAndServe(address, mux)) + } } // basicAuth checking for user authentication @@ -163,7 +170,7 @@ func handleFunc(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusOK) - response.Write(state) + _, _ = response.Write(state) case http.MethodPost: log.Printf("Saving state to %s", metadata.Params.String()) @@ -221,25 +228,24 @@ func (handler *handler) clientError(err error) { // If error was unknown, just use defaultCode and defaultResponse error message. func (handler *handler) responseError(defaultCode int, defaultResponse string, actualErr error) { log.Printf("%s", actualErr) - switch actualErr.(type) { + switch actualErr := actualErr.(type) { case *types.ErrLocked: handler.Response.WriteHeader(http.StatusConflict) - handler.Response.Write(actualErr.(*types.ErrLocked).Lock) + _, _ = handler.Response.Write(actualErr.Lock) default: switch actualErr { case types.ErrLockMissing: handler.Response.WriteHeader(http.StatusPreconditionRequired) - handler.Response.Write([]byte("428 - Locking Required")) + _, _ = handler.Response.Write([]byte("428 - Locking Required")) case types.ErrStateDidNotExisted: handler.Response.WriteHeader(http.StatusNoContent) case types.ErrUnauthorized: handler.Response.Header().Set("WWW-Authenticate", `Basic realm=terraform-backend-git`) handler.Response.WriteHeader(http.StatusUnauthorized) - handler.Response.Write([]byte("401 - Unauthorized")) + _, _ = handler.Response.Write([]byte("401 - Unauthorized")) default: handler.Response.WriteHeader(defaultCode) - handler.Response.Write([]byte(defaultResponse)) + _, _ = handler.Response.Write([]byte(defaultResponse)) } - } } diff --git a/test/tf/localhost.crt b/test/tf/localhost.crt new file mode 100644 index 0000000..30214ed --- /dev/null +++ b/test/tf/localhost.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC8DCCAdigAwIBAgIUMf5T8W7Y8GgzORoeySpVL45UnAswDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDUxNTA0NDE0M1oXDTIyMDYx +NDA0NDE0M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA1bISvpDhmZ35ekHx2TZaXwTlChwmuk2fUe21AIB8iNiq +Le27el6xRG2eyEwV5MioXLAmWMNyW2AK+R+rnAsXBcGSY52ud+Bh9Hq/3HwH0ulD +4iVZkvDVQyH2RA5o+09gtoq71vQvBHLqQySml5K9KFd4SWRPduwx8sm+15PJtnOO +ap0qmRjnd8nGe5fOZDGQakTOZ/+ym3T5eziIIMEi43vKnPcFaQY3AhO8rNDI8x1p +JVt2zss1ieaHWLUxd0jx0lmkPpc0m2iR9qJGoIJpxbONZaYIU1ACPBnPkK+/w7Hs +q+6dcaWbe163buCbu7toCIcpYOh/PtdlOZdZm3fwPwIDAQABozowODAUBgNVHREE +DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB +MA0GCSqGSIb3DQEBCwUAA4IBAQBjWnRYlqm3UeNZjwWaH/VGiIctW2C2gRLldWhb +CBxcJ6dga2Ys8vcOZ1tTFtBVUZyT0NgCzZ3eJD5n0E9s9UR8QbNzJxhBWJgvC+qV +vJV/K7AN1eXnr9DU+V/IoEOBJdF12DljeS6vWlxEdfmDqeO6A1q66mz0wnYcC63I +/3O651SJOCI11/oD5lMHX5la2wBEytn0vYRehOSA22EmGo4aSNKLP/ac38ptCLhk +ikkIJXEu0B0O+vvrwEeSy/encL6p+C2bybTdrbVs4b5+BwqJxfDEyDSq03loI4k4 +oevFJoyBSX3lhK8JnyjKdY13srZIm2PESxvTZKJiNsN894v3 +-----END CERTIFICATE----- diff --git a/test/tf/localhost.key b/test/tf/localhost.key new file mode 100644 index 0000000..a71be32 --- /dev/null +++ b/test/tf/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVshK+kOGZnfl6 +QfHZNlpfBOUKHCa6TZ9R7bUAgHyI2Kot7bt6XrFEbZ7ITBXkyKhcsCZYw3JbYAr5 +H6ucCxcFwZJjna534GH0er/cfAfS6UPiJVmS8NVDIfZEDmj7T2C2irvW9C8EcupD +JKaXkr0oV3hJZE927DHyyb7Xk8m2c45qnSqZGOd3ycZ7l85kMZBqRM5n/7KbdPl7 +OIggwSLje8qc9wVpBjcCE7ys0MjzHWklW3bOyzWJ5odYtTF3SPHSWaQ+lzSbaJH2 +okaggmnFs41lpghTUAI8Gc+Qr7/Dseyr7p1xpZt7Xrdu4Ju7u2gIhylg6H8+12U5 +l1mbd/A/AgMBAAECgf8gjgBNB8VWobpf5avya1VZJGXAJInB2BFtackpSmmub1N9 +q7nj6okW45xM50pukCMV+7/bxeqmAPuq+CgsnYPkXJjwlBUALi6+D/UqobqPZvnw +ecArooTatVHPDGLx5iXVqUz0cj63bspBcFPww2oNu0WX6LAybckuM43fSaRxJuJC +5wlUvCd8wiLn9bEJNPDypfK2ktrWIoPj13GCNgFkolcsaypN/KPEe5H08AFudf4P +qyB5VD6U1NszUcFqHpxELXxyrEyA5QF+KEQQETZr5hXkJEUoRHT5rllZkq5emVqc +gv9o/5Fwwcvqxk/i1IEQev4LlmTSMb5Z+not87kCgYEA707t5aL1b4mLWstT1XSH +ytveXkdqGZqhzYOsfQhAjYIgy9Gx/h1GNEweKjTfU9k05CvhC1+0P67CUqbj8YLZ +QkO4tF8J6bdxR/uU/ujutZy6BpxW0Ph2TNr3MWZ6FHTos0CzNFu6G0Az9Bc9/2h5 +zvVx6RMycy8tAvZbqqEAhaUCgYEA5JnOIpu90l5oRoGvo6GPqkjj1sfTgp5uZuGD +SnAav10oOyIvHM4IlLu4R2DrI+jlvJ/OhMFuJGR9OwyP5c3fHJ0l2BMFG8hwqeZJ +8PB3o9LpEgLlIbM4I2A6vshCl/4tgD4qSMctSNaSO6Knq644ePJdFXfajKlX6Iay +9iWY4RMCgYEAyshJUqOp8p/M1F0jZudeAgoZ/i3pvFSJ80o2qaSKft7bx5qjhz9r +M/mkPgObksOlzAtaoXaxmJ0P0VXWJdrJGxujskQudDub5HFNKkxbqs1p3RwxfNZt ++GY7vUKnBBqk7PBQanen1lurKpVfVcREI5lcszIvN+er7qyvtIDFnnkCgYEA2/wy +xVUwb3AQUsFcH1BLK2hncPntTcZeobyklo6Y/syL+ZPk2Ihg85hONspKnczyv/jd +SR3He1gEtz1YgKID8co8b9ml0d2qpaUKRMVzrIA7b+y/SRXpkQl6nruichfU+5NX +J6AcsPpj0OWvCuRmTeWVtCIZe8E+6nItZ/g4TWcCgYEA5ioiA6/uVlOwYF/Pckxc +9jJM6Pr+/taQkkdmrIo+kn2sBqq0v01pns9gMm243fWibp9rCanh6g5fNdgWErqB +vOTzHENSMXZsdki+yWrlCswT9cldNcd1dYSMHVHulWQOSB5zF7YBi0WhQMa3coox +6k4TesK9sHln8317eDYQlV0= +-----END PRIVATE KEY-----