Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bug 1986228: NE-310 e2e test for HSTS
- Loading branch information
Showing
4 changed files
with
220 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// +build e2e | ||
|
||
package e2e | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
configv1 "github.com/openshift/api/config/v1" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/types" | ||
"k8s.io/apimachinery/pkg/util/wait" | ||
) | ||
|
||
// Helper function for int32 pointers | ||
func intPtr(s int32) *int32 { | ||
return &s | ||
} | ||
|
||
func TestHstsPolicyWorks(t *testing.T) { | ||
icName := types.NamespacedName{Namespace: operatorNamespace, Name: "hsts-policy"} | ||
domain := icName.Name + "." + dnsConfig.Spec.BaseDomain | ||
|
||
// Setup a Required HSTS Policy for the ingress config | ||
maxAgePolicy := configv1.MaxAgePolicy{LargestMaxAge: intPtr(99999), SmallestMaxAge: intPtr(0)} | ||
domainPatterns := []string{domain} | ||
hstsPolicy := configv1.RequiredHSTSPolicy{ | ||
DomainPatterns: domainPatterns, // this policy will only validate routes with hosts in the DomainPatterns | ||
PreloadPolicy: configv1.RequirePreloadPolicy, | ||
IncludeSubDomainsPolicy: configv1.RequireIncludeSubDomains, | ||
MaxAge: maxAgePolicy, | ||
} | ||
|
||
ing := &configv1.Ingress{} | ||
appsDomain := "" | ||
|
||
// Update the ingress config with the new HSTS policy, and include its apps domain | ||
if err := wait.PollImmediate(1*time.Second, 1*time.Minute, func() (bool, error) { | ||
// Get the ingress config | ||
if err := kclient.Get(context.TODO(), clusterConfigName, ing); err != nil { | ||
t.Logf("Get ingress config failed: %v, retrying...", err) | ||
return false, nil | ||
} | ||
|
||
// Update the hsts.DomainPatterns with the ingress spec domain | ||
if !domainExists(ing.Spec.Domain, hstsPolicy.DomainPatterns) { | ||
hstsPolicy.DomainPatterns = append(hstsPolicy.DomainPatterns, "*."+ing.Spec.Domain) | ||
appsDomain = ing.Spec.Domain | ||
} | ||
|
||
// Update the ingress config with the new policy | ||
ing.Spec.RequiredHSTSPolicies = append(ing.Spec.RequiredHSTSPolicies, hstsPolicy) | ||
// Update the ingress config | ||
if err := kclient.Update(context.TODO(), ing); err != nil { | ||
t.Logf("failed to update ingress config: %v, retrying...", err) | ||
return false, nil | ||
} | ||
|
||
return true, nil | ||
|
||
}); err != nil { | ||
t.Fatalf("failed to update ingress config: %v", err) | ||
} | ||
defer func() { | ||
// Remove the HSTS policies from the ingress config | ||
ing.Spec.RequiredHSTSPolicies = nil | ||
if err := kclient.Update(context.TODO(), ing); err != nil { | ||
t.Fatalf("failed to restore ingress config: %v", err) | ||
} | ||
}() | ||
|
||
p := ing.Spec.RequiredHSTSPolicies[0] | ||
t.Logf("created a RequiredHSTSPolicy with DomainPatterns: %v,\n preload policy: %s,\n includeSubDomains policy: %s,\n largest age: %d,\n smallest age: %d\n", p.DomainPatterns, p.PreloadPolicy, p.IncludeSubDomainsPolicy, *p.MaxAge.LargestMaxAge, *p.MaxAge.SmallestMaxAge) | ||
|
||
// Use the same namespace for route, service, and pod | ||
ns := &corev1.Namespace{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: "hsts-policy-namespace", | ||
}, | ||
} | ||
if err := kclient.Create(context.TODO(), ns); err != nil { | ||
t.Fatalf("failed to create namespace: %v", err) | ||
} | ||
defer func() { | ||
// this will cleanup all components in this namespace | ||
if err := kclient.Delete(context.TODO(), ns); err != nil { | ||
t.Fatalf("failed to delete test namespace %s: %v", ns.Name, err) | ||
} | ||
}() | ||
|
||
// Create pod | ||
echoPod := buildEchoPod("hsts-policy-echo", ns.Name) | ||
if err := kclient.Create(context.TODO(), echoPod); err != nil { | ||
t.Fatalf("failed to create pod %s/%s: %v", ns, echoPod.Name, err) | ||
} | ||
|
||
// Create service | ||
echoService := buildEchoService(echoPod.Name, ns.Name, echoPod.ObjectMeta.Labels) | ||
if err := kclient.Create(context.TODO(), echoService); err != nil { | ||
t.Fatalf("failed to create service %s/%s: %v", echoService.Namespace, echoService.Name, err) | ||
} | ||
|
||
// Create a route that should fail the HSTS policy validation | ||
t.Logf("creating an invalid route at %s", time.Now().Format(time.StampMilli)) | ||
invalidRoute := buildRouteWithHSTS("invalid-route", echoPod.Namespace, echoService.Name, "invalid-route."+appsDomain, "max-age=99999999") | ||
if err := kclient.Create(context.TODO(), invalidRoute); err == nil { | ||
t.Fatalf("failed to reject an invalid route %s/%s, max-age 99999999", invalidRoute.Namespace, invalidRoute.Name) | ||
} else { | ||
t.Logf("rejected an invalid route at %s: %s/%s with annotation %s: %v", time.Now().Format(time.StampMilli), invalidRoute.Namespace, invalidRoute.Name, invalidRoute.Annotations, err) | ||
} | ||
|
||
// Create a route that should pass the HSTS policy validation | ||
exampleHeader := "max-age=0;preload;includesubdomains" | ||
t.Logf("creating a valid route at %s", time.Now().Format(time.StampMilli)) | ||
validRoute := buildRouteWithHSTS("valid-route", echoPod.Namespace, echoService.Name, "valid-route."+appsDomain, exampleHeader) | ||
if err := kclient.Create(context.TODO(), validRoute); err != nil { | ||
t.Fatalf("failed to create a valid route %s/%s: %v", validRoute.Namespace, validRoute.Name, err) | ||
} else { | ||
t.Logf("created a valid route at %s: %s/%s with annotation %s", time.Now().Format(time.StampMilli), validRoute.Namespace, validRoute.Name, validRoute.Annotations) | ||
} | ||
|
||
code := 0 | ||
// Wait for route service to respond, and when it does, check for the header that should be there | ||
if err := wait.PollImmediate(10*time.Second, 5*time.Minute, func() (bool, error) { | ||
resultHeader, code, _, err := getHttpResponse(t, validRoute) | ||
if code != http.StatusOK && err != nil { | ||
return false, nil | ||
} | ||
header := resultHeader.Get("strict-transport-security") | ||
if header == "" { | ||
t.Logf("request to %s got no HSTS header: %s (%d) %v, trying again....", validRoute.Spec.Host, resultHeader, code, err) | ||
return false, nil | ||
} | ||
if strings.Compare(header, exampleHeader) != 0 { | ||
t.Fatalf("request to %s got wrong HSTS header, should be %s, not %s", validRoute.Spec.Host, exampleHeader, header) | ||
} | ||
t.Logf("request to %s got correct HSTS header: %s", validRoute.Spec.Host, header) | ||
return true, nil | ||
}); err != nil { | ||
t.Fatalf("failed to find service %s for header %s in 5 minutes, http code %d: %v", validRoute.Spec.To.Name, exampleHeader, code, err) | ||
} | ||
} | ||
|
||
func domainExists(appsDomain string, patterns []string) bool { | ||
for _, domain := range patterns { | ||
if domain == appsDomain { | ||
return true | ||
} | ||
} | ||
return false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters