diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 240ed77..9946a69 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: @@ -17,6 +19,7 @@ jobs: permissions: contents: read packages: write + id-token: write steps: - name: Checkout repository @@ -32,12 +35,24 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + 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 + - 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 diff --git a/blues-expert/main.go b/blues-expert/main.go index 685fef4..d373dba 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,36 @@ func init() { flag.StringVar(&envFilePath, "env", "", "Path to .env file to load environment variables") } +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") + + 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() @@ -97,12 +128,14 @@ 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", 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,11 +157,11 @@ 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)") } 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