Skip to content
Open
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
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,24 @@ The Loki query tool supports the following environment variables:
- `LOKI_PASSWORD`: Default password for basic authentication if not specified in the request
- `LOKI_TOKEN`: Default bearer token for authentication if not specified in the request

**Security Note**: When using authentication environment variables, be careful not to expose sensitive credentials in logs or configuration files. Consider using token-based authentication over username/password when possible.
##### mTLS Authentication

For environments requiring mutual TLS (mTLS) authentication:

- `LOKI_CLIENT_CERT`: Path to client certificate file for mTLS authentication
- `LOKI_CLIENT_KEY`: Path to client key file for mTLS authentication
- `LOKI_CA_CERT`: Path to CA certificate file for mTLS authentication

##### TLS Configuration

For TLS connection configuration:

- `LOKI_TLS_INSECURE_SKIP_VERIFY`: Skip TLS certificate verification (set to "true" to disable certificate validation). **Use with caution - only for development/testing environments with self-signed certificates.**

**Security Notes**:
- When using authentication environment variables, be careful not to expose sensitive credentials in logs or configuration files. Consider using token-based authentication over username/password when possible.
- mTLS certificates should be properly secured and rotated regularly.
- Only use `LOKI_TLS_INSECURE_SKIP_VERIFY=true` in development environments or when dealing with self-signed certificates. This setting is controlled at the deployment level and cannot be overridden by the LLM for security reasons.

### Testing the MCP Server

Expand Down Expand Up @@ -107,6 +124,18 @@ export LOKI_URL="http://localhost:3100"
export LOKI_TOKEN="your-bearer-token"
./loki-mcp-client loki_query "{job=\"varlogs\"}"

# Using environment variables with mTLS authentication:
export LOKI_URL="https://secure-loki.example.com"
export LOKI_CLIENT_CERT="/path/to/client.crt"
export LOKI_CLIENT_KEY="/path/to/client.key"
export LOKI_CA_CERT="/path/to/ca.crt"
./loki-mcp-client loki_query "{job=\"varlogs\"}"

# Using environment variables with TLS skip verify (development only):
export LOKI_URL="https://dev-loki.example.com"
export LOKI_TLS_INSECURE_SKIP_VERIFY="true"
./loki-mcp-client loki_query "{job=\"varlogs\"}"

# Using all environment variables together:
export LOKI_URL="http://localhost:3100"
export LOKI_ORG_ID="tenant-123"
Expand Down Expand Up @@ -302,7 +331,11 @@ Or create your own configuration:
"LOKI_ORG_ID": "your-default-org-id",
"LOKI_USERNAME": "your-username",
"LOKI_PASSWORD": "your-password",
"LOKI_TOKEN": "your-bearer-token"
"LOKI_TOKEN": "your-bearer-token",
"LOKI_CLIENT_CERT": "/path/to/client.crt",
"LOKI_CLIENT_KEY": "/path/to/client.key",
"LOKI_CA_CERT": "/path/to/ca.crt",
"LOKI_TLS_INSECURE_SKIP_VERIFY": "false"
},
"disabled": false,
"autoApprove": ["loki_query"]
Expand Down Expand Up @@ -372,6 +405,11 @@ Docker configuration:
"-e", "LOKI_USERNAME=your-username",
"-e", "LOKI_PASSWORD=your-password",
"-e", "LOKI_TOKEN=your-bearer-token",
"-e", "LOKI_CLIENT_CERT=/certs/client.crt",
"-e", "LOKI_CLIENT_KEY=/certs/client.key",
"-e", "LOKI_CA_CERT=/certs/ca.crt",
"-e", "LOKI_TLS_INSECURE_SKIP_VERIFY=false",
"-v", "/path/to/certs:/certs:ro",
"loki-mcp-server:latest"]
}
}
Expand Down
140 changes: 131 additions & 9 deletions internal/handlers/loki.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package handlers

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -57,6 +59,18 @@ const EnvLokiPassword = "LOKI_PASSWORD"
// Environment variable name for Loki Token
const EnvLokiToken = "LOKI_TOKEN"

