mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Allow reverse tunnel join without exposing the web API (#13598)
This change allows agents to join over a reverse tunnel (port 3024 by default) only, instead of also requiring access to the web API (port 3080).
This commit is contained in:
@@ -596,7 +596,7 @@ func (i *TeleInstance) StartNode(tconf *service.Config) (*service.TeleportProces
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return i.startNode(tconf, port)
|
||||
return i.StartNodeWithTargetPort(tconf, port)
|
||||
}
|
||||
|
||||
// StartReverseTunnelNode starts a SSH node and connects it to the cluster via reverse tunnel.
|
||||
@@ -605,11 +605,11 @@ func (i *TeleInstance) StartReverseTunnelNode(tconf *service.Config) (*service.T
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return i.startNode(tconf, port)
|
||||
return i.StartNodeWithTargetPort(tconf, port)
|
||||
}
|
||||
|
||||
// startNode starts a node and connects it to the cluster.
|
||||
func (i *TeleInstance) startNode(tconf *service.Config, authPort string) (*service.TeleportProcess, error) {
|
||||
// StartNodeWithTargetPort starts a node and connects it to the cluster via a specified port.
|
||||
func (i *TeleInstance) StartNodeWithTargetPort(tconf *service.Config, authPort string) (*service.TeleportProcess, error) {
|
||||
dataDir, err := os.MkdirTemp("", "cluster-"+i.Secrets.SiteName)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -156,6 +156,7 @@ func TestIntegrations(t *testing.T) {
|
||||
t.Run("ListResourcesAcrossClusters", suite.bind(testListResourcesAcrossClusters))
|
||||
t.Run("SessionRecordingModes", suite.bind(testSessionRecordingModes))
|
||||
t.Run("DifferentPinnedIP", suite.bind(testDifferentPinnedIP))
|
||||
t.Run("JoinOverReverseTunnelOnly", suite.bind(testJoinOverReverseTunnelOnly))
|
||||
t.Run("SFTP", suite.bind(testSFTP))
|
||||
t.Run("EscapeSequenceTriggers", suite.bind(testEscapeSequenceTriggers))
|
||||
t.Run("AuthLocalNodeControlStream", suite.bind(testAuthLocalNodeControlStream))
|
||||
@@ -6615,6 +6616,36 @@ func testListResourcesAcrossClusters(t *testing.T, suite *integrationTestSuite)
|
||||
}
|
||||
}
|
||||
|
||||
func testJoinOverReverseTunnelOnly(t *testing.T, suite *integrationTestSuite) {
|
||||
lib.SetInsecureDevMode(true)
|
||||
defer lib.SetInsecureDevMode(false)
|
||||
|
||||
// Create a Teleport instance with Auth/Proxy.
|
||||
mainConfig := suite.defaultServiceConfig()
|
||||
mainConfig.Auth.Enabled = true
|
||||
|
||||
mainConfig.Proxy.Enabled = true
|
||||
mainConfig.Proxy.DisableWebService = false
|
||||
mainConfig.Proxy.DisableWebInterface = true
|
||||
|
||||
mainConfig.SSH.Enabled = false
|
||||
|
||||
main := suite.NewTeleportWithConfig(t, nil, nil, mainConfig)
|
||||
t.Cleanup(func() { require.NoError(t, main.StopAll()) })
|
||||
|
||||
// Create a Teleport instance with a Node.
|
||||
nodeConfig := suite.defaultServiceConfig()
|
||||
nodeConfig.Hostname = Host
|
||||
nodeConfig.SetToken("token")
|
||||
|
||||
nodeConfig.Auth.Enabled = false
|
||||
nodeConfig.Proxy.Enabled = false
|
||||
nodeConfig.SSH.Enabled = true
|
||||
|
||||
_, err := main.StartNodeWithTargetPort(nodeConfig, helpers.PortStr(t, main.ReverseTunnel))
|
||||
require.NoError(t, err, "Node failed to join over reverse tunnel")
|
||||
}
|
||||
|
||||
func testSFTP(t *testing.T, suite *integrationTestSuite) {
|
||||
// Create Teleport instance.
|
||||
teleport := suite.newTeleport(t, nil, true)
|
||||
|
||||
@@ -366,22 +366,23 @@ var (
|
||||
postgresCancelRequest = []byte{0x0, 0x0, 0x0, 0x10, 0x4, 0xd2, 0x16, 0x2e}
|
||||
)
|
||||
|
||||
var httpMethods = [...][]byte{
|
||||
[]byte("GET"),
|
||||
[]byte("POST"),
|
||||
[]byte("PUT"),
|
||||
[]byte("DELETE"),
|
||||
[]byte("HEAD"),
|
||||
[]byte("CONNECT"),
|
||||
[]byte("OPTIONS"),
|
||||
[]byte("TRACE"),
|
||||
[]byte("PATCH"),
|
||||
}
|
||||
|
||||
// isHTTP returns true if the first few bytes of the prefix indicate
|
||||
// the use of an HTTP method.
|
||||
func isHTTP(in []byte) bool {
|
||||
methods := [...][]byte{
|
||||
[]byte("GET"),
|
||||
[]byte("POST"),
|
||||
[]byte("PUT"),
|
||||
[]byte("DELETE"),
|
||||
[]byte("HEAD"),
|
||||
[]byte("CONNECT"),
|
||||
[]byte("OPTIONS"),
|
||||
[]byte("TRACE"),
|
||||
[]byte("PATCH"),
|
||||
}
|
||||
for _, verb := range methods {
|
||||
if bytes.HasPrefix(verb, in) {
|
||||
for _, verb := range httpMethods {
|
||||
if bytes.HasPrefix(in, verb) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,3 +868,25 @@ func (noopListener) Close() error {
|
||||
func (l noopListener) Addr() net.Addr {
|
||||
return l.addr
|
||||
}
|
||||
|
||||
func TestIsHTTP(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, verb := range httpMethods {
|
||||
t.Run(fmt.Sprintf("Accept %v", string(verb)), func(t *testing.T) {
|
||||
data := fmt.Sprintf("%v /some/path HTTP/1.1", string(verb))
|
||||
require.True(t, isHTTP([]byte(data)))
|
||||
})
|
||||
}
|
||||
|
||||
rejectedInputs := []string{
|
||||
"some random junk",
|
||||
"FAKE /some/path HTTP/1.1",
|
||||
// This case checks for a bug where the arguments to bytes.HasPrefix are reversed.
|
||||
"GE",
|
||||
}
|
||||
for _, input := range rejectedInputs {
|
||||
t.Run(fmt.Sprintf("Reject %q", input), func(t *testing.T) {
|
||||
require.False(t, isHTTP([]byte(input)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,8 +675,17 @@ func (c *agentPoolRuntimeConfig) updateRemote(ctx context.Context, addr *utils.N
|
||||
if ok := errors.As(err, &tls.RecordHeaderError{}); !ok {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
tlsRoutingEnabled = ping.Proxy.TLSRoutingEnabled
|
||||
}
|
||||
|
||||
if ping != nil {
|
||||
// Only use the ping results if they weren't from a minimal handler.
|
||||
// The minimal API handler only exists when the proxy and reverse tunnel are
|
||||
// listening on separate ports, so it will never do TLS routing.
|
||||
isMinimalHandler := addr.Addr == ping.Proxy.SSH.TunnelListenAddr &&
|
||||
ping.Proxy.SSH.TunnelListenAddr != ping.Proxy.SSH.WebListenAddr
|
||||
if !isMinimalHandler {
|
||||
tlsRoutingEnabled = ping.Proxy.TLSRoutingEnabled
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
|
||||
+154
-35
@@ -2869,16 +2869,18 @@ func (process *TeleportProcess) initProxy() error {
|
||||
}
|
||||
|
||||
type proxyListeners struct {
|
||||
mux *multiplexer.Mux
|
||||
tls *multiplexer.WebListener
|
||||
ssh net.Listener
|
||||
web net.Listener
|
||||
reverseTunnel net.Listener
|
||||
kube net.Listener
|
||||
db dbListeners
|
||||
alpn net.Listener
|
||||
proxy net.Listener
|
||||
grpc net.Listener
|
||||
mux *multiplexer.Mux
|
||||
tls *multiplexer.WebListener
|
||||
ssh net.Listener
|
||||
web net.Listener
|
||||
reverseTunnel net.Listener
|
||||
kube net.Listener
|
||||
db dbListeners
|
||||
alpn net.Listener
|
||||
proxy net.Listener
|
||||
grpc net.Listener
|
||||
reverseTunnelMux *multiplexer.Mux
|
||||
minimalTLS *multiplexer.WebListener
|
||||
}
|
||||
|
||||
// dbListeners groups database access listeners.
|
||||
@@ -2940,6 +2942,12 @@ func (l *proxyListeners) Close() {
|
||||
if l.proxy != nil {
|
||||
l.proxy.Close()
|
||||
}
|
||||
if l.reverseTunnelMux != nil {
|
||||
l.reverseTunnelMux.Close()
|
||||
}
|
||||
if l.minimalTLS != nil {
|
||||
l.minimalTLS.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// setupProxyListeners sets up web proxy listeners based on the configuration
|
||||
@@ -3105,10 +3113,17 @@ func (process *TeleportProcess) setupProxyListeners(networkingConfig types.Clust
|
||||
default:
|
||||
process.log.Debug("Setup Proxy: Proxy and reverse tunnel are listening on separate ports.")
|
||||
if !cfg.Proxy.DisableReverseTunnel && !cfg.Proxy.ReverseTunnelListenAddr.IsEmpty() {
|
||||
listeners.reverseTunnel, err = process.importOrCreateListener(ListenerProxyTunnel, cfg.Proxy.ReverseTunnelListenAddr.Addr)
|
||||
if err != nil {
|
||||
listeners.Close()
|
||||
return nil, trace.Wrap(err)
|
||||
if cfg.Proxy.DisableWebService {
|
||||
listeners.reverseTunnel, err = process.importOrCreateListener(ListenerProxyTunnel, cfg.Proxy.ReverseTunnelListenAddr.Addr)
|
||||
if err != nil {
|
||||
listeners.Close()
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
if err := process.initMinimalReverseTunnelListener(cfg, &listeners); err != nil {
|
||||
listeners.Close()
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cfg.Proxy.DisableWebService && !cfg.Proxy.WebAddr.IsEmpty() {
|
||||
@@ -3156,6 +3171,31 @@ func (process *TeleportProcess) setupProxyListeners(networkingConfig types.Clust
|
||||
}
|
||||
}
|
||||
|
||||
// initMinimalReverseTunnelListener starts a listener over a reverse tunnel that multiplexes a minimal subset of the
|
||||
// web API.
|
||||
func (process *TeleportProcess) initMinimalReverseTunnelListener(cfg *Config, listeners *proxyListeners) error {
|
||||
listener, err := process.importOrCreateListener(ListenerProxyTunnel, cfg.Proxy.ReverseTunnelListenAddr.Addr)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
listeners.reverseTunnelMux, err = multiplexer.New(multiplexer.Config{
|
||||
EnableProxyProtocol: cfg.Proxy.EnableProxyProtocol,
|
||||
Listener: listener,
|
||||
ID: teleport.Component(teleport.ComponentProxy, "tunnel", "web", process.id),
|
||||
})
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
listeners.reverseTunnel = listeners.reverseTunnelMux.SSH()
|
||||
go func() {
|
||||
if err := listeners.reverseTunnelMux.Serve(); err != nil {
|
||||
process.log.WithError(err).Debug("Minimal reverse tunnel mux exited with error")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// muxPostgresOnWebPort starts Postgres proxy listener multiplexed on Teleport Proxy web port,
|
||||
// unless postgres_listen_addr was specified.
|
||||
func (process *TeleportProcess) muxPostgresOnWebPort(cfg *Config, listeners *proxyListeners) {
|
||||
@@ -3357,6 +3397,9 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
// Register web proxy server
|
||||
var webServer *http.Server
|
||||
var webHandler *web.APIHandler
|
||||
var minimalWebServer *http.Server
|
||||
var minimalWebHandler *web.APIHandler
|
||||
|
||||
if !process.Config.Proxy.DisableWebService {
|
||||
var fs http.FileSystem
|
||||
if !process.Config.Proxy.DisableWebInterface {
|
||||
@@ -3372,27 +3415,27 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
accessPoint: accessPoint,
|
||||
}
|
||||
|
||||
webHandler, err = web.NewHandler(
|
||||
web.Config{
|
||||
Proxy: tsrv,
|
||||
AuthServers: cfg.AuthServers[0],
|
||||
DomainName: cfg.Hostname,
|
||||
ProxyClient: conn.Client,
|
||||
ProxySSHAddr: proxySSHAddr,
|
||||
ProxyWebAddr: cfg.Proxy.WebAddr,
|
||||
ProxyPublicAddrs: cfg.Proxy.PublicAddrs,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
FIPS: cfg.FIPS,
|
||||
AccessPoint: accessPoint,
|
||||
Emitter: streamEmitter,
|
||||
PluginRegistry: process.PluginRegistry,
|
||||
HostUUID: process.Config.HostUUID,
|
||||
Context: process.ExitContext(),
|
||||
StaticFS: fs,
|
||||
ClusterFeatures: process.getClusterFeatures(),
|
||||
ProxySettings: proxySettings,
|
||||
PublicProxyAddr: process.proxyPublicAddr().Addr,
|
||||
})
|
||||
webConfig := web.Config{
|
||||
Proxy: tsrv,
|
||||
AuthServers: cfg.AuthServers[0],
|
||||
DomainName: cfg.Hostname,
|
||||
ProxyClient: conn.Client,
|
||||
ProxySSHAddr: proxySSHAddr,
|
||||
ProxyWebAddr: cfg.Proxy.WebAddr,
|
||||
ProxyPublicAddrs: cfg.Proxy.PublicAddrs,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
FIPS: cfg.FIPS,
|
||||
AccessPoint: accessPoint,
|
||||
Emitter: streamEmitter,
|
||||
PluginRegistry: process.PluginRegistry,
|
||||
HostUUID: process.Config.HostUUID,
|
||||
Context: process.ExitContext(),
|
||||
StaticFS: fs,
|
||||
ClusterFeatures: process.getClusterFeatures(),
|
||||
ProxySettings: proxySettings,
|
||||
PublicProxyAddr: process.proxyPublicAddr().Addr,
|
||||
}
|
||||
webHandler, err = web.NewHandler(webConfig)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
@@ -3434,6 +3477,12 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
log.Info("Exited.")
|
||||
return nil
|
||||
})
|
||||
|
||||
if listeners.reverseTunnelMux != nil {
|
||||
if minimalWebServer, minimalWebHandler, err = process.initMinimalReverseTunnel(listeners, tlsConfigWeb, cfg, webConfig, log); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Info("Web UI is disabled.")
|
||||
}
|
||||
@@ -3783,6 +3832,12 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
if webHandler != nil {
|
||||
warnOnErr(webHandler.Close(), log)
|
||||
}
|
||||
if minimalWebServer != nil {
|
||||
warnOnErr(minimalWebServer.Close(), log)
|
||||
}
|
||||
if minimalWebHandler != nil {
|
||||
warnOnErr(minimalWebHandler.Close(), log)
|
||||
}
|
||||
warnOnErr(sshProxy.Close(), log)
|
||||
if kubeServer != nil {
|
||||
warnOnErr(kubeServer.Close(), log)
|
||||
@@ -3812,12 +3867,18 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
if webServer != nil {
|
||||
warnOnErr(webServer.Shutdown(ctx), log)
|
||||
}
|
||||
if minimalWebServer != nil {
|
||||
warnOnErr(minimalWebServer.Shutdown(ctx), log)
|
||||
}
|
||||
if kubeServer != nil {
|
||||
warnOnErr(kubeServer.Shutdown(ctx), log)
|
||||
}
|
||||
if webHandler != nil {
|
||||
warnOnErr(webHandler.Close(), log)
|
||||
}
|
||||
if minimalWebHandler != nil {
|
||||
warnOnErr(minimalWebHandler.Close(), log)
|
||||
}
|
||||
if grpcServer != nil {
|
||||
grpcServer.GracefulStop()
|
||||
}
|
||||
@@ -3844,6 +3905,64 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (process *TeleportProcess) initMinimalReverseTunnel(listeners *proxyListeners, tlsConfigWeb *tls.Config, cfg *Config, webConfig web.Config, log *logrus.Entry) (*http.Server, *web.APIHandler, error) {
|
||||
var minimalWebServer *http.Server
|
||||
var minimalWebHandler *web.APIHandler
|
||||
|
||||
internalListener := listeners.reverseTunnelMux.TLS()
|
||||
if !cfg.Proxy.DisableTLS {
|
||||
internalListener = tls.NewListener(internalListener, tlsConfigWeb)
|
||||
}
|
||||
|
||||
minimalListener, err := multiplexer.NewWebListener(multiplexer.WebListenerConfig{
|
||||
Listener: internalListener,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
listeners.minimalTLS = minimalListener
|
||||
|
||||
minimalProxyLimiter, err := limiter.NewLimiter(cfg.Proxy.Limiter)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
webConfig.MinimalReverseTunnelRoutesOnly = true
|
||||
minimalWebHandler, err = web.NewHandler(webConfig)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
}
|
||||
minimalProxyLimiter.WrapHandle(minimalWebHandler)
|
||||
|
||||
process.RegisterCriticalFunc("proxy.reversetunnel.tls", func() error {
|
||||
log.Infof("TLS multiplexer is starting on %v.", cfg.Proxy.ReverseTunnelListenAddr.Addr)
|
||||
if err := minimalListener.Serve(); !trace.IsConnectionProblem(err) {
|
||||
log.WithError(err).Warn("TLS multiplexer error.")
|
||||
}
|
||||
log.Info("TLS multiplexer exited.")
|
||||
return nil
|
||||
})
|
||||
|
||||
minimalWebServer = &http.Server{
|
||||
Handler: minimalProxyLimiter,
|
||||
ReadHeaderTimeout: apidefaults.DefaultDialTimeout,
|
||||
ErrorLog: utils.NewStdlogger(log.Error, teleport.ComponentReverseTunnelServer),
|
||||
}
|
||||
process.RegisterCriticalFunc("proxy.reversetunnel.web", func() error {
|
||||
utils.Consolef(
|
||||
cfg.Console, log, teleport.ComponentProxy,
|
||||
"Minimal web proxy service %s:%s is starting on %v.",
|
||||
teleport.Version, teleport.Gitref, cfg.Proxy.ReverseTunnelListenAddr.Addr)
|
||||
log.Infof("Minimal web proxy service %s:%s is starting on %v.", teleport.Version, teleport.Gitref, cfg.Proxy.ReverseTunnelListenAddr.Addr)
|
||||
defer minimalWebHandler.Close()
|
||||
if err := minimalWebServer.Serve(minimalListener.Web()); err != nil && err != http.ErrServerClosed {
|
||||
log.Warningf("Error while serving web requests: %v", err)
|
||||
}
|
||||
log.Info("Exited.")
|
||||
return nil
|
||||
})
|
||||
return minimalWebServer, minimalWebHandler, nil
|
||||
}
|
||||
|
||||
// kubeDialAddr returns Proxy Kube service address used for dialing local kube service
|
||||
// by remote trusted cluster.
|
||||
// If the proxy is running with Multiplex mode the WebPort is returned
|
||||
|
||||
+150
-118
@@ -193,6 +193,10 @@ type Config struct {
|
||||
// ProxySettings allows fetching the current proxy settings.
|
||||
ProxySettings proxySettingsGetter
|
||||
|
||||
// MinimalReverseTunnelRoutesOnly mode handles only the endpoints required for
|
||||
// a reverse tunnel agent to establish a connection.
|
||||
MinimalReverseTunnelRoutesOnly bool
|
||||
|
||||
// PublicProxyAddr is used to template the public proxy address
|
||||
// into the installer script responses
|
||||
PublicProxyAddr string
|
||||
@@ -212,7 +216,7 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// request is already authenticated (has a session cookie), forward to
|
||||
// application handlers. If the request is unauthenticated and requesting a
|
||||
// FQDN that is not of the proxy, redirect to application launcher.
|
||||
if app.HasFragment(r) || app.HasSession(r) || app.HasClientCert(r) {
|
||||
if h.appHandler != nil && (app.HasFragment(r) || app.HasSession(r) || app.HasClientCert(r)) {
|
||||
h.appHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -295,16 +299,157 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if cfg.MinimalReverseTunnelRoutesOnly {
|
||||
h.bindMinimalEndpoints()
|
||||
} else {
|
||||
h.bindDefaultEndpoints(challengeLimiter)
|
||||
}
|
||||
|
||||
// if Web UI is enabled, check the assets dir:
|
||||
var indexPage *template.Template
|
||||
if cfg.StaticFS != nil {
|
||||
index, err := cfg.StaticFS.Open("/index.html")
|
||||
if err != nil {
|
||||
h.log.WithError(err).Error("Failed to open index file.")
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
defer index.Close()
|
||||
indexContent, err := io.ReadAll(index)
|
||||
if err != nil {
|
||||
return nil, trace.ConvertSystemError(err)
|
||||
}
|
||||
indexPage, err = template.New("index").Parse(string(indexContent))
|
||||
if err != nil {
|
||||
return nil, trace.BadParameter("failed parsing index.html template: %v", err)
|
||||
}
|
||||
|
||||
h.Handle("GET", "/web/config.js", httplib.MakeHandler(h.getWebConfig))
|
||||
}
|
||||
|
||||
routingHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// request is going to the API?
|
||||
if strings.HasPrefix(r.URL.Path, apiPrefix) {
|
||||
http.StripPrefix(apiPrefix, h).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// request is going to the web UI
|
||||
if cfg.StaticFS == nil {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to "/web" when someone hits "/"
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/web", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// serve Web UI:
|
||||
if strings.HasPrefix(r.URL.Path, "/web/app") {
|
||||
httplib.SetStaticFileHeaders(w.Header())
|
||||
http.StripPrefix("/web", makeGzipHandler(http.FileServer(cfg.StaticFS))).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/web/") || r.URL.Path == "/web" {
|
||||
csrfToken, err := csrf.AddCSRFProtection(w, r)
|
||||
if err != nil {
|
||||
h.log.WithError(err).Warn("Failed to generate CSRF token.")
|
||||
}
|
||||
|
||||
session, err := h.authenticateWebSession(w, r)
|
||||
if err != nil {
|
||||
h.log.WithError(err).Debug("Could not authenticate.")
|
||||
}
|
||||
session.XCSRF = csrfToken
|
||||
|
||||
httplib.SetIndexHTMLHeaders(w.Header())
|
||||
if err := indexPage.Execute(w, session); err != nil {
|
||||
h.log.WithError(err).Error("Failed to execute index page template.")
|
||||
}
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
h.NotFound = routingHandler
|
||||
|
||||
if cfg.PluginRegistry != nil {
|
||||
if err := cfg.PluginRegistry.RegisterProxyWebHandlers(h); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := h.cfg.ProxySettings.GetProxySettings(cfg.Context)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Create application specific handler. This handler handles sessions and
|
||||
// forwarding for application access.
|
||||
var appHandler *app.Handler
|
||||
if !cfg.MinimalReverseTunnelRoutesOnly {
|
||||
appHandler, err = app.NewHandler(cfg.Context, &app.HandlerConfig{
|
||||
Clock: h.clock,
|
||||
AuthClient: cfg.ProxyClient,
|
||||
AccessPoint: cfg.AccessPoint,
|
||||
ProxyClient: cfg.Proxy,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
WebPublicAddr: resp.SSH.PublicAddr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &APIHandler{
|
||||
handler: h,
|
||||
appHandler: appHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type webSession struct {
|
||||
Session string
|
||||
XCSRF string
|
||||
}
|
||||
|
||||
func (h *Handler) authenticateWebSession(w http.ResponseWriter, r *http.Request) (webSession, error) {
|
||||
ctx, err := h.AuthenticateRequest(w, r, false)
|
||||
if err != nil {
|
||||
return webSession{}, trace.Wrap(err)
|
||||
}
|
||||
resp, err := newSessionResponse(ctx)
|
||||
if err != nil {
|
||||
return webSession{}, trace.Wrap(err)
|
||||
}
|
||||
out, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return webSession{}, trace.Wrap(err)
|
||||
}
|
||||
return webSession{
|
||||
Session: base64.StdEncoding.EncodeToString(out),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// bindMinimalEndpoints binds only the endpoints required for a reverse tunnel
|
||||
// agent to establish a connection.
|
||||
func (h *Handler) bindMinimalEndpoints() {
|
||||
// find is like ping, but is faster because it is optimized for servers
|
||||
// and does not fetch the data that servers don't need, e.g.
|
||||
// OIDC connectors and auth preferences
|
||||
h.GET("/webapi/find", httplib.MakeHandler(h.find))
|
||||
// Issue host credentials.
|
||||
h.POST("/webapi/host/credentials", httplib.MakeHandler(h.hostCredentials))
|
||||
}
|
||||
|
||||
// bindDefaultEndpoints binds the default endpoints for the web API.
|
||||
func (h *Handler) bindDefaultEndpoints(challengeLimiter *limiter.RateLimiter) {
|
||||
h.bindMinimalEndpoints()
|
||||
|
||||
// ping endpoint is used to check if the server is up. the /webapi/ping
|
||||
// endpoint returns the default authentication method and configuration that
|
||||
// the server supports. the /webapi/ping/:connector endpoint can be used to
|
||||
// query the authentication configuration for a specific connector.
|
||||
h.GET("/webapi/ping", httplib.MakeHandler(h.ping))
|
||||
h.GET("/webapi/ping/:connector", httplib.MakeHandler(h.pingWithConnector))
|
||||
// find is like ping, but is faster because it is optimized for servers
|
||||
// and does not fetch the data that servers don't need, e.g.
|
||||
// OIDC connectors and auth preferences
|
||||
h.GET("/webapi/find", httplib.MakeHandler(h.find))
|
||||
|
||||
// Unauthenticated access to JWT public keys.
|
||||
h.GET("/.well-known/jwks.json", httplib.MakeHandler(h.jwks))
|
||||
@@ -432,9 +577,6 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
|
||||
// User Status (used by client to check if user session is valid)
|
||||
h.GET("/webapi/user/status", h.WithAuth(h.getUserStatus))
|
||||
|
||||
// Issue host credentials.
|
||||
h.POST("/webapi/host/credentials", httplib.MakeHandler(h.hostCredentials))
|
||||
|
||||
h.GET("/webapi/roles", h.WithAuth(h.getRolesHandle))
|
||||
h.PUT("/webapi/roles", h.WithAuth(h.upsertRoleHandle))
|
||||
h.POST("/webapi/roles", h.WithAuth(h.upsertRoleHandle))
|
||||
@@ -463,116 +605,6 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
|
||||
|
||||
// GET a Connection Diagnostics by its name
|
||||
h.GET("/webapi/sites/:site/diagnostics/connections/:connectionid", h.WithClusterAuth(h.getConnectionDiagnostic))
|
||||
|
||||
// if Web UI is enabled, check the assets dir:
|
||||
var indexPage *template.Template
|
||||
if cfg.StaticFS != nil {
|
||||
index, err := cfg.StaticFS.Open("/index.html")
|
||||
if err != nil {
|
||||
h.log.WithError(err).Error("Failed to open index file.")
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
defer index.Close()
|
||||
indexContent, err := io.ReadAll(index)
|
||||
if err != nil {
|
||||
return nil, trace.ConvertSystemError(err)
|
||||
}
|
||||
indexPage, err = template.New("index").Parse(string(indexContent))
|
||||
if err != nil {
|
||||
return nil, trace.BadParameter("failed parsing index.html template: %v", err)
|
||||
}
|
||||
|
||||
h.Handle("GET", "/web/config.js", httplib.MakeHandler(h.getWebConfig))
|
||||
}
|
||||
|
||||
routingHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// request is going to the API?
|
||||
if strings.HasPrefix(r.URL.Path, apiPrefix) {
|
||||
http.StripPrefix(apiPrefix, h).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// request is going to the web UI
|
||||
if cfg.StaticFS == nil {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to "/web" when someone hits "/"
|
||||
if r.URL.Path == "/" {
|
||||
http.Redirect(w, r, "/web", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// serve Web UI:
|
||||
if strings.HasPrefix(r.URL.Path, "/web/app") {
|
||||
httplib.SetStaticFileHeaders(w.Header())
|
||||
http.StripPrefix("/web", makeGzipHandler(http.FileServer(cfg.StaticFS))).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/web/") || r.URL.Path == "/web" {
|
||||
csrfToken, err := csrf.AddCSRFProtection(w, r)
|
||||
if err != nil {
|
||||
h.log.WithError(err).Warn("Failed to generate CSRF token.")
|
||||
}
|
||||
|
||||
session := struct {
|
||||
Session string
|
||||
XCSRF string
|
||||
}{
|
||||
XCSRF: csrfToken,
|
||||
}
|
||||
|
||||
ctx, err := h.AuthenticateRequest(w, r, false)
|
||||
if err == nil {
|
||||
resp, err := newSessionResponse(ctx)
|
||||
if err == nil {
|
||||
out, err := json.Marshal(resp)
|
||||
if err == nil {
|
||||
session.Session = base64.StdEncoding.EncodeToString(out)
|
||||
}
|
||||
} else {
|
||||
h.log.WithError(err).Debug("Could not authenticate.")
|
||||
}
|
||||
}
|
||||
httplib.SetIndexHTMLHeaders(w.Header())
|
||||
if err := indexPage.Execute(w, session); err != nil {
|
||||
h.log.WithError(err).Error("Failed to execute index page template.")
|
||||
}
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
h.NotFound = routingHandler
|
||||
|
||||
if cfg.PluginRegistry != nil {
|
||||
if err := cfg.PluginRegistry.RegisterProxyWebHandlers(h); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := h.cfg.ProxySettings.GetProxySettings(cfg.Context)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Create application specific handler. This handler handles sessions and
|
||||
// forwarding for application access.
|
||||
appHandler, err := app.NewHandler(cfg.Context, &app.HandlerConfig{
|
||||
Clock: h.clock,
|
||||
AuthClient: cfg.ProxyClient,
|
||||
AccessPoint: cfg.AccessPoint,
|
||||
ProxyClient: cfg.Proxy,
|
||||
CipherSuites: cfg.CipherSuites,
|
||||
WebPublicAddr: resp.SSH.PublicAddr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &APIHandler{
|
||||
handler: h,
|
||||
appHandler: appHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProxyClient returns authenticated auth server client
|
||||
|
||||
Reference in New Issue
Block a user