From cbf35ca0edaf1321c6726c39b3c078b632395bc2 Mon Sep 17 00:00:00 2001 From: FBLGit Date: Fri, 22 Oct 2021 05:58:30 +0800 Subject: [PATCH] feat: add BasicAuth Support to Atlantis ServeHTTP (#1777) * Add BasicAuth Support to Atlantis ServeHTTP * Added Security notes Co-authored-by: xmurias --- cmd/server.go | 24 +++++++++++++++++++ runatlantis.io/docs/security.md | 11 ++++++++- server/middleware.go | 41 +++++++++++++++++++++++++++++---- server/server.go | 9 ++++++-- server/user_config.go | 3 +++ 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index 0699d5b825..882ac23ab5 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -103,6 +103,9 @@ const ( TFEHostnameFlag = "tfe-hostname" TFETokenFlag = "tfe-token" WriteGitCredsFlag = "write-git-creds" + WebBasicAuthFlag = "web-basic-auth" + WebUsernameFlag = "web-username" + WebPasswordFlag = "web-password" // NOTE: Must manually set these as defaults in the setDefaults function. DefaultADBasicUser = "" @@ -120,6 +123,9 @@ const ( DefaultTFDownloadURL = "https://releases.hashicorp.com" DefaultTFEHostname = "app.terraform.io" DefaultVCSStatusName = "atlantis" + DefaultWebBasicAuth = false + DefaultWebUsername = "atlantis" + DefaultWebPassword = "atlantis" ) var stringFlags = map[string]stringFlag{ @@ -287,6 +293,14 @@ var stringFlags = map[string]stringFlag{ description: "Name used to identify Atlantis for pull request statuses.", defaultValue: DefaultVCSStatusName, }, + WebUsernameFlag: { + description: "Username used for Web Basic Authentication on Atlantis HTTP Middleware", + defaultValue: DefaultWebUsername, + }, + WebPasswordFlag: { + description: "Password used for Web Basic Authentication on Atlantis HTTP Middleware", + defaultValue: DefaultWebPassword, + }, } var boolFlags = map[string]boolFlag{ @@ -385,6 +399,10 @@ var boolFlags = map[string]boolFlag{ description: "Skips cloning the PR repo if there are no projects were changed in the PR.", defaultValue: false, }, + WebBasicAuthFlag: { + description: "Switches on or off the Basic Authentication on the HTTP Middleware interface", + defaultValue: DefaultWebBasicAuth, + }, } var intFlags = map[string]intFlag{ ParallelPoolSize: { @@ -634,6 +652,12 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) { if c.TFEHostname == "" { c.TFEHostname = DefaultTFEHostname } + if c.WebUsername == "" { + c.WebUsername = DefaultWebUsername + } + if c.WebPassword == "" { + c.WebPassword = DefaultWebPassword + } } func (s *ServerCmd) validate(userConfig server.UserConfig) error { diff --git a/runatlantis.io/docs/security.md b/runatlantis.io/docs/security.md index 4793190afe..a1cfa57812 100644 --- a/runatlantis.io/docs/security.md +++ b/runatlantis.io/docs/security.md @@ -34,7 +34,7 @@ To prevent this, allowlist [Bitbucket's IP addresses](https://confluence.atlassi ## Mitigations ### Don't Use On Public Repos -Because anyone can comment on public pull requests, even with all the security mitigations available, it's still dangerous to run Atlantis on public repos until Atlantis gets an authentication system. +Because anyone can comment on public pull requests, even with all the security mitigations available, it's still dangerous to run Atlantis on public repos without proper configuration of the security settings. ### Don't Use `--allow-fork-prs` If you're running on a public repo (which isn't recommended, see above) you shouldn't set `--allow-fork-prs` (defaults to false) @@ -79,3 +79,12 @@ Azure DevOps supports sending a basic authentication header in all webhook event If you're using webhook secrets but your traffic is over HTTP then the webhook secrets could be stolen. Enable SSL/HTTPS using the `--ssl-cert-file` and `--ssl-key-file` flags. + +### Enable Authentication on Atlantis Web Server +It is very reccomended to enable authentication in the web service. Enable BasicAuth using the `--web-basic-auth=true` and setup a username and a password using `--web-username=yourUsername` and `--web-password=yourPassword` flags. + +You can also pass these as environment variables `ATLANTIS_WEB_BASIC_AUTH=true` `ATLANTIS_WEB_USERNAME=yourUsername` and `ATLANTIS_WEB_PASSWORD=yourPassword`. + +::tip Tip +We do encourage the usage of complex passwords in order to prevent basic bruteforcing attacks. +::: \ No newline at end of file diff --git a/server/middleware.go b/server/middleware.go index 5aee2ebb6d..4a8ec444d4 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -21,18 +21,49 @@ import ( ) // NewRequestLogger creates a RequestLogger. -func NewRequestLogger(logger logging.SimpleLogging) *RequestLogger { - return &RequestLogger{logger} +func NewRequestLogger(s *Server) *RequestLogger { + return &RequestLogger{ + s.Logger, + s.WebAuthentication, + s.WebUsername, + s.WebPassword, + } } -// RequestLogger logs requests and their response codes. +// RequestLogger logs requests and their response codes +// as well as handle the basicauth on the requests type RequestLogger struct { - logger logging.SimpleLogging + logger logging.SimpleLogging + WebAuthentication bool + WebUsername string + WebPassword string } // ServeHTTP implements the middleware function. It logs all requests at DEBUG level. func (l *RequestLogger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { l.logger.Debug("%s %s – from %s", r.Method, r.URL.RequestURI(), r.RemoteAddr) - next(rw, r) + allowed := false + if r.URL.Path == "/events" || !l.WebAuthentication { + allowed = true + } else { + user, pass, ok := r.BasicAuth() + if ok { + r.SetBasicAuth(user, pass) + l.logger.Debug("user: %s / pass: %s >> url: %s", user, pass, r.URL.RequestURI()) + if user == l.WebUsername && pass == l.WebPassword { + l.logger.Debug("[VALID] user: %s / pass: %s >> url: %s", user, pass, r.URL.RequestURI()) + allowed = true + } else { + allowed = false + l.logger.Info("[INVALID] user: %s / pass: %s >> url: %s", user, pass, r.URL.RequestURI()) + } + } + } + if !allowed { + rw.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(rw, "Unauthorized", http.StatusUnauthorized) + } else { + next(rw, r) + } l.logger.Debug("%s %s – respond HTTP %d", r.Method, r.URL.RequestURI(), rw.(negroni.ResponseWriter).Status()) } diff --git a/server/server.go b/server/server.go index e14158ebff..5f2502b9e8 100644 --- a/server/server.go +++ b/server/server.go @@ -97,6 +97,9 @@ type Server struct { SSLCertFile string SSLKeyFile string Drainer *events.Drainer + WebAuthentication bool + WebUsername string + WebPassword string } // Config holds config for server that isn't passed in by the user. @@ -655,7 +658,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GithubOrg: userConfig.GithubOrg, GithubStatusName: userConfig.VCSStatusName, } - return &Server{ AtlantisVersion: config.AtlantisVersion, AtlantisURL: parsedURL, @@ -675,6 +677,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { SSLKeyFile: userConfig.SSLKeyFile, SSLCertFile: userConfig.SSLCertFile, Drainer: drainer, + WebAuthentication: userConfig.WebBasicAuth, + WebUsername: userConfig.WebUsername, + WebPassword: userConfig.WebPassword, }, nil } @@ -699,7 +704,7 @@ func (s *Server) Start() error { PrintStack: false, StackAll: false, StackSize: 1024 * 8, - }, NewRequestLogger(s.Logger)) + }, NewRequestLogger(s)) n.UseHandler(s.Router) defer s.Logger.Flush() diff --git a/server/user_config.go b/server/user_config.go index 297b692886..13d7071e62 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -87,6 +87,9 @@ type UserConfig struct { VCSStatusName string `mapstructure:"vcs-status-name"` DefaultTFVersion string `mapstructure:"default-tf-version"` Webhooks []WebhookConfig `mapstructure:"webhooks"` + WebBasicAuth bool `mapstructure:"web-basic-auth"` + WebUsername string `mapstructure:"web-username"` + WebPassword string `mapstructure:"web-password"` WriteGitCreds bool `mapstructure:"write-git-creds"` }