From f5ccccbfc801d084093536ef8206f4846a469602 Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Mon, 4 Aug 2025 21:33:44 +0100 Subject: [PATCH 1/5] feat: add AWS ECR registry --- .github/workflows/docker.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 240ed77..ee7af85 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,7 +9,9 @@ on: env: REGISTRY_GHCR: ghcr.io - IMAGE_NAME: blues-expert-mcp + IMAGE_NAME_GHCR: blues-expert-mcp + REGISTRY_ECR: 660644769541.dkr.ecr.us-east-1.amazonaws.com + IMAGE_NAME_ECR: /blues-dev/blues-expert-mcp jobs: build-and-push: @@ -32,12 +34,23 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Log in to Amazon ECR + uses: aws-actions/amazon-ecr-login@v2 + - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: | - ${{ env.REGISTRY_GHCR }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + ${{ env.REGISTRY_GHCR }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME_GHCR }} + ${{ env.REGISTRY_ECR }}${{ env.IMAGE_NAME_ECR }} tags: | type=ref,event=branch type=ref,event=pr From 7e6a11c4b7e69f3abc99d6134206276ad94c9ac9 Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 5 Aug 2025 14:06:02 +0100 Subject: [PATCH 2/5] feat: add basic auth to logging --- blues-expert/main.go | 46 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/blues-expert/main.go b/blues-expert/main.go index 685fef4..79eca7a 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -1,6 +1,7 @@ package main import ( + "crypto/subtle" "flag" "log" "net/http" @@ -22,6 +23,38 @@ func init() { flag.StringVar(&envFilePath, "env", "", "Path to .env file to load environment variables") } +// withBasicAuth wraps an HTTP handler with basic authentication +func withBasicAuth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + username := os.Getenv("LOGS_AUTH_USER") + password := os.Getenv("LOGS_AUTH_PASS") + + // Skip auth if credentials are not configured + if username == "" || password == "" { + log.Printf("Warning: LOGS_AUTH_USER or LOGS_AUTH_PASS not set, logging endpoints are unprotected") + handler(w, r) + return + } + + user, pass, ok := r.BasicAuth() + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="Logging Endpoints"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Use constant-time comparison to prevent timing attacks + if subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || + subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { + w.Header().Set("WWW-Authenticate", `Basic realm="Logging Endpoints"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + handler(w, r) + } +} + func main() { flag.Parse() @@ -100,9 +133,9 @@ func main() { // Logging endpoints loggingEnabled := os.Getenv("ENABLE_LOGGING_ENDPOINTS") != "" if loggingEnabled { - mux.HandleFunc("/logs", lib.LogsHandler) - mux.HandleFunc("/logs/stream", lib.LogsStreamHandler) - mux.HandleFunc("/logs/stats", lib.LogsStatsHandler) + mux.HandleFunc("/logs", withBasicAuth(lib.LogsHandler)) + mux.HandleFunc("/logs/stream", withBasicAuth(lib.LogsStreamHandler)) + mux.HandleFunc("/logs/stats", withBasicAuth(lib.LogsStatsHandler)) } // Route all other requests to the MCP server @@ -124,9 +157,10 @@ func main() { log.Printf("Metrics available at /metrics") if loggingEnabled { - log.Printf("Logs available at /logs") - log.Printf("Logs streaming (Loki) at /logs/stream") - log.Printf("Logs buffer stats at /logs/stats") + log.Printf("Logs available at /logs (requires basic auth)") + log.Printf("Logs streaming (Loki) at /logs/stream (requires basic auth)") + log.Printf("Logs buffer stats at /logs/stats (requires basic auth)") + log.Printf("Set LOGS_AUTH_USER and LOGS_AUTH_PASS environment variables for authentication") } else { log.Printf("Logging endpoints disabled (set ENABLE_LOGGING_ENDPOINTS to enable)") } From 9cb69cf07ed6311fd14dac4175cbbb4d1a62568b Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 5 Aug 2025 15:30:13 +0100 Subject: [PATCH 3/5] chore: cleanup --- blues-expert/main.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/blues-expert/main.go b/blues-expert/main.go index 79eca7a..d373dba 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -23,13 +23,11 @@ func init() { flag.StringVar(&envFilePath, "env", "", "Path to .env file to load environment variables") } -// withBasicAuth wraps an HTTP handler with basic authentication func withBasicAuth(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { username := os.Getenv("LOGS_AUTH_USER") password := os.Getenv("LOGS_AUTH_PASS") - // Skip auth if credentials are not configured if username == "" || password == "" { log.Printf("Warning: LOGS_AUTH_USER or LOGS_AUTH_PASS not set, logging endpoints are unprotected") handler(w, r) @@ -130,8 +128,10 @@ func main() { // Metrics endpoint mux.Handle("/metrics", promhttp.Handler()) - // Logging endpoints - loggingEnabled := os.Getenv("ENABLE_LOGGING_ENDPOINTS") != "" + // Logging endpoints - enabled if authentication credentials are provided + logsAuthUser := os.Getenv("LOGS_AUTH_USER") + logsAuthPass := os.Getenv("LOGS_AUTH_PASS") + loggingEnabled := logsAuthUser != "" && logsAuthPass != "" if loggingEnabled { mux.HandleFunc("/logs", withBasicAuth(lib.LogsHandler)) mux.HandleFunc("/logs/stream", withBasicAuth(lib.LogsStreamHandler)) @@ -160,9 +160,8 @@ func main() { log.Printf("Logs available at /logs (requires basic auth)") log.Printf("Logs streaming (Loki) at /logs/stream (requires basic auth)") log.Printf("Logs buffer stats at /logs/stats (requires basic auth)") - log.Printf("Set LOGS_AUTH_USER and LOGS_AUTH_PASS environment variables for authentication") } else { - log.Printf("Logging endpoints disabled (set ENABLE_LOGGING_ENDPOINTS to enable)") + log.Printf("Logging endpoints disabled (set credentials to enable)") } // Start HTTP server with our custom multiplexer From aeb38fa83bb2c92ada76496b68a734140021339e Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 5 Aug 2025 16:13:56 +0100 Subject: [PATCH 4/5] fix: use OIDC --- .github/workflows/docker.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ee7af85..6bb61ee 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -19,6 +19,7 @@ jobs: permissions: contents: read packages: write + id-token: write steps: - name: Checkout repository @@ -37,8 +38,8 @@ jobs: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: arn:aws:iam::660644769541:role/blues-expert-mcp-github + role-session-name: GitHubActions aws-region: us-east-1 - name: Log in to Amazon ECR From 53336f4a466471ecfcf2ca6e847ef01c99d162cd Mon Sep 17 00:00:00 2001 From: Alex Bucknall Date: Tue, 5 Aug 2025 16:23:25 +0100 Subject: [PATCH 5/5] fix: aws audience --- .github/workflows/docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 6bb61ee..9946a69 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -41,6 +41,7 @@ jobs: role-to-assume: arn:aws:iam::660644769541:role/blues-expert-mcp-github role-session-name: GitHubActions aws-region: us-east-1 + audience: sts.amazonaws.com - name: Log in to Amazon ECR uses: aws-actions/amazon-ecr-login@v2