Skip to content

Commit 4a5c843

Browse files
authored
Merge pull request #155 from zylxjtu/dev
Create random hostname for GMSA
2 parents 68be831 + d56c3d1 commit 4a5c843

File tree

11 files changed

+348
-13
lines changed

11 files changed

+348
-13
lines changed

.github/workflows/build.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ jobs:
113113
env:
114114
T: integration
115115
DEPLOY_METHOD: chart
116-
integration-rotation-enabled:
116+
integration-optional-features:
117117
runs-on: ubuntu-20.04
118118
steps:
119119
- uses: actions/checkout@v4
@@ -126,5 +126,5 @@ jobs:
126126
env:
127127
T: integration
128128
DEPLOY_METHOD: chart
129-
HELM_INSTALL_FLAGS_FLAGS: --set certificates.certReload.enabled=true
129+
HELM_INSTALL_FLAGS_FLAGS: --set certificates.certReload.enabled=true, --set randomHostname=true
130130

admission-webhook/integration_tests/integration_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,70 @@ func TestPossibleToUpdatePodWithNewCert(t *testing.T) {
463463
assert.Equal(t, expectedCredSpec0, extractContainerCredSpecContents(t, pod3, testName3))
464464
}
465465

466+
func TestPossibleHostnameRandomization(t *testing.T) {
467+
deployMethod := os.Getenv("DEPLOY_METHOD")
468+
if deployMethod != "chart" {
469+
t.Skip("Non chart deployment method not supported for this test")
470+
}
471+
472+
webHookNs := os.Getenv("NAMESPACE")
473+
webHookDeploymentName := os.Getenv("DEPLOYMENT_NAME")
474+
webhook, err := kubeClient(t).AppsV1().Deployments(webHookNs).Get(context.Background(), webHookDeploymentName, metav1.GetOptions{})
475+
if err != nil {
476+
t.Fatal(err)
477+
}
478+
479+
randomHostnameEnabled := false
480+
for _, envVar := range webhook.Spec.Template.Spec.Containers[0].Env {
481+
if strings.EqualFold(envVar.Name, "RANDOM_HOSTNAME") && strings.EqualFold(envVar.Value, "true") {
482+
randomHostnameEnabled = true
483+
}
484+
}
485+
486+
if randomHostnameEnabled {
487+
testName1 := "happy-path-with-hostname-randomization"
488+
credSpecTemplates1 := []string{"credspec-0"}
489+
templates1 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-with-gmsa"}
490+
491+
testConfig1, tearDownFunc1 := integrationTestSetup(t, testName1, credSpecTemplates1, templates1)
492+
defer tearDownFunc1()
493+
494+
pod := waitForPodToComeUp(t, testConfig1.Namespace, "app="+testName1)
495+
assert.NotEqual(t, testName1, pod.Spec.Hostname)
496+
assert.Equal(t, 15, len(pod.Spec.Hostname))
497+
498+
testName2 := "hostnameset-no-hostname-randomization"
499+
credSpecTemplates2 := []string{"credspec-0"}
500+
templates2 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-with-gmsa-hostname"}
501+
502+
testConfig2, tearDownFunc2 := integrationTestSetup(t, testName2, credSpecTemplates2, templates2)
503+
defer tearDownFunc2()
504+
505+
pod = waitForPodToComeUp(t, testConfig2.Namespace, "app="+testName2)
506+
assert.Equal(t, testName2, pod.Spec.Hostname)
507+
508+
testName3 := "nogmsa-hostname-randomization"
509+
credSpecTemplates3 := []string{"credspec-0"}
510+
templates3 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-without-gmsa"}
511+
512+
testConfig3, tearDownFunc3 := integrationTestSetup(t, testName3, credSpecTemplates3, templates3)
513+
defer tearDownFunc3()
514+
pod = waitForPodToComeUp(t, testConfig3.Namespace, "app="+testName3)
515+
516+
assert.Equal(t, "", pod.Spec.Hostname)
517+
} else {
518+
testName4 := "notenabled-hostname-randomization"
519+
credSpecTemplates4 := []string{"credspec-0"}
520+
templates4 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-with-gmsa"}
521+
522+
testConfig4, tearDownFunc4 := integrationTestSetup(t, testName4, credSpecTemplates4, templates4)
523+
defer tearDownFunc4()
524+
pod := waitForPodToComeUp(t, testConfig4.Namespace, "app="+testName4)
525+
526+
assert.Equal(t, "", pod.Spec.Hostname)
527+
}
528+
}
529+
466530
/* Helpers */
467531

