Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ curl \

To open SPYT UI, just paste service domain in your browser. If you have correct cookies in your `baseDomain`, task proxy will use it to auth your browser request. Auth cookie name is specified in Helm chart values in `auth.cookieName` parameter.

You can also use header `x-yt-taskproxy-id` with left part of domain as value (`645236d8` in our example) as alternative way of routing instead of domain. It is suitable in situations
then using domain is not possible, i.e. when your infrastructure doesn't support wildcard domains:
```sh
curl \
-H "Authorization: Bearer ${IAM_TOKEN}" \
-H "x-yt-taskproxy-id: 645236d8" \
"https://task-proxy.my-cluster.ytsaurus.example.net"
```

## Extended task proxy annotation format

Annotation `task_proxy={enabled=%true}` is minimal required format. Using it, each service given port indexes in order of task appearance and default service names `port_${PORT_INDEX}`, as well as `HTTP` protocol. If you want to specify service names, which port index they use and protocol, you can use extended annotation format.
Expand Down
17 changes: 10 additions & 7 deletions server/pkg/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,21 @@ func (s *authServer) Check(ctx context.Context, req *authv3.CheckRequest) (*auth
path := httpAttrs.GetPath()
headers := httpAttrs.GetHeaders()

host := httpAttrs.Host
if host == "" {
s.logger.Warnf("authority (host) header is missing in request")
var hash string
if routerHeaderValue, ok := httpAttrs.Headers[routerHeaderName]; ok {
hash = routerHeaderValue
} else if host := httpAttrs.Host; host != "" {
hash = strings.Split(host, ".")[0]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho, domain prefix has a higher priority than header

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or we should fail if there is both a domain prefix and a header (especially if they are different)

Copy link
Collaborator Author

@futujaos futujaos Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Host is always present in httpAttrs, since we make request to task proxy with some FQDN
last else clause is unreachable in practice, unless someone passes empty Host header value directly

} else {
s.logger.Warnf("authority (host) or %s headers are missing in request", routerHeaderName)
return deniedResponse, nil
}

s.logger.Debugf("checking auth for host %q, path %q", host, path)
s.logger.Debugf("checking auth for hash %q, path %q", hash, path)

hash := strings.Split(host, ".")[0]
task, ok := s.getHashToTasks()[hash]
if !ok {
s.logger.Warnf("no entry for host %q in tasks registry", host)
s.logger.Warnf("no entry for hash %q in tasks registry", hash)
return deniedResponse, nil
}

Expand All @@ -63,7 +66,7 @@ func (s *authServer) Check(ctx context.Context, req *authv3.CheckRequest) (*auth
return okResponse, nil
}

s.logger.Debugf("auth for host %q, path %q, task %v", host, path, task)
s.logger.Debugf("auth for hash %q, path %q, task %v", hash, path, task)

allowed, err := s.checkOperationPermission(ctx, task.operationID, headers)
if err != nil {
Expand Down
68 changes: 48 additions & 20 deletions server/pkg/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ import (
clustergrpc "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
discoverygrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
listenergrpc "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3"
matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
cachetypes "github.com/envoyproxy/go-control-plane/pkg/cache/types"
cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
serverv3 "github.com/envoyproxy/go-control-plane/pkg/server/v3"
)

const extAuthClusterName = "extAuthz"
const (
extAuthClusterName = "extAuthz"
routerHeaderName = "x-yt-taskproxy-id"
)

func ServeGRPC(s serverv3.Server, authServer *authServer) error {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", serverPort))
Expand All @@ -57,6 +61,9 @@ func ServeGRPC(s serverv3.Server, authServer *authServer) error {
func makeSnapshot(hashToTask map[string]Task, version string, baseDomain string, tls bool, authEnabled bool) (*cachev3.Snapshot, error) {
var clusters []cachetypes.Resource
var vhosts []*routev3.VirtualHost

var defaultVhostRoutes []*routev3.Route

for hash, task := range hashToTask {
grpc := task.protocol == "grpc"
vhostName := fmt.Sprintf("%s-%s-%s", task.operationID, task.taskName, task.service)
Expand All @@ -70,39 +77,60 @@ func makeSnapshot(hashToTask map[string]Task, version string, baseDomain string,
Weight: &wrapperspb.UInt32Value{Value: 1},
})
}

action := &routev3.Route_Route{
Route: &routev3.RouteAction{
ClusterSpecifier: &routev3.RouteAction_WeightedClusters{
WeightedClusters: &routev3.WeightedCluster{
Clusters: vhostClusters,
},
},
},
}
// route either by domain
vhosts = append(vhosts, &routev3.VirtualHost{
Name: vhostName,
Domains: []string{getTaskDomain(hash, baseDomain)},
Routes: []*routev3.Route{{
Match: &routev3.RouteMatch{PathSpecifier: &routev3.RouteMatch_Prefix{Prefix: "/"}},
Action: &routev3.Route_Route{
Route: &routev3.RouteAction{
ClusterSpecifier: &routev3.RouteAction_WeightedClusters{
WeightedClusters: &routev3.WeightedCluster{
Clusters: vhostClusters,
Match: &routev3.RouteMatch{PathSpecifier: &routev3.RouteMatch_Prefix{Prefix: "/"}},
Action: action,
}},
})
// ... or by custom header
defaultVhostRoutes = append(defaultVhostRoutes, &routev3.Route{
Match: &routev3.RouteMatch{
PathSpecifier: &routev3.RouteMatch_Prefix{Prefix: "/"},
Headers: []*routev3.HeaderMatcher{
{
Name: routerHeaderName,
HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
StringMatch: &matcherv3.StringMatcher{
MatchPattern: &matcherv3.StringMatcher_Exact{
Exact: hash,
},
},
},
},
},
}},
},
Action: action,
})
}

defaultVhostRoutes = append(defaultVhostRoutes, &routev3.Route{
Match: &routev3.RouteMatch{PathSpecifier: &routev3.RouteMatch_Prefix{Prefix: "/"}},
Action: &routev3.Route_DirectResponse{
DirectResponse: &routev3.DirectResponseAction{
Status: 404,
Body: &corev3.DataSource{
Specifier: &corev3.DataSource_InlineString{InlineString: "no such task"},
},
},
},
})
vhosts = append(vhosts, &routev3.VirtualHost{
Name: "vhost_default",
Domains: []string{"*"},
Routes: []*routev3.Route{{
Match: &routev3.RouteMatch{PathSpecifier: &routev3.RouteMatch_Prefix{Prefix: "/"}},
Action: &routev3.Route_DirectResponse{
DirectResponse: &routev3.DirectResponseAction{
Status: 404,
Body: &corev3.DataSource{
Specifier: &corev3.DataSource_InlineString{InlineString: "no such task"},
},
},
},
}},
Routes: defaultVhostRoutes,
})

if authEnabled {
Expand Down
Loading