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

HTTP Basic auth support #6

Merged
merged 7 commits into from
Nov 18, 2024
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
/build
/cert.pem
/key.pem
/pipe.fifo
/pipe.fifo
/basic-auth-secret.txt
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ It currently does the following things:
hashes, etc.
- **Actions**: Ability to execute shell commands via API
- **Configuration** through file uploads

Future features:

- Set a password for http-basic-auth (persisted, for all future requests)
- **HTTP Basic Auth** for API endpoints
- All actions show up in the event log

---

Expand Down Expand Up @@ -93,3 +91,43 @@ $ go run cmd/system-api/main.go --config systemapi-config.toml
# Execute the example action
$ curl -v -X POST -d "@README.md" localhost:3535/api/v1/file-upload/testfile
```

## HTTP Basic Auth

All API endpoints can be protected with HTTP Basic Auth.

The API endpoints are initially unauthenticated, until a secret is configured
either via file or via API. If the secret is configured via API, the salted SHA256
hash is be stored in a file (specified in the config file) to enable basic auth protection
across restarts.

The config file ([systemapi-config.toml](./systemapi-config.toml)) includes a `basic_auth_secret_path`.
- If the file exists and is not empty, then the APIs are authenticated for passwords that match the hash in this file.
- If the file exists and is empty, then the APIs are unauthenticated until a secret is configured.
- If this file is specified but doesn't exist, system-api will create it (empty).

```bash
# The included systemapi-config.toml uses basic-auth-secret.txt for basic_auth_secret_path
cat systemapi-config.toml

# Start the server
go run cmd/system-api/main.go --config systemapi-config.toml

# Initially, requests are unauthenticated
curl -v localhost:3535/livez

# Set the basic auth secret. From here on, authentication is required for all API requests.
curl -d "foobar" localhost:3535/api/v1/set-basic-auth
MoeMahhouk marked this conversation as resolved.
Show resolved Hide resolved

# Check that hash was written to the file
cat basic-auth-secret.txt

# API calls with no basic auth credentials are provided fail now, with '401 Unauthorized' because
curl -v localhost:3535/livez

# API calls work if correct basic auth credentials are provided
curl -v -u admin:foobar localhost:3535/livez

# The update also shows up in the logs
curl -u admin:foobar localhost:3535/logs
```
7 changes: 2 additions & 5 deletions cmd/system-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,9 @@ func runCli(cCtx *cli.Context) (err error) {
)

// Setup and start the server (in the background)
cfg := &systemapi.HTTPServerConfig{
Log: log,
Config: config,
}
server, err := systemapi.NewServer(cfg)
server, err := systemapi.NewServer(log, config)
if err != nil {
log.Error("Error creating server", "err", err)
return err
}
go server.Start()
Expand Down
11 changes: 10 additions & 1 deletion systemapi-config.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
[general]
listen_addr = "0.0.0.0:3535"
pipe_file = "pipe.fifo"
pprof = true
log_json = false
log_debug = true

# HTTP Basic Auth
basic_auth_secret_path = "basic-auth-secret.txt" # basic auth is supported if a path is provided
basic_auth_secret_salt = "D;%yL9TS:5PalS/d" # use a random string for the salt

# HTTP server timeouts
# http_read_timeout_ms = 2500
# http_write_timeout_ms = 2500

[actions]
echo_test = "echo test"
# reboot = "reboot"
# rbuilder_restart = "/etc/init.d/rbuilder restart"
# rbuilder_stop = "/etc/init.d/rbuilder stop"
echo_test = "echo test"

[file_uploads]
testfile = "/tmp/testfile.txt"
15 changes: 11 additions & 4 deletions systemapi/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import (
)

type systemAPIConfigGeneral struct {
ListenAddr string `toml:"listen_addr"`
PipeFile string `toml:"pipe_file"`
LogJSON bool `toml:"log_json"`
LogDebug bool `toml:"log_debug"`
ListenAddr string `toml:"listen_addr"`
PipeFile string `toml:"pipe_file"`
LogJSON bool `toml:"log_json"`
LogDebug bool `toml:"log_debug"`
EnablePprof bool `toml:"pprof"` // Enables pprof endpoints

BasicAuthSecretPath string `toml:"basic_auth_secret_path"`
BasicAuthSecretSalt string `toml:"basic_auth_secret_salt"`

HTTPReadTimeoutMillis int `toml:"http_read_timeout_ms"`
HTTPWriteTimeoutMillis int `toml:"http_write_timeout_ms"`
}

type SystemAPIConfig struct {
Expand Down
52 changes: 52 additions & 0 deletions systemapi/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package systemapi

import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"net/http"
)

// BasicAuth implements a simple middleware handler for adding basic http auth to a route.
func BasicAuth(realm, salt string, getHashedCredentials func() map[string]string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Loading credentials dynamically because they can be updated at runtime
hashedCredentials := getHashedCredentials()

// If no credentials are set, just pass through (unauthenticated)
if len(hashedCredentials) == 0 {
next.ServeHTTP(w, r)
return
}

// Load credentials from request
user, pass, ok := r.BasicAuth()
if !ok {
basicAuthFailed(w, realm)
return
}

// Hash the password and see if credentials are allowed
h := sha256.New()
h.Write([]byte(pass))
h.Write([]byte(salt))
userPassHash := hex.EncodeToString(h.Sum(nil))

// Compare to allowed credentials
credPassHash, credUserOk := hashedCredentials[user]
if !credUserOk || subtle.ConstantTimeCompare([]byte(userPassHash), []byte(credPassHash)) != 1 {
basicAuthFailed(w, realm)
return
}

next.ServeHTTP(w, r)
})
}
}

func basicAuthFailed(w http.ResponseWriter, realm string) {
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
w.WriteHeader(http.StatusUnauthorized)
}
Loading
Loading