468532
type testConfig struct {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## a simple deployment with a pod-level GMSA cred spec
2+
3+
apiVersion: apps/v1
4+
kind: Deployment
5+
metadata:
6+
labels:
7+
app: {{ .TestName }}
8+
name: {{ .TestName }}
9+
namespace: {{ .Namespace }}
10+
spec:
11+
replicas: 1
12+
selector:
13+
matchLabels:
14+
app: {{ .TestName }}
15+
template:
16+
metadata:
17+
labels:
18+
app: {{ .TestName }}
19+
spec:
20+
hostname: {{ .TestName }}
21+
serviceAccountName: {{ .ServiceAccountName }}
22+
securityContext:
23+
windowsOptions:
24+
gmsaCredentialSpecName: {{ index .CredSpecNames 0 }}
25+
containers:
26+
- image: registry.k8s.io/pause
27+
name: nginx
28+
{{- range $line := .ExtraSpecLines }}
29+
{{ $line }}
30+
{{- end }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## a simple deployment with a pod-level GMSA cred spec
2+
3+
apiVersion: apps/v1
4+
kind: Deployment
5+
metadata:
6+
labels:
7+
app: {{ .TestName }}
8+
name: {{ .TestName }}
9+
namespace: {{ .Namespace }}
10+
spec:
11+
replicas: 1
12+
selector:
13+
matchLabels:
14+
app: {{ .TestName }}
15+
template:
16+
metadata:
17+
labels:
18+
app: {{ .TestName }}
19+
spec:
20+
serviceAccountName: {{ .ServiceAccountName }}
21+
containers:
22+
- image: registry.k8s.io/pause
23+
name: nginx
24+
{{- range $line := .ExtraSpecLines }}
25+
{{ $line }}
26+
{{- end }}

admission-webhook/main.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ func main() {
2222
panic(err)
2323
}
2424

25-
webhook := newWebhookWithOptions(kubeClient, WithCertReload(*enableCertReload))
25+
randomHostname := env_bool("RANDOM_HOSTNAME")
26+
27+
options := []WebhookOption{WithCertReload(*enableCertReload)}
28+
options = append(options, WithRandomHostname(randomHostname))
29+
30+
webhook := newWebhookWithOptions(kubeClient, options...)
2631

2732
tlsConfig := &tlsConfig{
2833
crtPath: env("TLS_CRT"),
@@ -98,6 +103,20 @@ func env_float(key string, defaultFloat float32) float32 {
98103
return defaultFloat
99104
}
100105

106+
func env_bool(key string) bool {
107+
if v, found := os.LookupEnv(key); found {
108+
// Convert string to bool
109+
if boolValue, err := strconv.ParseBool(v); err == nil {
110+
return boolValue
111+
}
112+
// throw error if unable to parse
113+
panic(fmt.Errorf("unable to parse environment variable %s with value %s to bool", key, v))
114+
}
115+
116+
// return bool default value: false
117+
return false
118+
}
119+
101120
func env_int(key string, defaultInt int) int {
102121
if v, found := os.LookupEnv(key); found {
103122
if i, err := strconv.Atoi(v); err == nil {

admission-webhook/main_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"fmt"
45
"os"
56
"testing"
67
)
@@ -86,3 +87,60 @@ func Test_env_int(t *testing.T) {
8687
})
8788
}
8889
}
90+
91+
func Test_env_bool(t *testing.T) {
92+
tests := []struct {
93+
name string
94+
envkey string
95+
envval string
96+
want bool
97+
}{
98+
{
99+
name: "Environment variable set to true",
100+
envkey: "TEST_ENV_BOOL",
101+
envval: "true",
102+
want: true,
103+
},
104+
{
105+
name: "Environment variable set to false",
106+
envkey: "TEST_ENV_BOOL",
107+
envval: "false",
108+
want: false,
109+
},
110+
{
111+
name: "Environment variable not set",
112+
envkey: "TEST_ENV_BOOL",
113+
envval: "",
114+
want: false,
115+
},
116+
}
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
if tt.envval != "" {
120+
os.Setenv(tt.envkey, tt.envval)
121+
} else {
122+
os.Unsetenv(tt.envkey)
123+
}
124+
if got := env_bool(tt.envkey); got != tt.want {
125+
t.Errorf("env_bool() = %v, want %v", got, tt.want)
126+
}
127+
})
128+
}
129+
130+
envkey := "TEST_ENV_BOOL"
131+
envVal := "invalid"
132+
// Test panic
133+
defer func() {
134+
if r := recover(); r == nil {
135+
t.Errorf("The code did not panic")
136+
} else {
137+
t.Logf("Recovered from panic: %v", r)
138+
if r.(error).Error() != fmt.Sprintf("unable to parse environment variable %s with value %s to bool", envkey, envVal) {
139+
t.Errorf("Unexpected panic message: %v", r)
140+
}
141+
}
142+
}()
143+
144+
os.Setenv(envkey, envVal)
145+
env_bool("TEST_ENV_BOOL")
146+
}