// Environment variable name for Loki client certificate file
const EnvLokiClientCert = "LOKI_CLIENT_CERT"

// Environment variable name for Loki client key file
const EnvLokiClientKey = "LOKI_CLIENT_KEY"

// Environment variable name for Loki CA certificate file
const EnvLokiCACert = "LOKI_CA_CERT"

// Environment variable name for TLS insecure skip verify
const EnvLokiTLSInsecureSkipVerify = "LOKI_TLS_INSECURE_SKIP_VERIFY"

// Default Loki URL when environment variable is not set
const DefaultLokiURL = "http://localhost:3100"

Expand All @@ -74,6 +88,42 @@ type LokiLabelValuesResult struct {
Error string `json:"error,omitempty"`
}

// createTLSConfig creates a TLS configuration for mTLS authentication
func createTLSConfig(clientCertFile, clientKeyFile, caCertFile string, insecureSkipVerify bool) (*tls.Config, error) {
tlsConfig := &tls.Config{
InsecureSkipVerify: insecureSkipVerify,
}

// Load client certificate and key if provided
if clientCertFile != "" && clientKeyFile != "" {
cert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate: %v", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}

// Load CA certificate if provided
if caCertFile != "" {
// Read the CA certificate file
caCert, err := os.ReadFile(caCertFile)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate file: %v", err)
}

// Create a certificate pool and add the CA certificate
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to parse CA certificate")
}

// Set the CA certificate pool in the TLS configuration
tlsConfig.RootCAs = caCertPool
}

return tlsConfig, nil
}

// NewLokiQueryTool creates and returns a tool for querying Grafana Loki
func NewLokiQueryTool() mcp.Tool {
// Get Loki URL from environment variable or use default
Expand Down Expand Up @@ -173,6 +223,15 @@ func HandleLokiQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal
orgID = os.Getenv(EnvLokiOrgID)
}

// Extract mTLS certificate parameters from environment variables only
clientCert := os.Getenv(EnvLokiClientCert)
clientKey := os.Getenv(EnvLokiClientKey)
caCert := os.Getenv(EnvLokiCACert)

// Extract TLS insecure skip verify from environment variable only
envValue := os.Getenv(EnvLokiTLSInsecureSkipVerify)
tlsInsecureSkipVerify := envValue == "true"

// Set defaults for optional parameters
start := time.Now().Add(-1 * time.Hour).Unix()
end := time.Now().Unix()
Expand Down Expand Up @@ -212,7 +271,7 @@ func HandleLokiQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal
}

// Execute query with authentication
result, err := executeLokiQuery(ctx, queryURL, username, password, token, orgID)
result, err := executeLokiQuery(ctx, queryURL, username, password, token, orgID, clientCert, clientKey, caCert, tlsInsecureSkipVerify)
if err != nil {
return nil, fmt.Errorf("query execution failed: %v", err)
}
Expand Down Expand Up @@ -309,7 +368,7 @@ func buildLokiQueryURL(baseURL, query string, start, end int64, limit int) (stri
}

