Skip to content

Commit 4c3afcf

Browse files
dewjamJim DeWaard
authored andcommitted
[FEAT] Add support for mTLS to loki backend
1 parent c814856 commit 4c3afcf

File tree

3 files changed

+177
-17
lines changed

3 files changed

+177
-17
lines changed

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,24 @@ The Loki query tool supports the following environment variables:
7373
- `LOKI_PASSWORD`: Default password for basic authentication if not specified in the request
7474
- `LOKI_TOKEN`: Default bearer token for authentication if not specified in the request
7575

76-
**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.
76+
##### mTLS Authentication
77+
78+
For environments requiring mutual TLS (mTLS) authentication:
79+
80+
- `LOKI_CLIENT_CERT`: Path to client certificate file for mTLS authentication
81+
- `LOKI_CLIENT_KEY`: Path to client key file for mTLS authentication
82+
- `LOKI_CA_CERT`: Path to CA certificate file for mTLS authentication
83+
84+
##### TLS Configuration
85+
86+
For TLS connection configuration:
87+
88+
- `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.**
89+
90+
**Security Notes**:
91+
- 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.
92+
- mTLS certificates should be properly secured and rotated regularly.
93+
- 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.
7794

7895
### Testing the MCP Server
7996

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

127+
# Using environment variables with mTLS authentication:
128+
export LOKI_URL="https://secure-loki.example.com"
129+
export LOKI_CLIENT_CERT="/path/to/client.crt"
130+
export LOKI_CLIENT_KEY="/path/to/client.key"
131+
export LOKI_CA_CERT="/path/to/ca.crt"
132+
./loki-mcp-client loki_query "{job=\"varlogs\"}"
133+
134+
# Using environment variables with TLS skip verify (development only):
135+
export LOKI_URL="https://dev-loki.example.com"
136+
export LOKI_TLS_INSECURE_SKIP_VERIFY="true"
137+
./loki-mcp-client loki_query "{job=\"varlogs\"}"
138+
110139
# Using all environment variables together:
111140
export LOKI_URL="http://localhost:3100"
112141
export LOKI_ORG_ID="tenant-123"
@@ -302,7 +331,11 @@ Or create your own configuration:
302331
"LOKI_ORG_ID": "your-default-org-id",
303332
"LOKI_USERNAME": "your-username",
304333
"LOKI_PASSWORD": "your-password",
305-
"LOKI_TOKEN": "your-bearer-token"
334+
"LOKI_TOKEN": "your-bearer-token",
335+
"LOKI_CLIENT_CERT": "/path/to/client.crt",
336+
"LOKI_CLIENT_KEY": "/path/to/client.key",
337+
"LOKI_CA_CERT": "/path/to/ca.crt",
338+
"LOKI_TLS_INSECURE_SKIP_VERIFY": "false"
306339
},
307340
"disabled": false,
308341
"autoApprove": ["loki_query"]
@@ -372,6 +405,11 @@ Docker configuration:
372405
"-e", "LOKI_USERNAME=your-username",
373406
"-e", "LOKI_PASSWORD=your-password",
374407
"-e", "LOKI_TOKEN=your-bearer-token",
408+
"-e", "LOKI_CLIENT_CERT=/certs/client.crt",
409+
"-e", "LOKI_CLIENT_KEY=/certs/client.key",
410+
"-e", "LOKI_CA_CERT=/certs/ca.crt",
411+
"-e", "LOKI_TLS_INSECURE_SKIP_VERIFY=false",
412+
"-v", "/path/to/certs:/certs:ro",
375413
"loki-mcp-server:latest"]
376414
}
377415
}

internal/handlers/loki.go

Lines changed: 131 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package handlers
22

33
import (
44
"context"
5+
"crypto/tls"
6+
"crypto/x509"
57
"encoding/json"
68
"fmt"
79
"io"
@@ -57,6 +59,18 @@ const EnvLokiPassword = "LOKI_PASSWORD"
5759
// Environment variable name for Loki Token
5860
const EnvLokiToken = "LOKI_TOKEN"
5961

62+
// Environment variable name for Loki client certificate file
63+
const EnvLokiClientCert = "LOKI_CLIENT_CERT"
64+
65+
// Environment variable name for Loki client key file
66+
const EnvLokiClientKey = "LOKI_CLIENT_KEY"
67+
68+
// Environment variable name for Loki CA certificate file
69+
const EnvLokiCACert = "LOKI_CA_CERT"
70+
71+
// Environment variable name for TLS insecure skip verify
72+
const EnvLokiTLSInsecureSkipVerify = "LOKI_TLS_INSECURE_SKIP_VERIFY"
73+
6074
// Default Loki URL when environment variable is not set
6175
const DefaultLokiURL = "http://localhost:3100"
6276

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

91+
// createTLSConfig creates a TLS configuration for mTLS authentication
92+
func createTLSConfig(clientCertFile, clientKeyFile, caCertFile string, insecureSkipVerify bool) (*tls.Config, error) {
93+
tlsConfig := &tls.Config{
94+
InsecureSkipVerify: insecureSkipVerify,
95+
}
96+
97+
// Load client certificate and key if provided
98+
if clientCertFile != "" && clientKeyFile != "" {
99+
cert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to load client certificate: %v", err)
102+
}
103+
tlsConfig.Certificates = []tls.Certificate{cert}
104+
}
105+
106+
// Load CA certificate if provided
107+
if caCertFile != "" {
108+
// Read the CA certificate file
109+
caCert, err := os.ReadFile(caCertFile)
110+
if err != nil {
111+
return nil, fmt.Errorf("failed to read CA certificate file: %v", err)
112+
}
113+
114+
// Create a certificate pool and add the CA certificate
115+
caCertPool := x509.NewCertPool()
116+
if !caCertPool.AppendCertsFromPEM(caCert) {
117+
return nil, fmt.Errorf("failed to parse CA certificate")
118+
}
119+
120+
// Set the CA certificate pool in the TLS configuration
121+
tlsConfig.RootCAs = caCertPool
122+
}
123+
124+
return tlsConfig, nil
125+
}
126+
77127
// NewLokiQueryTool creates and returns a tool for querying Grafana Loki
78128
func NewLokiQueryTool() mcp.Tool {
79129
// Get Loki URL from environment variable or use default
@@ -173,6 +223,15 @@ func HandleLokiQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal
173223
orgID = os.Getenv(EnvLokiOrgID)
174224
}
175225

226+
// Extract mTLS certificate parameters from environment variables only
227+
clientCert := os.Getenv(EnvLokiClientCert)
228+
clientKey := os.Getenv(EnvLokiClientKey)
229+
caCert := os.Getenv(EnvLokiCACert)
230+
231+
// Extract TLS insecure skip verify from environment variable only
232+
envValue := os.Getenv(EnvLokiTLSInsecureSkipVerify)
233+
tlsInsecureSkipVerify := envValue == "true"
234+
176235
// Set defaults for optional parameters
177236
start := time.Now().Add(-1 * time.Hour).Unix()
178237
end := time.Now().Unix()
@@ -212,7 +271,7 @@ func HandleLokiQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.Cal
212271
}
213272

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

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

333-
// Execute request
392+
// Create HTTP client with optional TLS configuration
334393
client := &http.Client{
335394
Timeout: 30 * time.Second,
336395
}
396+
397+
// Configure TLS if mTLS certificates are provided or if we need to skip verification
398+
if (clientCert != "" && clientKey != "") || insecureSkipVerify {
399+
tlsConfig, err := createTLSConfig(clientCert, clientKey, caCert, insecureSkipVerify)
400+
if err != nil {
401+
return nil, fmt.Errorf("failed to create TLS config: %v", err)
402+
}
403+
404+
transport := &http.Transport{
405+
TLSClientConfig: tlsConfig,
406+
}
407+
client.Transport = transport
408+
}
409+
410+
// Execute request
337411
resp, err := client.Do(req)
338412
if err != nil {
339413
return nil, err
@@ -598,6 +672,15 @@ func HandleLokiLabelNames(ctx context.Context, request mcp.CallToolRequest) (*mc
598672
orgID = os.Getenv(EnvLokiOrgID)
599673
}
600674

675+
// Extract mTLS certificate parameters from environment variables only
676+
clientCert := os.Getenv(EnvLokiClientCert)
677+
clientKey := os.Getenv(EnvLokiClientKey)
678+
caCert := os.Getenv(EnvLokiCACert)
679+
680+
// Extract TLS insecure skip verify from environment variable only
681+
envValue := os.Getenv(EnvLokiTLSInsecureSkipVerify)
682+
tlsInsecureSkipVerify := envValue == "true"
683+
601684
// Set defaults for optional parameters
602685
start := time.Now().Add(-1 * time.Hour).Unix()
603686
end := time.Now().Unix()
@@ -632,7 +715,7 @@ func HandleLokiLabelNames(ctx context.Context, request mcp.CallToolRequest) (*mc
632715
}
633716

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

773+
// Extract mTLS certificate parameters from environment variables only
774+
clientCert := os.Getenv(EnvLokiClientCert)
775+
clientKey := os.Getenv(EnvLokiClientKey)
776+
caCert := os.Getenv(EnvLokiCACert)
777+
778+
// Extract TLS insecure skip verify from environment variable only
779+
envValue := os.Getenv(EnvLokiTLSInsecureSkipVerify)
780+
tlsInsecureSkipVerify := envValue == "true"
781+
690782
// Set defaults for optional parameters
691783
start := time.Now().Add(-1 * time.Hour).Unix()
692784
end := time.Now().Unix()
@@ -721,7 +813,7 @@ func HandleLokiLabelValues(ctx context.Context, request mcp.CallToolRequest) (*m
721813
}
722814

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

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

818-
// Execute request
910+
// Create HTTP client with optional TLS configuration
819911
client := &http.Client{
820912
Timeout: 30 * time.Second,
821913
}
914+
915+
// Configure TLS if mTLS certificates are provided or if we need to skip verification
916+
if (clientCert != "" && clientKey != "") || insecureSkipVerify {
917+
tlsConfig, err := createTLSConfig(clientCert, clientKey, caCert, insecureSkipVerify) // Pass false for insecureSkipVerify
918+
if err != nil {
919+
return nil, fmt.Errorf("failed to create TLS config: %v", err)
920+
}
921+
922+
transport := &http.Transport{
923+
TLSClientConfig: tlsConfig,
924+
}
925+
client.Transport = transport
926+
}
927+
928+
// Execute request
822929
resp, err := client.Do(req)
823930
if err != nil {
824931
return nil, err
@@ -851,7 +958,7 @@ func executeLokiLabelsQuery(ctx context.Context, queryURL string, username, pass
851958
}
852959

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

873-
// Execute request
980+
// Create HTTP client with optional TLS configuration
874981
client := &http.Client{
875982
Timeout: 30 * time.Second,
876983
}
984+
985+
// Configure TLS if mTLS certificates are provided
986+
if (clientCert != "" && clientKey != "") || insecureSkipVerify {
987+
tlsConfig, err := createTLSConfig(clientCert, clientKey, caCert, insecureSkipVerify) // Pass false for insecureSkipVerify
988+
if err != nil {
989+
return nil, fmt.Errorf("failed to create TLS config: %v", err)
990+
}
991+
992+
transport := &http.Transport{
993+
TLSClientConfig: tlsConfig,
994+
}
995+
client.Transport = transport
996+
}
997+
998+
// Execute request
877999
resp, err := client.Do(req)
8781000
if err != nil {
8791001
return nil, err

internal/handlers/loki_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestFormatLokiResults_TimestampParsing(t *testing.T) {
3535
}
3636

3737
// Format the results
38-
output, err := formatLokiResults(result)
38+
output, err := formatLokiResults(result, "raw")
3939
if err != nil {
4040
t.Fatalf("formatLokiResults failed: %v", err)
4141
}
@@ -83,7 +83,7 @@ func TestFormatLokiResults_MultipleTimestamps(t *testing.T) {
8383
},
8484
}
8585

86-
output, err := formatLokiResults(result)
86+
output, err := formatLokiResults(result, "raw")
8787
if err != nil {
8888
t.Fatalf("formatLokiResults failed: %v", err)
8989
}
@@ -119,7 +119,7 @@ func TestFormatLokiResults_InvalidTimestamp(t *testing.T) {
119119
},
120120
}
121121

122-
output, err := formatLokiResults(result)
122+
output, err := formatLokiResults(result, "text")
123123
if err != nil {
124124
t.Fatalf("formatLokiResults failed: %v", err)
125125
}
@@ -145,7 +145,7 @@ func TestFormatLokiResults_EmptyResult(t *testing.T) {
145145
},
146146
}
147147

148-
output, err := formatLokiResults(result)
148+
output, err := formatLokiResults(result, "raw")
149149
if err != nil {
150150
t.Fatalf("formatLokiResults failed: %v", err)
151151
}
@@ -180,7 +180,7 @@ func TestFormatLokiResults_RecentTimestamp(t *testing.T) {
180180
},
181181
}
182182

183-
output, err := formatLokiResults(result)
183+
output, err := formatLokiResults(result, "raw")
184184
if err != nil {
185185
t.Fatalf("formatLokiResults failed: %v", err)
186186
}
@@ -241,7 +241,7 @@ func TestFormatLokiResults_NoYear2262Bug(t *testing.T) {
241241
},
242242
}
243243

244-
output, err := formatLokiResults(result)
244+
output, err := formatLokiResults(result, "raw")
245245
if err != nil {
246246
t.Fatalf("formatLokiResults failed: %v", err)
247247
}

0 commit comments

Comments
 (0)