Skip to content

Commit 48169a2

Browse files
domdom82ScheererJrfranzke
authored
do not leak api-server cluster ip from seed to shoot (gardener#10949)
* fix: do not leak api-server cluster ip from seed to shoot * Apply suggestions from code review new lines etc. Co-authored-by: Johannes Scheerer <johannes.scheerer@sap.com> Co-authored-by: Rafael Franzke <rafael.franzke@sap.com> * refactor: remove unused func * refactor: remove unnecessary const * refactor: move seed range check from shoot to cidr * docs: add reserved ranges to shoot networking guide * refactor: rename ReservedSeedServiceRange => ReservedKubeApiServerMappingRange * feat: add IsIPv4, IsIPv6 helper functions to cidr --------- Co-authored-by: Johannes Scheerer <johannes.scheerer@sap.com> Co-authored-by: Rafael Franzke <rafael.franzke@sap.com>
1 parent 26d505b commit 48169a2

File tree

9 files changed

+321
-7
lines changed

9 files changed

+321
-7
lines changed

docs/usage/networking/shoot_networking.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,17 @@ Number of IPs per podCIDRs: 128
8585
```
8686

8787
With the configuration above, a Shoot cluster can at most have **32 nodes** which are ready to run workload in the Pod network.
88+
89+
## Reserved Networks
90+
91+
Some network ranges are reserved for specific use-cases in the communication between seeds and shoots.
92+
93+
| IPv | CIDR | Name | Purpose |
94+
|------|-----------------------|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
95+
| IPv4 | 192.168.123.0/24 | Default VPN Range | Used for communication between seed API server and shoot resources via VPN. Will be removed once feature gate `NewVPN` is graduated. |
96+
| IPv6 | fd8f:6d53:b97a:1::/96 | Default VPN Range | |
97+
| IPv4 | 240.0.0.0/8 | Kube-ApiServer Mapping Range | Used for the `kubernetes.default.svc.cluster.local` service in a shoot |
98+
99+
> :warning: Do not use any of the CIDR ranges mentioned above for any of the node, pod or service networks.
100+
> Gardener will prevent their creation. Pre-existing shoots using reserved ranges will still work, though it is recommended
101+
> to recreate them with compatible network ranges.

pkg/apis/core/v1beta1/constants/types_constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,8 @@ const (
781781
DefaultVPNRange = "192.168.123.0/24"
782782
// DefaultVPNRangeV6 is the default IPv6 network range for the VPN between seed and shoot cluster.
783783
DefaultVPNRangeV6 = "fd8f:6d53:b97a:1::/96"
784+
// ReservedKubeApiServerMappingRange is the IPv4 network range for the "kubernetes" service used by apiserver-proxy
785+
ReservedKubeApiServerMappingRange = "240.0.0.0/8"
784786

785787
// BackupSecretName is the name of secret having credentials for etcd backups.
786788
BackupSecretName string = "etcd-backup"

pkg/apis/core/validation/seed.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,15 +248,22 @@ func validateSeedNetworks(seedNetworks core.SeedNetworks, fldPath *field.Path, i
248248
}
249249

250250
var (
251-
primaryIPFamily = helper.DeterminePrimaryIPFamily(seedNetworks.IPFamilies)
252-
networks []cidrvalidation.CIDR
251+
primaryIPFamily = helper.DeterminePrimaryIPFamily(seedNetworks.IPFamilies)
252+
networks []cidrvalidation.CIDR
253+
reservedSeedServiceRange = cidrvalidation.NewCIDR(v1beta1constants.ReservedKubeApiServerMappingRange, field.NewPath(""))
253254
)
254255

255256
if !inTemplate || len(seedNetworks.Pods) > 0 {
256257
networks = append(networks, cidrvalidation.NewCIDR(seedNetworks.Pods, fldPath.Child("pods")))
257258
}
258259
if !inTemplate || len(seedNetworks.Services) > 0 {
259-
networks = append(networks, cidrvalidation.NewCIDR(seedNetworks.Services, fldPath.Child("services")))
260+
services := cidrvalidation.NewCIDR(seedNetworks.Services, fldPath.Child("services"))
261+
networks = append(networks, services)
262+
// Service range must not be larger than /8 for ipv4
263+
if services.IsIPv4() {
264+
maxSize, _ := reservedSeedServiceRange.GetIPNet().Mask.Size()
265+
allErrs = append(allErrs, services.ValidateMaxSize(maxSize)...)
266+
}
260267
}
261268
if seedNetworks.Nodes != nil {
262269
networks = append(networks, cidrvalidation.NewCIDR(*seedNetworks.Nodes, fldPath.Child("nodes")))
@@ -277,6 +284,7 @@ func validateSeedNetworks(seedNetworks core.SeedNetworks, fldPath *field.Path, i
277284
}
278285
allErrs = append(allErrs, cidrvalidation.ValidateCIDROverlap(networks, false)...)
279286

287+
allErrs = append(allErrs, reservedSeedServiceRange.ValidateNotOverlap(networks...)...)
280288
vpnRange := cidrvalidation.NewCIDR(v1beta1constants.DefaultVPNRange, field.NewPath(""))
281289
allErrs = append(allErrs, vpnRange.ValidateNotOverlap(networks...)...)
282290
vpnRangeV6 := cidrvalidation.NewCIDR(v1beta1constants.DefaultVPNRangeV6, field.NewPath(""))

pkg/apis/core/validation/seed_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,32 @@ var _ = Describe("Seed Validation Tests", func() {
535535
"Detail": Equal(`must not overlap with "[]" ("192.168.123.0/24")`),
536536
}))
537537
})
538+
539+
It("should forbid Seed with overlap to reserved range", func() {
540+
// Service CIDR overlaps with reserved range
541+
seed.Spec.Networks.Pods = "240.0.0.0/16" // 240.0.0.0 -> 240.0.255.255
542+
543+
errorList := ValidateSeed(seed)
544+
545+
Expect(errorList).To(ConsistOfFields(Fields{
546+
"Type": Equal(field.ErrorTypeInvalid),
547+
"Field": Equal("spec.networks.pods"),
548+
"Detail": Equal(`must not overlap with "[]" ("240.0.0.0/8")`),
549+
}))
550+
})
551+
552+
It("should forbid Seed with too large service range", func() {
553+
// Service CIDR too large
554+
seed.Spec.Networks.Services = "90.0.0.0/7" // 90.0.0.0 -> 91.255.255.255
555+
556+
errorList := ValidateSeed(seed)
557+
558+
Expect(errorList).To(ConsistOfFields(Fields{
559+
"Type": Equal(field.ErrorTypeInvalid),
560+
"Field": Equal("spec.networks.services"),
561+
"Detail": Equal(`cannot be larger than /8`),
562+
}))
563+
})
538564
})
539565

540566
Context("IPv6", func() {
@@ -616,6 +642,18 @@ var _ = Describe("Seed Validation Tests", func() {
616642
"Detail": ContainSubstring("must be a valid IPv6 address"),
617643
}))
618644
})
645+
646+
It("should allow Seed with very large service range because the range limit only applies to ipv4", func() {
647+
seed.Spec.Networks.Nodes = ptr.To("2001:db8:11::/48")
648+
seed.Spec.Networks.Pods = "2001:db8:12::/48"
649+
seed.Spec.Networks.Services = "3001::/7" // larger than /8 ipv4 limit
650+
seed.Spec.Networks.ShootDefaults.Pods = ptr.To("2001:db8:1::/48")
651+
seed.Spec.Networks.ShootDefaults.Services = ptr.To("2001:db8:3::/48")
652+
653+
errorList := ValidateSeed(seed)
654+
655+
Expect(errorList).To(BeEmpty())
656+
})
619657
})
620658
})
621659

pkg/gardenlet/operation/botanist/kubeapiserverexposure.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,22 @@ func (b *Botanist) DeployKubeAPIServerSNI(ctx context.Context) error {
6868
}
6969

7070
func (b *Botanist) setAPIServerServiceClusterIP(clusterIP string) {
71-
if b.Shoot.GetInfo().Spec.Networking.IPFamilies[0] == v1beta1.IPFamilyIPv6 && net.ParseIP(clusterIP).To4() != nil {
72-
// "64:ff9b:1::" is a well known prefix for address translation for use
73-
// in local networks.
74-
b.APIServerClusterIP = "64:ff9b:1::" + clusterIP
71+
clusterIPv4 := net.ParseIP(clusterIP).To4()
72+
73+
if clusterIPv4 != nil {
74+
if b.Shoot.GetInfo().Spec.Networking.IPFamilies[0] == v1beta1.IPFamilyIPv6 {
75+
// "64:ff9b:1::" is a well known prefix for address translation for use
76+
// in local networks.
77+
b.APIServerClusterIP = "64:ff9b:1::" + clusterIP
78+
} else {
79+
// prevent leakage of real cluster ip to shoot. we use the reserved range 240.0.0.0/8 as prefix instead.
80+
// e.g. cluster ip in seed: 192.168.102.23 => ip in shoot: 240.168.102.23
81+
prefixIp, _, _ := net.ParseCIDR(v1beta1constants.ReservedKubeApiServerMappingRange)
82+
prefix := prefixIp.To4()
83+
b.APIServerClusterIP = net.IPv4(prefix[0], clusterIPv4[1], clusterIPv4[2], clusterIPv4[3]).String()
84+
}
7585
} else {
86+
// regular ipv6 cluster ip
7687
b.APIServerClusterIP = clusterIP
7788
}
7889
b.Shoot.Components.ControlPlane.KubeAPIServerSNI = kubeapiserverexposure.NewSNI(

pkg/utils/validation/cidr/cidr.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ type CIDR interface {
4141
LastIPInRange() net.IP
4242
// ValidateOverlap returns errors if the subnets do not overlap with CIDR.
4343
ValidateOverlap(subsets ...CIDR) field.ErrorList
44+
// ValidateMaxSize returns errors if the subnet is larger than the given bits. e.g. /15 is larger than /16
45+
ValidateMaxSize(bits int) field.ErrorList
46+
// IsIPv4 returns true if the CIDR is a valid v4 CIDR, false otherwise.
47+
IsIPv4() bool
48+
// IsIPv6 returns true if the CIDR is a valid v6 CIDR, false otherwise.
49+
IsIPv6() bool
4450
}
4551

4652
type cidrPath struct {
@@ -147,6 +153,22 @@ func (c *cidrPath) ValidateIPFamily(ipFamily string) field.ErrorList {
147153
return allErrs
148154
}
149155

156+
// ValidateMaxSize returns an error if CIDR size is larger than given bits. e.g. /15 is larger than /16
157+
func (c *cidrPath) ValidateMaxSize(bits int) field.ErrorList {
158+
allErrs := field.ErrorList{}
159+
160+
if c.ParseError != nil {
161+
return allErrs
162+
}
163+
cidrBits, _ := c.net.Mask.Size()
164+
165+
if cidrBits < bits {
166+
allErrs = append(allErrs, field.Invalid(c.fieldPath, c.net.String(), fmt.Sprintf("cannot be larger than /%d", bits)))
167+
}
168+
169+
return allErrs
170+
}
171+
150172
func (c *cidrPath) Parse() (success bool) {
151173
return c.ParseError == nil
152174
}
@@ -176,3 +198,17 @@ func (c *cidrPath) LastIPInRange() net.IP {
176198

177199
return res
178200
}
201+
202+
func (c *cidrPath) IsIPv4() bool {
203+
if c.ParseError == nil && len(c.ValidateIPFamily(IPFamilyIPv4)) == 0 {
204+
return true
205+
}
206+
return false
207+
}
208+
209+
func (c *cidrPath) IsIPv6() bool {
210+
if c.ParseError == nil && len(c.ValidateIPFamily(IPFamilyIPv6)) == 0 {
211+
return true
212+
}
213+
return false
214+
}

pkg/utils/validation/cidr/cidr_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,69 @@ var _ = Describe("cidr", func() {
274274
Expect(other.ValidateOverlap(cdr)).To(BeEmpty())
275275
})
276276
})
277+
278+
Describe("ValidateMaxSize", func() {
279+
It("should return no errors if cidr is same size than limit", func() {
280+
goodPath := field.NewPath("good")
281+
goodCIDR := "11.12.0.0/16"
282+
good := NewCIDR(goodCIDR, goodPath)
283+
284+
Expect(good.ValidateMaxSize(16)).To(BeEmpty())
285+
})
286+
287+
It("should return no errors if cidr is smaller than limit", func() {
288+
goodPath := field.NewPath("good")
289+
goodCIDR := "11.12.0.0/17"
290+
good := NewCIDR(goodCIDR, goodPath)
291+
292+
Expect(good.ValidateMaxSize(16)).To(BeEmpty())
293+
})
294+
295+
It("should return an error if cidr is larger than limit", func() {
296+
badPath := field.NewPath("bad")
297+
badCIDR := "11.12.0.0/15"
298+
bad := NewCIDR(badCIDR, badPath)
299+
300+
Expect(bad.ValidateMaxSize(16)).To(ConsistOfFields(Fields{
301+
"Type": Equal(field.ErrorTypeInvalid),
302+
"Field": Equal(badPath.String()),
303+
"BadValue": Equal(badCIDR),
304+
"Detail": Equal(`cannot be larger than /16`),
305+
}))
306+
})
307+
})
308+
309+
Describe("IsIPv4", func() {
310+
It("should return true on a valid IPv4 cidr", func() {
311+
goodPath := field.NewPath("good")
312+
goodCIDR := "10.100.0.0/16"
313+
good := NewCIDR(goodCIDR, goodPath)
314+
315+
Expect(good.Parse()).To(BeTrue())
316+
Expect(good.IsIPv4()).To(BeTrue())
317+
Expect(good.IsIPv6()).To(BeFalse())
318+
})
319+
320+
It("should return false on a malformed IPv4 cidr", func() {
321+
badPath := field.NewPath("bad")
322+
badCIDR := "10.100.0.0/123"
323+
bad := NewCIDR(badCIDR, badPath)
324+
325+
Expect(bad.Parse()).To(BeFalse())
326+
Expect(bad.IsIPv4()).To(BeFalse())
327+
Expect(bad.IsIPv6()).To(BeFalse())
328+
})
329+
330+
It("should return false on a valid IPv6 cidr", func() {
331+
wrongPath := field.NewPath("wrong")
332+
wrongCIDR := "2001:0db8::/16"
333+
wrong := NewCIDR(wrongCIDR, wrongPath)
334+
335+
Expect(wrong.Parse()).To(BeTrue())
336+
Expect(wrong.IsIPv4()).To(BeFalse())
337+
Expect(wrong.IsIPv6()).To(BeTrue())
338+
})
339+
})
277340
})
278341

279342
Context("IPv6", func() {
@@ -520,5 +583,68 @@ var _ = Describe("cidr", func() {
520583
Expect(cdr.ValidateOverlap(other)).To(BeEmpty())
521584
})
522585
})
586+
587+
Describe("ValidateMaxSize", func() {
588+
It("should return no errors if cidr is same size as limit", func() {
589+
goodPath := field.NewPath("good")
590+
goodCIDR := "2001:1db8::/32"
591+
good := NewCIDR(goodCIDR, goodPath)
592+
593+
Expect(good.ValidateMaxSize(32)).To(BeEmpty())
594+
})
595+
596+
It("should return no errors if cidr is smaller than limit", func() {
597+
goodPath := field.NewPath("good")
598+
goodCIDR := "2001:1db8::/33"
599+
good := NewCIDR(goodCIDR, goodPath)
600+
601+
Expect(good.ValidateMaxSize(32)).To(BeEmpty())
602+
})
603+
604+
It("should return an error if cidr is larger than limit", func() {
605+
badPath := field.NewPath("bad")
606+
badCIDR := "2001:1db8::/31"
607+
bad := NewCIDR(badCIDR, badPath)
608+
609+
Expect(bad.ValidateMaxSize(32)).To(ConsistOfFields(Fields{
610+
"Type": Equal(field.ErrorTypeInvalid),
611+
"Field": Equal(badPath.String()),
612+
"BadValue": Equal(badCIDR),
613+
"Detail": Equal(`cannot be larger than /32`),
614+
}))
615+
})
616+
})
617+
618+
Describe("IsIPv6", func() {
619+
It("should return true on a valid IPv6 cidr", func() {
620+
goodPath := field.NewPath("good")
621+
goodCIDR := "2001:0db8::/16"
622+
good := NewCIDR(goodCIDR, goodPath)
623+
624+
Expect(good.Parse()).To(BeTrue())
625+
Expect(good.IsIPv4()).To(BeFalse())
626+
Expect(good.IsIPv6()).To(BeTrue())
627+
})
628+
629+
It("should return false on a malformed IPv6 cidr", func() {
630+
badPath := field.NewPath("bad")
631+
badCIDR := "2001:0db8:xxyy:/64"
632+
bad := NewCIDR(badCIDR, badPath)
633+
634+
Expect(bad.Parse()).To(BeFalse())
635+
Expect(bad.IsIPv4()).To(BeFalse())
636+
Expect(bad.IsIPv6()).To(BeFalse())
637+
})
638+
639+
It("should return false on a valid IPv4 cidr", func() {
640+
wrongPath := field.NewPath("wrong")
641+
wrongCIDR := "10.100.0.0/16"
642+
wrong := NewCIDR(wrongCIDR, wrongPath)
643+
644+
Expect(wrong.Parse()).To(BeTrue())
645+
Expect(wrong.IsIPv4()).To(BeTrue())
646+
Expect(wrong.IsIPv6()).To(BeFalse())
647+
})
648+
})
523649
})
524650
})

pkg/utils/validation/cidr/disjoint.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ func validateOverlapWithSeed(fldPath *field.Path, shootNetwork []string, network
6565
if NetworksIntersect(v1beta1constants.DefaultVPNRangeV6, network) {
6666
allErrs = append(allErrs, field.Invalid(fldPath, network, fmt.Sprintf("shoot %s network intersects with default vpn network (%s)", networkType, v1beta1constants.DefaultVPNRangeV6)))
6767
}
68+
69+
if NetworksIntersect(v1beta1constants.ReservedKubeApiServerMappingRange, network) {
70+
allErrs = append(allErrs, field.Invalid(fldPath, network, fmt.Sprintf("shoot %s network intersects with reserved kube-apiserver mapping range (%s)", networkType, v1beta1constants.ReservedKubeApiServerMappingRange)))
71+
}
6872
}
6973
if len(shootNetwork) == 0 && networkRequired {
7074
allErrs = append(allErrs, field.Required(fldPath, networkType+"s is required"))

0 commit comments

Comments
 (0)