// executeLokiQuery sends the HTTP request to Loki
func executeLokiQuery(ctx context.Context, queryURL string, username, password, token, orgID string) (*LokiResult, error) {
func executeLokiQuery(ctx context.Context, queryURL string, username, password, token, orgID, clientCert, clientKey, caCert string, insecureSkipVerify bool) (*LokiResult, error) {
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
if err != nil {
Expand All @@ -330,10 +389,25 @@ func executeLokiQuery(ctx context.Context, queryURL string, username, password,
req.Header.Add("X-Scope-OrgID", orgID)
}

// Execute request
// Create HTTP client with optional TLS configuration
client := &http.Client{
Timeout: 30 * time.Second,
}

// Configure TLS if mTLS certificates are provided or if we need to skip verification
if (clientCert != "" && clientKey != "") || insecureSkipVerify {
tlsConfig, err := createTLSConfig(clientCert, clientKey, caCert, insecureSkipVerify)
if err != nil {
return nil, fmt.Errorf("failed to create TLS config: %v", err)
}

transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
client.Transport = transport
}

// Execute request
resp, err := client.Do(req)
if err != nil {
return nil, err
Expand Down Expand Up @@ -598,6 +672,15 @@ func HandleLokiLabelNames(ctx context.Context, request mcp.CallToolRequest) (*mc
orgID = os.Getenv(EnvLokiOrgID)
}

// Extract mTLS certificate parameters from environment variables only
clientCert := os.Getenv(EnvLokiClientCert)
clientKey := os.Getenv(EnvLokiClientKey)
caCert := os.Getenv(EnvLokiCACert)

// Extract TLS insecure skip verify from environment variable only
envValue := os.Getenv(EnvLokiTLSInsecureSkipVerify)
tlsInsecureSkipVerify := envValue == "true"

// Set defaults for optional parameters
start := time.Now().Add(-1 * time.Hour).Unix()
end := time.Now().Unix()
Expand Down Expand Up @@ -632,7 +715,7 @@ func HandleLokiLabelNames(ctx context.Context, request mcp.CallToolRequest) (*mc
}

// Execute labels request
result, err := executeLokiLabelsQuery(ctx, labelsURL, username, password, token, orgID)
result, err := executeLokiLabelsQuery(ctx, labelsURL, username, password, token, orgID, clientCert, clientKey, caCert, tlsInsecureSkipVerify)
if err != nil {
return nil, fmt.Errorf("labels query execution failed: %v", err)
}
Expand Down Expand Up @@ -687,6 +770,15 @@ func HandleLokiLabelValues(ctx context.Context, request mcp.CallToolRequest) (*m
orgID = os.Getenv(EnvLokiOrgID)
}

// Extract mTLS certificate parameters from environment variables only
clientCert := os.Getenv(EnvLokiClientCert)
clientKey := os.Getenv(EnvLokiClientKey)
caCert := os.Getenv(EnvLokiCACert)

// Extract TLS insecure skip verify from environment variable only
envValue := os.Getenv(EnvLokiTLSInsecureSkipVerify)
tlsInsecureSkipVerify := envValue == "true"

// Set defaults for optional parameters
start := time.Now().Add(-1 * time.Hour).Unix()
end := time.Now().Unix()
Expand Down Expand Up @@ -721,7 +813,7 @@ func HandleLokiLabelValues(ctx context.Context, request mcp.CallToolRequest) (*m
}

// Execute label values request
result, err := executeLokiLabelValuesQuery(ctx, labelValuesURL, username, password, token, orgID)
result, err := executeLokiLabelValuesQuery(ctx, labelValuesURL, username, password, token, orgID, clientCert, clientKey, caCert, tlsInsecureSkipVerify)
if err != nil {
return nil, fmt.Errorf("label values query execution failed: %v", err)
}
Expand Down Expand Up @@ -796,7 +888,7 @@ func buildLokiLabelValuesURL(baseURL, labelName string, start, end int64) (strin
}

// executeLokiLabelsQuery sends the HTTP request to Loki labels endpoint
func executeLokiLabelsQuery(ctx context.Context, queryURL string, username, password, token, orgID string) (*LokiLabelsResult, error) {
func executeLokiLabelsQuery(ctx context.Context, queryURL string, username, password, token, orgID, clientCert, clientKey, caCert string, insecureSkipVerify bool) (*LokiLabelsResult, error) {
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
if err != nil {
Expand All @@ -815,10 +907,25 @@ func executeLokiLabelsQuery(ctx context.Context, queryURL string, username, pass
req.Header.Add("X-Scope-OrgID", orgID)
}

// Execute request
// Create HTTP client with optional TLS configuration
client := &http.Client{
Timeout: 30 * time.Second,
}

// Configure TLS if mTLS certificates are provided or if we need to skip verification
if (clientCert != "" && clientKey != "") || insecureSkipVerify {
tlsConfig, err := createTLSConfig(clientCert, clientKey, caCert, insecureSkipVerify) // Pass false for insecureSkipVerify
if err != nil {
return nil, fmt.Errorf("failed to create TLS config: %v", err)
}

transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
client.Transport = transport
}

// Execute request
resp, err := client.Do(req)
if err != nil {
return nil, err
Expand Down Expand Up @@ -851,7 +958,7 @@ func executeLokiLabelsQuery(ctx context.Context, queryURL string, username, pass
}

// executeLokiLabelValuesQuery sends the HTTP request to Loki label values endpoint
func executeLokiLabelValuesQuery(ctx context.Context, queryURL string, username, password, token, orgID string) (*LokiLabelValuesResult, error) {
func executeLokiLabelValuesQuery(ctx context.Context, queryURL string, username, password, token, orgID, clientCert, clientKey, caCert string, insecureSkipVerify bool) (*LokiLabelValuesResult, error) {
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
if err != nil {
Expand All @@ -870,10 +977,25 @@ func executeLokiLabelValuesQuery(ctx context.Context, queryURL string, username,
req.Header.Add("X-Scope-OrgID", orgID)
}

// Execute request
// Create HTTP client with optional TLS configuration
client := &http.Client{
Timeout: 30 * time.Second,
}

// Configure TLS if mTLS certificates are provided
if (clientCert != "" && clientKey != "") || insecureSkipVerify {
tlsConfig, err := createTLSConfig(clientCert, clientKey, caCert, insecureSkipVerify) // Pass false for insecureSkipVerify
if err != nil {
return nil, fmt.Errorf("failed to create TLS config: %v", err)
}

transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
client.Transport = transport
}

// Execute request
resp, err := client.Do(req)
if err != nil {
return nil, err
Expand Down
12 changes: 6 additions & 6 deletions internal/handlers/loki_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestFormatLokiResults_TimestampParsing(t *testing.T) {
}

// Format the results
output, err := formatLokiResults(result)
output, err := formatLokiResults(result, "raw")
if err != nil {
t.Fatalf("formatLokiResults failed: %v", err)
}
Expand Down Expand Up @@ -83,7 +83,7 @@ func TestFormatLokiResults_MultipleTimestamps(t *testing.T) {
},
}

output, err := formatLokiResults(result)
output, err := formatLokiResults(result, "raw")
if err != nil {
t.Fatalf("formatLokiResults failed: %v", err)
}
Expand Down Expand Up @@ -119,7 +119,7 @@ func TestFormatLokiResults_InvalidTimestamp(t *testing.T) {
},
}

output, err := formatLokiResults(result)
output, err := formatLokiResults(result, "text")
if err != nil {
t.Fatalf("formatLokiResults failed: %v", err)
}
Expand All @@ -145,7 +145,7 @@ func TestFormatLokiResults_EmptyResult(t *testing.T) {
},
}

output, err := formatLokiResults(result)
output, err := formatLokiResults(result, "raw")
if err != nil {
t.Fatalf("formatLokiResults failed: %v", err)
}
Expand Down Expand Up @@ -180,7 +180,7 @@ func TestFormatLokiResults_RecentTimestamp(t *testing.T) {
},
}

output, err := formatLokiResults(result)
output, err := formatLokiResults(result, "raw")
if err != nil {
t.Fatalf("formatLokiResults failed: %v", err)
}
Expand Down Expand Up @@ -241,7 +241,7 @@ func TestFormatLokiResults_NoYear2262Bug(t *testing.T) {
},
}

output, err := formatLokiResults(result)
output, err := formatLokiResults(result, "raw")
if err != nil {
t.Fatalf("formatLokiResults failed: %v", err)
}
Expand Down