admission-webhook/utils_test.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ type dummyKubeClient struct {
2222
retrieveCredSpecContentsFunc func(ctx context.Context, credSpecName string) (contents string, httpCode int, err error)
2323
}
2424

25-
2625
func (dkc *dummyKubeClient) isAuthorizedToUseCredSpec(ctx context.Context, serviceAccountName, namespace, credSpecName string) (authorized bool, reason string) {
2726
if dkc.isAuthorizedToUseCredSpecFunc != nil {
2827
return dkc.isAuthorizedToUseCredSpecFunc(ctx, serviceAccountName, namespace, credSpecName)
@@ -59,6 +58,14 @@ func setWindowsOptions(winOptions *corev1.WindowsSecurityContextOptions, credSpe
5958
// case a `*corev1.WindowsSecurityContextOptions` is built using that string as the name of the cred spec to use.
6059
// Same goes for the values of `containerNamesAndWindowsOptions`.
6160
func buildPod(serviceAccountName string, podWindowsOptions *corev1.WindowsSecurityContextOptions, containerNamesAndWindowsOptions map[string]*corev1.WindowsSecurityContextOptions) *corev1.Pod {
61+
return buildPodWithHostName(serviceAccountName, nil, podWindowsOptions, containerNamesAndWindowsOptions)
62+
}
63+
64+
// buildPod builds a pod for unit tests.
65+
// `podWindowsOptions` should be either a full `*corev1.WindowsSecurityContextOptions` or a string, in which
66+
// case a `*corev1.WindowsSecurityContextOptions` is built using that string as the name of the cred spec to use.
67+
// Same goes for the values of `containerNamesAndWindowsOptions`.
68+
func buildPodWithHostName(serviceAccountName string, hostname *string, podWindowsOptions *corev1.WindowsSecurityContextOptions, containerNamesAndWindowsOptions map[string]*corev1.WindowsSecurityContextOptions) *corev1.Pod {
6269
containers := make([]corev1.Container, len(containerNamesAndWindowsOptions))
6370
i := 0
6471
for name, winOptions := range containerNamesAndWindowsOptions {
@@ -70,10 +77,16 @@ func buildPod(serviceAccountName string, podWindowsOptions *corev1.WindowsSecuri
7077
}
7178

7279
shuffleContainers(containers)
80+
7381
podSpec := corev1.PodSpec{
7482
ServiceAccountName: serviceAccountName,
7583
Containers: containers,
7684
}
85+
86+
if hostname != nil {
87+
podSpec.Hostname = *hostname
88+
}
89+
7790
if podWindowsOptions != nil {
7891
podSpec.SecurityContext = &corev1.PodSecurityContext{WindowsOptions: podWindowsOptions}
7992
}

admission-webhook/webhook.go

+37-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"strings"
1414
"time"
1515

16+
"github.com/google/uuid"
17+
1618
"github.com/sirupsen/logrus"
1719
admissionV1 "k8s.io/api/admission/v1"
1820
corev1 "k8s.io/api/core/v1"
@@ -48,7 +50,8 @@ type podAdmissionError struct {
4850
}
4951

5052
type WebhookConfig struct {
51-
EnableCertReload bool
53+
EnableCertReload bool
54+
EnableRandomHostName bool
5255
}
5356

5457
type WebhookOption func(*WebhookConfig)
@@ -59,12 +62,18 @@ func WithCertReload(enabled bool) WebhookOption {
5962
}
6063
}
6164

65+
func WithRandomHostname(enabled bool) WebhookOption {
66+
return func(cfg *WebhookConfig) {
67+
cfg.EnableRandomHostName = enabled
68+
}
69+
}
70+
6271
func newWebhook(client kubeClientInterface) *webhook {
6372
return newWebhookWithOptions(client)
6473
}
6574

6675
func newWebhookWithOptions(client kubeClientInterface, options ...WebhookOption) *webhook {
67-
config := &WebhookConfig{EnableCertReload: false}
76+
config := &WebhookConfig{EnableCertReload: false, EnableRandomHostName: false}
6877

6978
for _, option := range options {
7079
option(config)
@@ -358,9 +367,11 @@ func compareCredSpecContents(fromResource, fromCRD string) (bool, error) {
358367
// mutateCreateRequest inlines the requested GMSA's into the pod's and containers' `WindowsSecurityOptions` structs.
359368
func (webhook *webhook) mutateCreateRequest(ctx context.Context, pod *corev1.Pod) (*admissionV1.AdmissionResponse, *podAdmissionError) {
360369
var patches []map[string]string
370+
hasGMSA := false
361371

362372
if err := iterateOverWindowsSecurityOptions(pod, func(windowsOptions *corev1.WindowsSecurityContextOptions, resourceKind gmsaResourceKind, resourceName string, containerIndex int) *podAdmissionError {
363373
if credSpecName := windowsOptions.GMSACredentialSpecName; credSpecName != nil {
374+
hasGMSA = true
364375
// if the user has pre-set the GMSA's contents, we won't override it - it'll be down
365376
// to the validation endpoint to make sure the contents actually are what they should
366377
if credSpecContents := windowsOptions.GMSACredentialSpec; credSpecContents == nil {
@@ -390,8 +401,23 @@ func (webhook *webhook) mutateCreateRequest(ctx context.Context, pod *corev1.Pod
390401
return nil, err
391402
}
392403

393-
admissionResponse := &admissionV1.AdmissionResponse{Allowed: true}
404+
if hasGMSA && webhook.config.EnableRandomHostName {
405+
// Pods are GMSA related, Env enabled, patch the hostname only if it is empty
406+
hostName := pod.Spec.Hostname
407+
if hostName == "" {
408+
hostName = generateUUID()
409+
patches = append(patches, map[string]string{
410+
"op": "add",
411+
"path": "/spec/hostname",
412+
"value": hostName,
413+
})
414+
} else {
415+
// Will honor the hostname set in the spec, print out a message
416+
logrus.Warnf("hostname is set in spec and will be hornored instead of being randomized")
417+
}
418+
}
394419

420+
admissionResponse := &admissionV1.AdmissionResponse{Allowed: true}
395421
if len(patches) != 0 {
396422
patchesBytes, err := json.Marshal(patches)
397423
if err != nil {
@@ -537,3 +563,11 @@ func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
537563
tc.SetKeepAlivePeriod(3 * time.Minute)
538564
return tc, nil
539565
}
566+
567+
func generateUUID() string {
568+
// Generate a new UUID
569+
id := uuid.New()
570+
// Convert to string and get the first 15 characters in lower case
571+
shortUUID := strings.ToLower(id.String()[:15])
572+
return shortUUID
573+
}

0 commit comments

Comments
 (0)