Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add BasicAuth Support to Atlantis ServeHTTP #1777

Merged
merged 2 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,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 = ""
Expand All @@ -117,6 +120,9 @@ const (
DefaultTFDownloadURL = "https://releases.hashicorp.com"
DefaultTFEHostname = "app.terraform.io"
DefaultVCSStatusName = "atlantis"
DefaultWebBasicAuth = false
DefaultWebUsername = "atlantis"
DefaultWebPassword = "atlantis"
)

var stringFlags = map[string]stringFlag{
Expand Down Expand Up @@ -280,6 +286,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{
Expand Down Expand Up @@ -374,6 +388,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: {
Expand Down Expand Up @@ -620,6 +638,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 {
Expand Down
11 changes: 10 additions & 1 deletion runatlantis.io/docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
:::
41 changes: 36 additions & 5 deletions server/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
9 changes: 7 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,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.
Expand Down Expand Up @@ -652,7 +655,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
GithubHostname: userConfig.GithubHostname,
GithubOrg: userConfig.GithubOrg,
}

return &Server{
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: parsedURL,
Expand All @@ -672,6 +674,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
}

Expand All @@ -696,7 +701,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()
Expand Down
3 changes: 3 additions & 0 deletions server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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"`
}

Expand Down