@@ -17,19 +17,29 @@ limitations under the License.
17
17
package main
18
18
19
19
import (
20
+ "encoding/base64"
20
21
"encoding/json"
21
22
"errors"
23
+ "fmt"
22
24
"io"
23
25
"net/http"
24
26
"os"
27
+ "reflect"
28
+ "strings"
25
29
"time"
26
30
31
+ "gopkg.in/go-jose/go-jose.v2/jwt"
32
+
27
33
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28
34
"k8s.io/klog/v2"
29
35
credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1"
30
36
)
31
37
32
- const metadataTokenEndpoint = "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/token"
38
+ const (
39
+ metadataTokenEndpoint = "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/token"
40
+
41
+ pluginModeEnvVar = "PLUGIN_MODE"
42
+ )
33
43
34
44
func main () {
35
45
if err := getCredentials (metadataTokenEndpoint , os .Stdin , os .Stdout ); err != nil {
@@ -56,6 +66,40 @@ func getCredentials(tokenEndpoint string, r io.Reader, w io.Writer) error {
56
66
return err
57
67
}
58
68
69
+ pluginUsingServiceAccount := os .Getenv (pluginModeEnvVar ) == "serviceaccount"
70
+ if pluginUsingServiceAccount {
71
+ if len (authRequest .ServiceAccountToken ) == 0 {
72
+ return errors .New ("service account token is empty" )
73
+ }
74
+ expectedAnnotations := map [string ]string {
75
+ "domain.io/identity-id" : "123456" ,
76
+ "domain.io/identity-type" : "serviceaccount" ,
77
+ }
78
+ if ! reflect .DeepEqual (authRequest .ServiceAccountAnnotations , expectedAnnotations ) {
79
+ return fmt .Errorf ("unexpected service account annotations, want: %v, got: %v" , expectedAnnotations , authRequest .ServiceAccountAnnotations )
80
+ }
81
+ // The service account token is not actually used for authentication by this test plugin.
82
+ // We extract the claims from the token to validate the audience.
83
+ // This is solely for testing assertions and is not an actual security layer.
84
+ // Post validation in this block, we proceed with the default flow for fetching credentials.
85
+ c , err := getClaims (authRequest .ServiceAccountToken )
86
+ if err != nil {
87
+ return err
88
+ }
89
+ // The audience in the token should match the audience configured in tokenAttributes.serviceAccountTokenAudience
90
+ // in CredentialProviderConfig.
91
+ if len (c .Audience ) != 1 || c .Audience [0 ] != "test-audience" {
92
+ return fmt .Errorf ("unexpected audience: %v" , c .Audience )
93
+ }
94
+ } else {
95
+ if len (authRequest .ServiceAccountToken ) > 0 {
96
+ return errors .New ("service account token is not expected" )
97
+ }
98
+ if len (authRequest .ServiceAccountAnnotations ) > 0 {
99
+ return errors .New ("service account annotations are not expected" )
100
+ }
101
+ }
102
+
59
103
auth , err := provider .Provide (authRequest .Image )
60
104
if err != nil {
61
105
return err
@@ -70,10 +114,66 @@ func getCredentials(tokenEndpoint string, r io.Reader, w io.Writer) error {
70
114
Auth : auth ,
71
115
}
72
116
117
+ if pluginUsingServiceAccount {
118
+ response .CacheKeyType = credentialproviderv1 .GlobalPluginCacheKeyType
119
+ }
120
+
73
121
if err := json .NewEncoder (w ).Encode (response ); err != nil {
74
122
// The error from json.Marshal is intentionally not included so as to not leak credentials into the logs
75
123
return errors .New ("error marshaling response" )
76
124
}
77
125
78
126
return nil
79
127
}
128
+
129
+ // getClaims is used to extract claims from the service account token when the plugin is running in service account mode
130
+ // This is solely for testing assertions and is not an actual security layer.
131
+ // We get claims and validate the audience of the token (audience in the token matches the audience configured
132
+ // in tokenAttributes.serviceAccountTokenAudience in CredentialProviderConfig).
133
+ func getClaims (tokenData string ) (claims , error ) {
134
+ if strings .HasPrefix (strings .TrimSpace (tokenData ), "{" ) {
135
+ return claims {}, errors .New ("token is not a JWS" )
136
+ }
137
+ parts := strings .Split (tokenData , "." )
138
+ if len (parts ) != 3 {
139
+ return claims {}, errors .New ("token is not a JWS" )
140
+ }
141
+ payload , err := base64 .RawURLEncoding .DecodeString (parts [1 ])
142
+ if err != nil {
143
+ return claims {}, fmt .Errorf ("error decoding token payload: %w" , err )
144
+ }
145
+
146
+ var c claims
147
+ d := json .NewDecoder (strings .NewReader (string (payload )))
148
+ d .DisallowUnknownFields ()
149
+ if err := d .Decode (& c ); err != nil {
150
+ return claims {}, fmt .Errorf ("error decoding token payload: %w" , err )
151
+ }
152
+
153
+ return c , nil
154
+ }
155
+
156
+ type claims struct {
157
+ jwt.Claims
158
+ privateClaims
159
+ }
160
+
161
+ // copied from https://github.com/kubernetes/kubernetes/blob/60c4c2b2521fb454ce69dee737e3eb91a25e0535/pkg/serviceaccount/claims.go#L51-L67
162
+
163
+ type privateClaims struct {
164
+ Kubernetes kubernetes `json:"kubernetes.io,omitempty"`
165
+ }
166
+
167
+ type kubernetes struct {
168
+ Namespace string `json:"namespace,omitempty"`
169
+ Svcacct ref `json:"serviceaccount,omitempty"`
170
+ Pod * ref `json:"pod,omitempty"`
171
+ Secret * ref `json:"secret,omitempty"`
172
+ Node * ref `json:"node,omitempty"`
173
+ WarnAfter * jwt.NumericDate `json:"warnafter,omitempty"`
174
+ }
175
+
176
+ type ref struct {
177
+ Name string `json:"name,omitempty"`
178
+ UID string `json:"uid,omitempty"`
179
+ }
0 commit comments