-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathhandler.go
More file actions
204 lines (176 loc) · 6.7 KB
/
handler.go
File metadata and controls
204 lines (176 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package centralproxy
import (
"crypto/x509"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"sync/atomic"
"time"
"github.com/pkg/errors"
"github.com/stackrox/rox/pkg/centralsensor"
pkghttputil "github.com/stackrox/rox/pkg/httputil"
"github.com/stackrox/rox/pkg/k8sutil"
"github.com/stackrox/rox/pkg/logging"
"github.com/stackrox/rox/pkg/retryablehttp"
"github.com/stackrox/rox/pkg/urlfmt"
"github.com/stackrox/rox/sensor/common"
"github.com/stackrox/rox/sensor/common/centralcaps"
"github.com/stackrox/rox/sensor/common/centralproxy/allowedpaths"
"google.golang.org/grpc"
"k8s.io/client-go/kubernetes"
)
var (
log = logging.LoggerForModule()
k8sClientQPS = 50.0
k8sClientBurst = 100
_ common.Notifiable = (*Handler)(nil)
_ common.CentralGRPCConnAware = (*Handler)(nil)
)
// proxyErrorHandler is the error handler for the reverse proxy.
// It returns 503 for service unavailable errors and 500 for other errors.
func proxyErrorHandler(w http.ResponseWriter, _ *http.Request, err error) {
log.Errorf("Proxy error: %v", err)
if errors.Is(err, errServiceUnavailable) {
http.Error(w, fmt.Sprintf("proxy temporarily unavailable: %v", err), http.StatusServiceUnavailable)
return
}
http.Error(w, fmt.Sprintf("failed to contact central: %v", err), http.StatusInternalServerError)
}
// Handler handles HTTP proxy requests to Central.
type Handler struct {
centralReachable atomic.Bool
clusterIDGetter clusterIDGetter
authorizer *k8sAuthorizer
transport *scopedTokenTransport
proxy *httputil.ReverseProxy
}
// NewProxyHandler creates a new proxy handler that forwards requests to Central.
func NewProxyHandler(centralEndpoint string, centralCertificates []*x509.Certificate, clusterIDGetter clusterIDGetter) (*Handler, error) {
centralBaseURL, err := url.Parse(
urlfmt.FormatURL(centralEndpoint, urlfmt.HTTPS, urlfmt.NoTrailingSlash),
)
if err != nil {
return nil, errors.Wrap(err, "parsing endpoint")
}
baseTransport, err := createBaseTransport(centralBaseURL, centralCertificates)
if err != nil {
return nil, errors.Wrap(err, "creating base transport")
}
transport := newScopedTokenTransport(baseTransport, clusterIDGetter)
proxy := &httputil.ReverseProxy{
Transport: transport,
Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(centralBaseURL) },
ErrorHandler: proxyErrorHandler,
}
restConfig, err := k8sutil.GetK8sInClusterConfig()
if err != nil {
return nil, errors.Wrap(err, "getting in-cluster config")
}
// Set QPS and Burst to avoid client-side throttling.
// The default k8s client-go values (QPS=5, Burst=10) are quite conservative
// and can cause "client-side throttling, not priority and fairness" warnings.
restConfig.QPS = float32(k8sClientQPS)
restConfig.Burst = k8sClientBurst
retryablehttp.ConfigureRESTConfig(restConfig)
k8sClient, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, errors.Wrap(err, "creating kubernetes client")
}
return &Handler{
clusterIDGetter: clusterIDGetter,
authorizer: newK8sAuthorizer(k8sClient),
transport: transport,
proxy: proxy,
}, nil
}
// SetCentralGRPCClient implements common.CentralGRPCConnAware.
// It sets the gRPC connection used by the token provider to request tokens from Central.
func (h *Handler) SetCentralGRPCClient(cc grpc.ClientConnInterface) {
h.transport.SetClient(cc)
}
// Notify reacts to sensor going into online/offline mode.
func (h *Handler) Notify(e common.SensorComponentEvent) {
log.Info(common.LogSensorComponentEvent(e, "Central proxy handler"))
switch e {
case common.SensorComponentEventCentralReachable:
h.centralReachable.Store(true)
case common.SensorComponentEventOfflineMode:
h.centralReachable.Store(false)
}
}
// validateRequest validates the incoming request and returns an error if validation fails.
func (h *Handler) validateRequest(request *http.Request) error {
// Allow GET, POST, OPTIONS (for CORS preflight), and HEAD.
switch request.Method {
case http.MethodGet, http.MethodPost, http.MethodOptions, http.MethodHead:
// allowed
default:
return pkghttputil.Errorf(http.StatusMethodNotAllowed, "method %s not allowed", request.Method)
}
if !h.centralReachable.Load() {
return pkghttputil.NewError(http.StatusServiceUnavailable, "central not reachable")
}
// Only enforce path filtering when Central advertises the capability.
// Otherwise allow the request for backwards compatibility.
if centralcaps.Has(centralsensor.CentralProxyPathFiltering) {
normalizedPath := request.URL.EscapedPath()
if normalizedPath == "" {
normalizedPath = "/"
}
if !allowedpaths.IsAllowed(normalizedPath) {
return pkghttputil.Errorf(http.StatusForbidden,
"path %q is not allowed by the proxy allow-list", request.URL.Path)
}
}
return nil
}
// checkInternalTokenAPISupport checks if Central supports the internal token API capability.
// The proxy requires this capability to function; all requests are rejected if unsupported.
func checkInternalTokenAPISupport() error {
if !centralcaps.Has(centralsensor.InternalTokenAPISupported) {
return pkghttputil.NewError(http.StatusNotImplemented,
"proxy to Central is not available; Central does not support the internal token API required by this proxy")
}
return nil
}
// ServeHTTP handles incoming HTTP requests and proxies them to Central.
func (h *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
start := time.Now()
result := requestResultSuccess
defer func() {
observeProxyRequest(result, time.Since(start))
}()
if err := checkInternalTokenAPISupport(); err != nil {
result = requestResultNotImplemented
http.Error(writer, err.Error(), pkghttputil.StatusFromError(err))
return
}
if err := h.validateRequest(request); err != nil {
result = requestResultValidationError
http.Error(writer, err.Error(), pkghttputil.StatusFromError(err))
return
}
if h.authorizer == nil {
result = requestResultConfigError
log.Error("Authorizer is nil - this indicates a misconfiguration in the central proxy handler")
http.Error(writer, "authorizer not configured", http.StatusInternalServerError)
return
}
userInfo, err := h.authorizer.authenticate(request.Context(), request)
if err != nil {
result = requestResultAuthnError
http.Error(writer, err.Error(), pkghttputil.StatusFromError(err))
return
}
if err := h.authorizer.authorize(request.Context(), userInfo, request); err != nil {
result = requestResultAuthzError
http.Error(writer, err.Error(), pkghttputil.StatusFromError(err))
return
}
tracker := pkghttputil.NewStatusTrackingWriter(writer)
h.proxy.ServeHTTP(tracker, request)
if statusCode := tracker.GetStatusCode(); statusCode != nil && *statusCode >= http.StatusBadRequest {
result = requestResultProxyError
}
}