Skip to content

Commit

Permalink
HTTP Basic auth support (#6)
Browse files Browse the repository at this point in the history
## 📝 Summary

Enables basic auth support for API requests. 

The basic auth password is configurable through API and/or file. If set
via API, the salted hash is stored in the file to persist across
reboots.

Config-file updates:

```toml
[general]
# 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
```

`basic_auth_secret_path` specifies the file to store the salted, hashed
secret in. It's loaded (or created) on startup.
- if the file is not empty, API requests need to include a http basic
auth password that matches that sha256 hash (user `admin`)
- if empty, no authentication is required for API requests until secret
is configured through API or file. if `/api/v1/set-basic-auth` is
called, it uses the payload as secret (immediately) and writes the hash
of the secret it to the file (for reuse across restarts).
- if file does not exist, it is created (empty)

Only the salted SHA256 hash of the password is stored, both in the file
as well as in memory.

The secret can be overwritten (updated) via API call, if the request
provides the previous http basic auth secret.

Also added tests and updated the README.

---

## ✅ I have run these commands

* [x] `make lint`
* [x] `make test`
* [x] `go mod tidy`
  • Loading branch information
metachris authored Nov 18, 2024
1 parent 90706a5 commit 347544f
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 52 deletions.
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

# 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

0 comments on commit 347544f

Please sign in to comment.