Skip to content

Commit d5167d6

Browse files
authored
add ingress annotation for native ssh (#1413)
Add a new Ingress annotation 'ingress.pomerium.io/ssh_upstream' for native SSH routes, following the example of the similar 'tcp_upstream' and 'udp_upstream' annotations.
1 parent dfec475 commit d5167d6

4 files changed

Lines changed: 95 additions & 7 deletions

File tree

model/ingress_config.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ const (
3232
PathRegex = "path_regex"
3333
// UseServiceProxy will use standard k8s service proxy as upstream, opposed to individual endpoints
3434
UseServiceProxy = "service_proxy_upstream"
35-
// TCPUpstream indicates this route is a TCP service https://www.pomerium.com/docs/tcp/
35+
// SSHUpstream indicates this route is for natively-proxied SSH https://www.pomerium.com/docs/capabilities/native-ssh-access
36+
SSHUpstream = "ssh_upstream"
37+
// TCPUpstream indicates this route is for TCP tunneled over HTTP https://www.pomerium.com/docs/tcp/
3638
TCPUpstream = "tcp_upstream"
37-
// UDPUpstream indicates this route is a UDP service https://www.pomerium.com/docs/capabilities/udp/
39+
// UDPUpstream indicates this route is for UDP tunneled over HTTP https://www.pomerium.com/docs/capabilities/udp/
3840
UDPUpstream = "udp_upstream"
3941
// SubtleAllowEmptyHost is a required annotation when creating an ingress containing
4042
// rules with an empty (catch-all) host, as it can cause unexpected behavior
@@ -189,12 +191,17 @@ func (ic *IngressConfig) IsSecureUpstream() bool {
189191
return ic.IsAnnotationSet(SecureUpstream)
190192
}
191193

192-
// IsTCPUpstream returns true is this route represents a TCP service https://www.pomerium.com/docs/tcp/
194+
// IsSSHUpstream returns true if this route is for natively-proxied SSH https://www.pomerium.com/docs/capabilities/native-ssh-access
195+
func (ic *IngressConfig) IsSSHUpstream() bool {
196+
return ic.IsAnnotationSet(SSHUpstream)
197+
}
198+
199+
// IsTCPUpstream returns true if this route is for TCP tunneled over HTTP https://www.pomerium.com/docs/tcp/
193200
func (ic *IngressConfig) IsTCPUpstream() bool {
194201
return ic.IsAnnotationSet(TCPUpstream)
195202
}
196203

197-
// IsUDPUpstream returns true is this route represents a UDP service https://www.pomerium.com/docs/capabilities/tcp/
204+
// IsUDPUpstream returns true if this route is for UDP tunneled over HTTP https://www.pomerium.com/docs/capabilities/tcp/
198205
func (ic *IngressConfig) IsUDPUpstream() bool {
199206
return ic.IsAnnotationSet(UDPUpstream)
200207
}

pomerium/ingress_annotations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ var (
9494
handledElsewhere = boolMap([]string{
9595
model.PathRegex,
9696
model.SecureUpstream,
97+
model.SSHUpstream,
9798
model.TCPUpstream,
9899
model.UDPUpstream,
99100
model.UseServiceProxy,

pomerium/ingress_to_route.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,22 @@ func setRoutePath(r *pb.Route, p networkingv1.HTTPIngressPath, ic *model.Ingress
142142
return fmt.Errorf("pathType is required")
143143
}
144144

145+
if ic.IsSSHUpstream() {
146+
if *p.PathType != networkingv1.PathTypeImplementationSpecific {
147+
return fmt.Errorf("ssh services must have %s path type", networkingv1.PathTypeImplementationSpecific)
148+
}
149+
if p.Path != "" {
150+
return fmt.Errorf("ssh services must not specify path, got %s", p.Path)
151+
}
152+
return nil
153+
}
154+
145155
if ic.IsTCPUpstream() {
146156
if *p.PathType != networkingv1.PathTypeImplementationSpecific {
147157
return fmt.Errorf("tcp services must have %s path type", networkingv1.PathTypeImplementationSpecific)
148158
}
149159
if p.Path != "" {
150-
return fmt.Errorf("tcp services must not specify path, got %s", r.Path)
160+
return fmt.Errorf("tcp services must not specify path, got %s", p.Path)
151161
}
152162
return nil
153163
}
@@ -157,7 +167,7 @@ func setRoutePath(r *pb.Route, p networkingv1.HTTPIngressPath, ic *model.Ingress
157167
return fmt.Errorf("udp services must have %s path type", networkingv1.PathTypeImplementationSpecific)
158168
}
159169
if p.Path != "" {
160-
return fmt.Errorf("udp services must not specify path, got %s", r.Path)
170+
return fmt.Errorf("udp services must not specify path, got %s", p.Path)
161171
}
162172
return nil
163173
}
@@ -187,6 +197,10 @@ func setRouteFrom(r *pb.Route, host string, p networkingv1.HTTPIngressPath, ic *
187197
Host: host,
188198
}
189199

200+
if ic.IsSSHUpstream() {
201+
u.Scheme = "ssh"
202+
}
203+
190204
if ic.IsTCPUpstream() {
191205
_, _, port, err := getServiceFromPath(p, ic)
192206
if err != nil {
@@ -282,7 +296,9 @@ func getPathServiceHosts(r *pb.Route, p networkingv1.HTTPIngressPath, ic *model.
282296
}
283297

284298
func getUpstreamScheme(ic *model.IngressConfig) string {
285-
if ic.IsTCPUpstream() {
299+
if ic.IsSSHUpstream() {
300+
return "ssh"
301+
} else if ic.IsTCPUpstream() {
286302
return "tcp"
287303
} else if ic.IsUDPUpstream() {
288304
return "udp"

pomerium/routes_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,70 @@ func TestKubernetesToken(t *testing.T) {
467467
require.NoError(t, validate(ctx, cfg, "test"))
468468
}
469469

470+
func TestSSHUpstream(t *testing.T) {
471+
irv := networkingv1.IngressRuleValue{
472+
HTTP: &networkingv1.HTTPIngressRuleValue{
473+
Paths: []networkingv1.HTTPIngressPath{{
474+
PathType: new(networkingv1.PathTypeImplementationSpecific),
475+
Backend: networkingv1.IngressBackend{
476+
Service: &networkingv1.IngressServiceBackend{
477+
Name: "example-svc",
478+
Port: networkingv1.ServiceBackendPort{
479+
Number: 22,
480+
},
481+
},
482+
},
483+
}},
484+
},
485+
}
486+
ic := model.IngressConfig{
487+
AnnotationPrefix: "p",
488+
Ingress: &networkingv1.Ingress{
489+
ObjectMeta: metav1.ObjectMeta{
490+
Name: "ingress",
491+
Namespace: "default",
492+
Annotations: map[string]string{
493+
"p/ssh_upstream": "true",
494+
},
495+
},
496+
Spec: networkingv1.IngressSpec{
497+
Rules: []networkingv1.IngressRule{{
498+
Host: "my-ssh-route",
499+
IngressRuleValue: irv,
500+
}},
501+
},
502+
},
503+
Services: map[types.NamespacedName]*corev1.Service{
504+
{Name: "example-svc", Namespace: "default"}: {},
505+
},
506+
}
507+
508+
t.Run("ok", func(t *testing.T) {
509+
routes, err := ingressToRoutes(t.Context(), &ic)
510+
require.NoError(t, err)
511+
512+
require.Len(t, routes, 1)
513+
assert.Equal(t, "ssh://my-ssh-route", routes[0].From)
514+
assert.Equal(t, []string{"ssh://example-svc.default.svc.cluster.local:22"}, routes[0].To)
515+
})
516+
t.Run("bad path type", func(t *testing.T) {
517+
ic := ic
518+
ic.Ingress = ic.Ingress.DeepCopy()
519+
ic.Ingress.Spec.Rules[0].HTTP.Paths[0].PathType = new(networkingv1.PathTypeExact)
520+
521+
_, err := ingressToRoutes(t.Context(), &ic)
522+
assert.ErrorContains(t, err, "ssh services must have ImplementationSpecific path type")
523+
})
524+
t.Run("non-empty path", func(t *testing.T) {
525+
ic := ic
526+
ic.Ingress = ic.Ingress.DeepCopy()
527+
ic.Ingress.Spec.Rules[0].HTTP.Paths[0].Path = "/some/path"
528+
529+
_, err := ingressToRoutes(t.Context(), &ic)
530+
assert.ErrorContains(t, err, "ssh services must not specify path, got /some/path")
531+
})
532+
}
533+
470534
func TestTCPUpstream(t *testing.T) {
471535
typePrefix := networkingv1.PathTypePrefix
472536
typeImpSpec := networkingv1.PathTypeImplementationSpecific

0 commit comments

Comments
 (0)