Skip to content

Commit

Permalink
feat: local mode token protection (#245)
Browse files Browse the repository at this point in the history
Co-authored-by: UncleGedd <42304551+UncleGedd@users.noreply.github.com>
Co-authored-by: Tristan Holaday <40547442+TristanHoladay@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 29, 2024
1 parent a99bc23 commit d356940
Show file tree
Hide file tree
Showing 29 changed files with 571 additions and 55 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/api-auth-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: API Auth Tests
on:
workflow_call:
pull_request:
branches: [main]
types: [milestoned, opened, edited, synchronize]
paths-ignore:
- "**.md"
- "**.jpg"
- "**.png"
- "**.gif"
- "**.svg"
- "adr/**"
- "docs/**"
- "CODEOWNERS"
- "goreleaser.yml"

permissions:
contents: read

jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- name: Setup Environment (Go, Node, Homebrew, UDS CLI, k3d)
uses: ./.github/actions/setup

- name: Run tests
run: uds run test:api-auth
timeout-minutes: 30

- name: Debug Output
uses: defenseunicorns/uds-common/.github/actions/debug-output@76287d41ec5f06ecbdd0a6453877a78675aceffe # v0.11.2

- name: Save logs
if: always()
uses: defenseunicorns/uds-common/.github/actions/save-logs@76287d41ec5f06ecbdd0a6453877a78675aceffe # v0.11.2
with:
suffix: api-auth
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ vite.config.ts.timestamp-*
zarf-sbom
tmp/
*.tar.zst
.vscode/

*.pem
.github/test-infra/**/.terraform*
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ ENV PORT=8080
EXPOSE 8080

# run binary
CMD ["./app/uds-runtime"]
# Disable API auth when running UDS Runtime in-cluster
CMD ["API_AUTH_DISABLED=true ./app/uds-runtime"]
13 changes: 13 additions & 0 deletions docs/api-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# API AUTHENTICATION

API authentication is used to prevent unauthorized access to the API from other processes when running UDS Runtime locally. The API uses a token-based authentication system. The token is generated by the backend server and is used to authenticate the user. API authentication is enabled by default, to disable it you can set the `API_AUTH_DISABLED` environment variable to true.

How does the frontend authenticate?
- Backend generates a token when it is started up and launches UDS Runtime in the browser.
- i.e.(Runtime API connection: `http://127.0.0.1:8080/auth?token=r1hrQ9CcuZMKpY2egjsPrzmge3-YqfqOHjmlIOvdKrLGOLnHPgFWt3dzsdkHwzDdXQAfRRHiH~rbGEx7Jc7rTxTd4riCuqGH`)
- Frontend hits the /auth-status endpoint to see if API authentication is enabled.
- This is done so that the frontend can get the value of the `API_AUTH_DISABLED` environment variable at runtime.
- The frontend passes the token as a query parameter in the URL to the backend to authenticate the user.
- When authenticated, the token is stored in `sessionStorage` and is valid for the duration of the page session.
- The token is then used when creating the EventSources for the various views.
- Reauthentication is possible by hitting the /auth endpoint with the token as a query parameter.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ require (
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/defenseunicorns/pkg/exec v0.0.1
github.com/defenseunicorns/pkg/helpers v1.1.1 // indirect
github.com/defenseunicorns/pkg/helpers/v2 v2.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/defenseunicorns/pkg/exec v0.0.1 h1:mZtkZvwvOgInZOi+hvEjT0JAtjOgbeIo4RkpqMzU85g=
github.com/defenseunicorns/pkg/exec v0.0.1/go.mod h1:F/OPhrZuoXM6e2RgeDUlaSYFFW8I3rsErJnytLSkLFo=
github.com/defenseunicorns/pkg/helpers v1.1.1 h1:p3pKeK5SeFaoZUJZIX9sEsJqX1CGGMS8OpQMPgJtSqM=
github.com/defenseunicorns/pkg/helpers v1.1.1/go.mod h1:F4S5VZLDrlNWQKklzv4v9tFWjjZNhxJ1gT79j4XiLwk=
github.com/defenseunicorns/pkg/helpers/v2 v2.0.1 h1:j08rz9vhyD9Bs+yKiyQMY2tSSejXRMxTqEObZ5M1Wbk=
github.com/defenseunicorns/pkg/helpers/v2 v2.0.1/go.mod h1:u1PAqOICZyiGIVA2v28g55bQH1GiAt0Bc4U9/rnWQvQ=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
Expand Down Expand Up @@ -150,8 +154,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand All @@ -177,8 +181,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zarf-dev/zarf v0.38.2 h1:1MPhFeFp2orXuN1Xzb5pSY88h6hK1ZHvNj5rXHGV+14=
github.com/zarf-dev/zarf v0.38.2/go.mod h1:R3yCUkrGU4zIXcYG1Vi7GJi3+enNdXFXWhLypT4yS5o=
github.com/zarf-dev/zarf v0.38.3 h1:XSNBh5juPyPHeSDlaWgUGn5m7Jm6g7LIeTQOOUKvdC0=
github.com/zarf-dev/zarf v0.38.3/go.mod h1:KAsR0Ppj4JcP4YOSH/mxd5dIM/95nUMvUNk6oVC3D4c=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
27 changes: 27 additions & 0 deletions pkg/api/auth/random.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2024-Present The UDS Authors

package auth

import (
"crypto/rand"
)

// Very limited special chars for git / basic auth
// https://owasp.org/www-community/password-special-characters has complete list of safe chars.
const randomStringChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~-"

// RandomString generates a secure random string of the specified length.
func RandomString(length int) (string, error) {
bytes := make([]byte, length)

if _, err := rand.Read(bytes); err != nil {
return "", err
}

for i, b := range bytes {
bytes[i] = randomStringChars[b%byte(len(randomStringChars))]
}

return string(bytes), nil
}
29 changes: 29 additions & 0 deletions pkg/api/auth/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2024-Present The UDS Authors

// Package auth provides an endpoint for authenticating against the runtime server.
package auth

import (
"net/http"
)

// RequireSecret ensures the request has a valid token.
func RequireSecret(validToken string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token != validToken {
w.WriteHeader(http.StatusUnauthorized)
return
}

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

// Connect is a head-only request to test the connection.
func Connect(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}
58 changes: 57 additions & 1 deletion pkg/api/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ import (
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"

"strings"

"encoding/json"

"github.com/defenseunicorns/pkg/exec"
"github.com/defenseunicorns/uds-runtime/pkg/api/auth"
_ "github.com/defenseunicorns/uds-runtime/pkg/api/docs" //nolint:staticcheck
udsMiddleware "github.com/defenseunicorns/uds-runtime/pkg/api/middleware"
"github.com/defenseunicorns/uds-runtime/pkg/api/monitor"
Expand All @@ -29,6 +35,25 @@ import (
// @BasePath /api/v1
// @schemes http https
func Setup(assets *embed.FS) (*chi.Mux, error) {
apiAuth := true
if strings.ToLower(os.Getenv("API_AUTH_DISABLED")) == "true" {
apiAuth = false
}
port := "8080"

ip := "127.0.0.1"

// If the env variable API_TOKEN is set, use that for the API secret
token := os.Getenv("API_TOKEN")
var err error
// Otherwise, generate a random secret
if token == "" {
token, err = auth.RandomString(96)
if err != nil {
return nil, fmt.Errorf("failed to generate random string: %w", err)
}
}

r := chi.NewRouter()

r.Use(udsMiddleware.ConditionalCompress)
Expand All @@ -43,7 +68,16 @@ func Setup(assets *embed.FS) (*chi.Mux, error) {

// Add Swagger UI route
r.Get("/swagger/*", httpSwagger.WrapHandler)
// expose API_AUTH_DISABLED env var to frontend via endpoint
r.Get("/auth-status", serveAuthStatus)
r.Route("/api/v1", func(r chi.Router) {
// Require a valid token for API calls
if apiAuth {
// If api auth is enabled, require a valid token for all routes under /api/v1
r.Use(auth.RequireSecret(token))
// Endpoint to test if connected with auth
r.Head("/", auth.Connect)
}
r.Route("/monitor", func(r chi.Router) {
r.Get("/pepr/", monitor.Pepr)
r.Get("/pepr/{stream}", monitor.Pepr)
Expand Down Expand Up @@ -158,6 +192,17 @@ func Setup(assets *embed.FS) (*chi.Mux, error) {
})
})

if apiAuth {
colorYellow := "\033[33m"
colorReset := "\033[0m"
url := fmt.Sprintf("http://%s:%s/auth?token=%s", ip, port, token)
log.Printf("%sRuntime API connection: %s%s", colorYellow, url, colorReset)
err := exec.LaunchURL(url)
if err != nil {
return nil, fmt.Errorf("failed to launch URL: %w", err)
}
}

// Serve static files from embed.FS
if assets != nil {
staticFS, err := fs.Sub(assets, "ui/build")
Expand All @@ -169,7 +214,6 @@ func Setup(assets *embed.FS) (*chi.Mux, error) {
return nil, fmt.Errorf("failed to serve static files: %w", err)
}
}

return r, nil
}

Expand Down Expand Up @@ -215,3 +259,15 @@ func fileServer(r chi.Router, root http.FileSystem) error {

return nil
}

func serveAuthStatus(w http.ResponseWriter, _ *http.Request) {
config := map[string]string{
"API_AUTH_DISABLED": os.Getenv("API_AUTH_DISABLED"),
}

w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(config)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
31 changes: 26 additions & 5 deletions pkg/test/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
Expand All @@ -19,10 +20,22 @@ import (
"github.com/stretchr/testify/require"
)

func TestQueryParams(t *testing.T) {
func setup() (*chi.Mux, error) {
os.Setenv("API_AUTH_DISABLED", "true")
r, err := api.Setup(nil)
return r, err
}

func teardown() {
os.Setenv("API_AUTH_DISABLED", "false")
}

func TestQueryParams(t *testing.T) {
r, err := setup()
require.NoError(t, err)

defer teardown()

tests := []struct {
name string
url string
Expand Down Expand Up @@ -103,9 +116,11 @@ type TestRoute struct {
}

func TestPodRoutes(t *testing.T) {
r, err := api.Setup(nil)
r, err := setup()
require.NoError(t, err)

defer teardown()

// Map so can mutate reference between tests
uidMap := map[string]string{"uid": ""}

Expand All @@ -128,9 +143,11 @@ func TestPodRoutes(t *testing.T) {
}

func TestPackageRoutes(t *testing.T) {
r, err := api.Setup(nil)
r, err := setup()
require.NoError(t, err)

defer teardown()

// Map so can mutate reference between tests
uidMap := map[string]string{"uid": ""}

Expand All @@ -153,9 +170,11 @@ func TestPackageRoutes(t *testing.T) {
}

func TestPeprRoutes(t *testing.T) {
r, err := api.Setup(nil)
r, err := setup()
require.NoError(t, err)

defer teardown()

peprTests := []TestRoute{
{
name: "uds-packages",
Expand Down Expand Up @@ -191,9 +210,11 @@ func TestPeprRoutes(t *testing.T) {
}

func TestClusterOverview(t *testing.T) {
r, err := api.Setup(nil)
r, err := setup()
require.NoError(t, err)

defer teardown()

t.Run("cluster-overview", func(t *testing.T) {
// Create a new context with a timeout -- increase to 2s to aggregate data
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
Expand Down
12 changes: 12 additions & 0 deletions tasks/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ includes:
- setup: ./setup.yaml

tasks:
- name: api-auth
description: "run end-to-end tests (assumes api server is running on port 8080)"
actions:
- task: setup:build-ui
- cmd: npm run test:install # install playwright
dir: ui
- task: setup:build-api
- cmd: |
k3d cluster delete runtime && k3d cluster create runtime --k3s-arg "--disable=traefik@server:*" --k3s-arg "--disable=servicelb@server:*"
- task: deploy-load
- cmd: npm run test:api-auth
dir: ui
- name: e2e
description: "run end-to-end tests (assumes api server is running on port 8080)"
actions:
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:api-auth": "playwright test tests/api-auth.spec.ts --config=playwright.config.apiauth.ts",
"test:integration": "playwright test",
"test:install": "playwright install",
"test:unit": "vitest run"
Expand Down
Loading

0 comments on commit d356940

Please sign in to comment.