exec.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. /*
  2. Copyright 2018 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package exec
  14. import (
  15. "bytes"
  16. "crypto/tls"
  17. "crypto/x509"
  18. "errors"
  19. "fmt"
  20. "io"
  21. "net"
  22. "net/http"
  23. "os"
  24. "os/exec"
  25. "path/filepath"
  26. "reflect"
  27. "strings"
  28. "sync"
  29. "time"
  30. "golang.org/x/term"
  31. "k8s.io/apimachinery/pkg/runtime"
  32. "k8s.io/apimachinery/pkg/runtime/schema"
  33. "k8s.io/apimachinery/pkg/runtime/serializer"
  34. "k8s.io/apimachinery/pkg/util/dump"
  35. utilnet "k8s.io/apimachinery/pkg/util/net"
  36. "k8s.io/apimachinery/pkg/util/sets"
  37. "k8s.io/client-go/pkg/apis/clientauthentication"
  38. "k8s.io/client-go/pkg/apis/clientauthentication/install"
  39. clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
  40. clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
  41. "k8s.io/client-go/tools/clientcmd/api"
  42. "k8s.io/client-go/tools/metrics"
  43. "k8s.io/client-go/transport"
  44. "k8s.io/client-go/util/connrotation"
  45. "k8s.io/klog/v2"
  46. "k8s.io/utils/clock"
  47. )
  48. const execInfoEnv = "KUBERNETES_EXEC_INFO"
  49. const installHintVerboseHelp = `
  50. It looks like you are trying to use a client-go credential plugin that is not installed.
  51. To learn more about this feature, consult the documentation available at:
  52. https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins`
  53. var scheme = runtime.NewScheme()
  54. var codecs = serializer.NewCodecFactory(scheme)
  55. func init() {
  56. install.Install(scheme)
  57. }
  58. var (
  59. // Since transports can be constantly re-initialized by programs like kubectl,
  60. // keep a cache of initialized authenticators keyed by a hash of their config.
  61. globalCache = newCache()
  62. // The list of API versions we accept.
  63. apiVersions = map[string]schema.GroupVersion{
  64. clientauthenticationv1beta1.SchemeGroupVersion.String(): clientauthenticationv1beta1.SchemeGroupVersion,
  65. clientauthenticationv1.SchemeGroupVersion.String(): clientauthenticationv1.SchemeGroupVersion,
  66. }
  67. )
  68. func newCache() *cache {
  69. return &cache{m: make(map[string]*Authenticator)}
  70. }
  71. func cacheKey(conf *api.ExecConfig, cluster *clientauthentication.Cluster) string {
  72. key := struct {
  73. conf *api.ExecConfig
  74. cluster *clientauthentication.Cluster
  75. }{
  76. conf: conf,
  77. cluster: cluster,
  78. }
  79. return dump.Pretty(key)
  80. }
  81. type cache struct {
  82. mu sync.Mutex
  83. m map[string]*Authenticator
  84. }
  85. func (c *cache) get(s string) (*Authenticator, bool) {
  86. c.mu.Lock()
  87. defer c.mu.Unlock()
  88. a, ok := c.m[s]
  89. return a, ok
  90. }
  91. // put inserts an authenticator into the cache. If an authenticator is already
  92. // associated with the key, the first one is returned instead.
  93. func (c *cache) put(s string, a *Authenticator) *Authenticator {
  94. c.mu.Lock()
  95. defer c.mu.Unlock()
  96. existing, ok := c.m[s]
  97. if ok {
  98. return existing
  99. }
  100. c.m[s] = a
  101. return a
  102. }
  103. // sometimes rate limits how often a function f() is called. Specifically, Do()
  104. // will run the provided function f() up to threshold times every interval
  105. // duration.
  106. type sometimes struct {
  107. threshold int
  108. interval time.Duration
  109. clock clock.Clock
  110. mu sync.Mutex
  111. count int // times we have called f() in this window
  112. window time.Time // beginning of current window of length interval
  113. }
  114. func (s *sometimes) Do(f func()) {
  115. s.mu.Lock()
  116. defer s.mu.Unlock()
  117. now := s.clock.Now()
  118. if s.window.IsZero() {
  119. s.window = now
  120. }
  121. // If we are no longer in our saved time window, then we get to reset our run
  122. // count back to 0 and start increasing towards the threshold again.
  123. if inWindow := now.Sub(s.window) < s.interval; !inWindow {
  124. s.window = now
  125. s.count = 0
  126. }
  127. // If we have not run the function more than threshold times in this current
  128. // time window, we get to run it now!
  129. if underThreshold := s.count < s.threshold; underThreshold {
  130. s.count++
  131. f()
  132. }
  133. }
  134. // GetAuthenticator returns an exec-based plugin for providing client credentials.
  135. func GetAuthenticator(config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) {
  136. return newAuthenticator(globalCache, term.IsTerminal, config, cluster)
  137. }
  138. func newAuthenticator(c *cache, isTerminalFunc func(int) bool, config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) {
  139. key := cacheKey(config, cluster)
  140. if a, ok := c.get(key); ok {
  141. return a, nil
  142. }
  143. gv, ok := apiVersions[config.APIVersion]
  144. if !ok {
  145. return nil, fmt.Errorf("exec plugin: invalid apiVersion %q", config.APIVersion)
  146. }
  147. connTracker := connrotation.NewConnectionTracker()
  148. defaultDialer := connrotation.NewDialerWithTracker(
  149. (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
  150. connTracker,
  151. )
  152. if err := ValidatePluginPolicy(config.PluginPolicy); err != nil {
  153. return nil, fmt.Errorf("invalid plugin policy: %w", err)
  154. }
  155. allowlistLookup := sets.New[string]()
  156. for _, entry := range config.PluginPolicy.Allowlist {
  157. if entry.Name != "" {
  158. allowlistLookup.Insert(entry.Name)
  159. }
  160. }
  161. a := &Authenticator{
  162. // Clean is called to normalize the path to facilitate comparison with
  163. // the allowlist, when present
  164. cmd: filepath.Clean(config.Command),
  165. args: config.Args,
  166. group: gv,
  167. cluster: cluster,
  168. provideClusterInfo: config.ProvideClusterInfo,
  169. allowlistLookup: allowlistLookup,
  170. execPluginPolicy: config.PluginPolicy,
  171. installHint: config.InstallHint,
  172. sometimes: &sometimes{
  173. threshold: 10,
  174. interval: time.Hour,
  175. clock: clock.RealClock{},
  176. },
  177. stdin: os.Stdin,
  178. stderr: os.Stderr,
  179. interactiveFunc: func() (bool, error) { return isInteractive(isTerminalFunc, config) },
  180. now: time.Now,
  181. environ: os.Environ,
  182. connTracker: connTracker,
  183. }
  184. for _, env := range config.Env {
  185. a.env = append(a.env, env.Name+"="+env.Value)
  186. }
  187. // these functions are made comparable and stored in the cache so that repeated clientset
  188. // construction with the same rest.Config results in a single TLS cache and Authenticator
  189. a.getCert = &transport.GetCertHolder{GetCert: a.cert}
  190. a.dial = &transport.DialHolder{Dial: defaultDialer.DialContext}
  191. return c.put(key, a), nil
  192. }
  193. func isInteractive(isTerminalFunc func(int) bool, config *api.ExecConfig) (bool, error) {
  194. var shouldBeInteractive bool
  195. switch config.InteractiveMode {
  196. case api.NeverExecInteractiveMode:
  197. shouldBeInteractive = false
  198. case api.IfAvailableExecInteractiveMode:
  199. shouldBeInteractive = !config.StdinUnavailable && isTerminalFunc(int(os.Stdin.Fd()))
  200. case api.AlwaysExecInteractiveMode:
  201. if !isTerminalFunc(int(os.Stdin.Fd())) {
  202. return false, errors.New("standard input is not a terminal")
  203. }
  204. if config.StdinUnavailable {
  205. suffix := ""
  206. if len(config.StdinUnavailableMessage) > 0 {
  207. // only print extra ": <message>" if the user actually specified a message
  208. suffix = fmt.Sprintf(": %s", config.StdinUnavailableMessage)
  209. }
  210. return false, fmt.Errorf("standard input is unavailable%s", suffix)
  211. }
  212. shouldBeInteractive = true
  213. default:
  214. return false, fmt.Errorf("unknown interactiveMode: %q", config.InteractiveMode)
  215. }
  216. return shouldBeInteractive, nil
  217. }
  218. // Authenticator is a client credential provider that rotates credentials by executing a plugin.
  219. // The plugin input and output are defined by the API group client.authentication.k8s.io.
  220. type Authenticator struct {
  221. // Set by the config
  222. cmd string
  223. args []string
  224. group schema.GroupVersion
  225. env []string
  226. cluster *clientauthentication.Cluster
  227. provideClusterInfo bool
  228. allowlistLookup sets.Set[string]
  229. execPluginPolicy api.PluginPolicy
  230. // Used to avoid log spew by rate limiting install hint printing. We didn't do
  231. // this by interval based rate limiting alone since that way may have prevented
  232. // the install hint from showing up for kubectl users.
  233. sometimes *sometimes
  234. installHint string
  235. // Stubbable for testing
  236. stdin io.Reader
  237. stderr io.Writer
  238. interactiveFunc func() (bool, error)
  239. now func() time.Time
  240. environ func() []string
  241. // connTracker tracks all connections opened that we need to close when rotating a client certificate
  242. connTracker *connrotation.ConnectionTracker
  243. // Cached results.
  244. //
  245. // The mutex also guards calling the plugin. Since the plugin could be
  246. // interactive we want to make sure it's only called once.
  247. mu sync.Mutex
  248. cachedCreds *credentials
  249. exp time.Time
  250. // getCert makes Authenticator.cert comparable to support TLS config caching
  251. getCert *transport.GetCertHolder
  252. // dial is used for clients which do not specify a custom dialer
  253. // it is comparable to support TLS config caching
  254. dial *transport.DialHolder
  255. }
  256. type credentials struct {
  257. token string `datapolicy:"token"`
  258. cert *tls.Certificate `datapolicy:"secret-key"`
  259. }
  260. // UpdateTransportConfig updates the transport.Config to use credentials
  261. // returned by the plugin.
  262. func (a *Authenticator) UpdateTransportConfig(c *transport.Config) error {
  263. // If a bearer token is present in the request - avoid the GetCert callback when
  264. // setting up the transport, as that triggers the exec action if the server is
  265. // also configured to allow client certificates for authentication. For requests
  266. // like "kubectl get --token (token) pods" we should assume the intention is to
  267. // use the provided token for authentication. The same can be said for when the
  268. // user specifies basic auth or cert auth.
  269. if c.HasTokenAuth() || c.HasBasicAuth() || c.HasCertAuth() {
  270. return nil
  271. }
  272. c.Wrap(func(rt http.RoundTripper) http.RoundTripper {
  273. return &roundTripper{a, rt}
  274. })
  275. if c.HasCertCallback() {
  276. return errors.New("can't add TLS certificate callback: transport.Config.TLS.GetCert already set")
  277. }
  278. c.TLS.GetCertHolder = a.getCert // comparable for TLS config caching
  279. if c.DialHolder != nil {
  280. if c.DialHolder.Dial == nil {
  281. return errors.New("invalid transport.Config.DialHolder: wrapped Dial function is nil")
  282. }
  283. // if c has a custom dialer, we have to wrap it
  284. // TLS config caching is not supported for this config
  285. d := connrotation.NewDialerWithTracker(c.DialHolder.Dial, a.connTracker)
  286. c.DialHolder = &transport.DialHolder{Dial: d.DialContext}
  287. } else {
  288. c.DialHolder = a.dial // comparable for TLS config caching
  289. }
  290. return nil
  291. }
  292. var _ utilnet.RoundTripperWrapper = &roundTripper{}
  293. type roundTripper struct {
  294. a *Authenticator
  295. base http.RoundTripper
  296. }
  297. func (r *roundTripper) WrappedRoundTripper() http.RoundTripper {
  298. return r.base
  299. }
  300. func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
  301. // If a user has already set credentials, use that. This makes commands like
  302. // "kubectl get --token (token) pods" work.
  303. if req.Header.Get("Authorization") != "" {
  304. return r.base.RoundTrip(req)
  305. }
  306. creds, err := r.a.getCreds()
  307. if err != nil {
  308. return nil, fmt.Errorf("getting credentials: %v", err)
  309. }
  310. if creds.token != "" {
  311. req.Header.Set("Authorization", "Bearer "+creds.token)
  312. }
  313. res, err := r.base.RoundTrip(req)
  314. if err != nil {
  315. return nil, err
  316. }
  317. if res.StatusCode == http.StatusUnauthorized {
  318. if err := r.a.maybeRefreshCreds(creds); err != nil {
  319. klog.Errorf("refreshing credentials: %v", err)
  320. }
  321. }
  322. return res, nil
  323. }
  324. func (a *Authenticator) credsExpired() bool {
  325. if a.exp.IsZero() {
  326. return false
  327. }
  328. return a.now().After(a.exp)
  329. }
  330. func (a *Authenticator) cert() (*tls.Certificate, error) {
  331. creds, err := a.getCreds()
  332. if err != nil {
  333. return nil, err
  334. }
  335. return creds.cert, nil
  336. }
  337. func (a *Authenticator) getCreds() (*credentials, error) {
  338. a.mu.Lock()
  339. defer a.mu.Unlock()
  340. if a.cachedCreds != nil && !a.credsExpired() {
  341. return a.cachedCreds, nil
  342. }
  343. if err := a.refreshCredsLocked(); err != nil {
  344. return nil, err
  345. }
  346. return a.cachedCreds, nil
  347. }
  348. // maybeRefreshCreds executes the plugin to force a rotation of the
  349. // credentials, unless they were rotated already.
  350. func (a *Authenticator) maybeRefreshCreds(creds *credentials) error {
  351. a.mu.Lock()
  352. defer a.mu.Unlock()
  353. // Since we're not making a new pointer to a.cachedCreds in getCreds, no
  354. // need to do deep comparison.
  355. if creds != a.cachedCreds {
  356. // Credentials already rotated.
  357. return nil
  358. }
  359. return a.refreshCredsLocked()
  360. }
  361. // refreshCredsLocked executes the plugin and reads the credentials from
  362. // stdout. It must be called while holding the Authenticator's mutex.
  363. func (a *Authenticator) refreshCredsLocked() error {
  364. interactive, err := a.interactiveFunc()
  365. if err != nil {
  366. return fmt.Errorf("exec plugin cannot support interactive mode: %w", err)
  367. }
  368. cred := &clientauthentication.ExecCredential{
  369. Spec: clientauthentication.ExecCredentialSpec{
  370. Interactive: interactive,
  371. },
  372. }
  373. if a.provideClusterInfo {
  374. cred.Spec.Cluster = a.cluster
  375. }
  376. env := append(a.environ(), a.env...)
  377. data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
  378. if err != nil {
  379. return fmt.Errorf("encode ExecCredentials: %v", err)
  380. }
  381. env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data))
  382. stdout := &bytes.Buffer{}
  383. cmd := exec.Command(a.cmd, a.args...)
  384. cmd.Env = env
  385. cmd.Stderr = a.stderr
  386. cmd.Stdout = stdout
  387. if interactive {
  388. cmd.Stdin = a.stdin
  389. }
  390. err = a.updateCommandAndCheckAllowlistLocked(cmd)
  391. incrementPolicyMetric(err)
  392. if err != nil {
  393. return err
  394. }
  395. err = cmd.Run()
  396. incrementCallsMetric(err)
  397. if err != nil {
  398. return a.wrapCmdRunErrorLocked(err)
  399. }
  400. _, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred)
  401. if err != nil {
  402. return fmt.Errorf("decoding stdout: %v", err)
  403. }
  404. if gvk.Group != a.group.Group || gvk.Version != a.group.Version {
  405. return fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s",
  406. a.group, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version})
  407. }
  408. if cred.Status == nil {
  409. return fmt.Errorf("exec plugin didn't return a status field")
  410. }
  411. if cred.Status.Token == "" && cred.Status.ClientCertificateData == "" && cred.Status.ClientKeyData == "" {
  412. return fmt.Errorf("exec plugin didn't return a token or cert/key pair")
  413. }
  414. if (cred.Status.ClientCertificateData == "") != (cred.Status.ClientKeyData == "") {
  415. return fmt.Errorf("exec plugin returned only certificate or key, not both")
  416. }
  417. if cred.Status.ExpirationTimestamp != nil {
  418. a.exp = cred.Status.ExpirationTimestamp.Time
  419. } else {
  420. a.exp = time.Time{}
  421. }
  422. newCreds := &credentials{
  423. token: cred.Status.Token,
  424. }
  425. if cred.Status.ClientKeyData != "" && cred.Status.ClientCertificateData != "" {
  426. cert, err := tls.X509KeyPair([]byte(cred.Status.ClientCertificateData), []byte(cred.Status.ClientKeyData))
  427. if err != nil {
  428. return fmt.Errorf("failed parsing client key/certificate: %v", err)
  429. }
  430. // Leaf is initialized to be nil:
  431. // https://golang.org/pkg/crypto/tls/#X509KeyPair
  432. // Leaf certificate is the first certificate:
  433. // https://golang.org/pkg/crypto/tls/#Certificate
  434. // Populating leaf is useful for quickly accessing the underlying x509
  435. // certificate values.
  436. cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
  437. if err != nil {
  438. return fmt.Errorf("failed parsing client leaf certificate: %v", err)
  439. }
  440. newCreds.cert = &cert
  441. }
  442. oldCreds := a.cachedCreds
  443. a.cachedCreds = newCreds
  444. // Only close all connections when TLS cert rotates. Token rotation doesn't
  445. // need the extra noise.
  446. if oldCreds != nil && !reflect.DeepEqual(oldCreds.cert, a.cachedCreds.cert) {
  447. // Can be nil if the exec auth plugin only returned token auth.
  448. if oldCreds.cert != nil && oldCreds.cert.Leaf != nil {
  449. metrics.ClientCertRotationAge.Observe(time.Since(oldCreds.cert.Leaf.NotBefore))
  450. }
  451. a.connTracker.CloseAll()
  452. }
  453. expiry := time.Time{}
  454. if a.cachedCreds.cert != nil && a.cachedCreds.cert.Leaf != nil {
  455. expiry = a.cachedCreds.cert.Leaf.NotAfter
  456. }
  457. expirationMetrics.set(a, expiry)
  458. return nil
  459. }
  460. // wrapCmdRunErrorLocked pulls out the code to construct a helpful error message
  461. // for when the exec plugin's binary fails to Run().
  462. //
  463. // It must be called while holding the Authenticator's mutex.
  464. func (a *Authenticator) wrapCmdRunErrorLocked(err error) error {
  465. switch err.(type) {
  466. case *exec.Error: // Binary does not exist (see exec.Error).
  467. builder := strings.Builder{}
  468. fmt.Fprintf(&builder, "exec: executable %s not found", a.cmd)
  469. a.sometimes.Do(func() {
  470. fmt.Fprint(&builder, installHintVerboseHelp)
  471. if a.installHint != "" {
  472. fmt.Fprintf(&builder, "\n\n%s", a.installHint)
  473. }
  474. })
  475. return errors.New(builder.String())
  476. case *exec.ExitError: // Binary execution failed (see exec.Cmd.Run()).
  477. e := err.(*exec.ExitError)
  478. return fmt.Errorf(
  479. "exec: executable %s failed with exit code %d",
  480. a.cmd,
  481. e.ProcessState.ExitCode(),
  482. )
  483. default:
  484. return fmt.Errorf("exec: %v", err)
  485. }
  486. }
  487. // `updateCommandAndCheckAllowlistLocked` determines whether or not the specified executable may run
  488. // according to the credential plugin policy. If the plugin is allowed, `nil`
  489. // is returned. If the plugin is not allowed, an error must be returned
  490. // explaining why.
  491. func (a *Authenticator) updateCommandAndCheckAllowlistLocked(cmd *exec.Cmd) error {
  492. switch a.execPluginPolicy.PolicyType {
  493. case "", api.PluginPolicyAllowAll:
  494. return nil
  495. case api.PluginPolicyDenyAll:
  496. return fmt.Errorf("plugin %q not allowed: policy set to %q", a.cmd, api.PluginPolicyDenyAll)
  497. case api.PluginPolicyAllowlist:
  498. return a.checkAllowlistLocked(cmd)
  499. default:
  500. return fmt.Errorf("unknown plugin policy %q", a.execPluginPolicy.PolicyType)
  501. }
  502. }
  503. // `checkAllowlistLocked` checks the specified plugin against the allowlist,
  504. // and may update the Authenticator's allowlistLookup set.
  505. func (a *Authenticator) checkAllowlistLocked(cmd *exec.Cmd) error {
  506. // a.cmd is the original command as specified in the configuration, then filepath.Clean().
  507. // cmd.Path is the possibly-resolved command.
  508. // If either are an exact match in the allowlist, return success.
  509. if a.allowlistLookup.Has(a.cmd) || a.allowlistLookup.Has(cmd.Path) {
  510. return nil
  511. }
  512. var cmdResolvedPath string
  513. var cmdResolvedErr error
  514. if cmd.Path != a.cmd {
  515. // cmd.Path changed, use the already-resolved LookPath results
  516. cmdResolvedPath = cmd.Path
  517. cmdResolvedErr = cmd.Err
  518. } else {
  519. // cmd.Path is unchanged, do LookPath ourselves
  520. cmdResolvedPath, cmdResolvedErr = exec.LookPath(cmd.Path)
  521. // update cmd.Path to cmdResolvedPath so we only run the resolved path
  522. if cmdResolvedPath != "" {
  523. cmd.Path = cmdResolvedPath
  524. }
  525. }
  526. if cmdResolvedErr != nil {
  527. return fmt.Errorf("plugin path %q cannot be resolved for credential plugin allowlist check: %w", cmd.Path, cmdResolvedErr)
  528. }
  529. // cmdResolvedPath may have changed, and the changed value may be in the allowlist
  530. if a.allowlistLookup.Has(cmdResolvedPath) {
  531. return nil
  532. }
  533. // There is no verbatim match
  534. a.resolveAllowListEntriesLocked(cmd.Path)
  535. // allowlistLookup may have changed, recheck
  536. if a.allowlistLookup.Has(cmdResolvedPath) {
  537. return nil
  538. }
  539. return fmt.Errorf("plugin path %q is not permitted by the credential plugin allowlist", cmd.Path)
  540. }
  541. // resolveAllowListEntriesLocked tries to resolve allowlist entries with LookPath,
  542. // and adds successfully resolved entries to allowlistLookup.
  543. // The optional commandHint can be used to limit which entries are resolved to ones which match the hint basename.
  544. func (a *Authenticator) resolveAllowListEntriesLocked(commandHint string) {
  545. hintName := filepath.Base(commandHint)
  546. for _, entry := range a.execPluginPolicy.Allowlist {
  547. entryBasename := filepath.Base(entry.Name)
  548. if hintName != "" && hintName != entryBasename {
  549. // we got a hint, and this allowlist entry does not match it
  550. continue
  551. }
  552. entryResolvedPath, err := exec.LookPath(entry.Name)
  553. if err != nil {
  554. klog.V(5).ErrorS(err, "resolving credential plugin allowlist", "name", entry.Name)
  555. continue
  556. }
  557. if entryResolvedPath != "" {
  558. a.allowlistLookup.Insert(entryResolvedPath)
  559. }
  560. }
  561. }
  562. func ValidatePluginPolicy(policy api.PluginPolicy) error {
  563. switch policy.PolicyType {
  564. // "" is equivalent to "AllowAll"
  565. case "", api.PluginPolicyAllowAll, api.PluginPolicyDenyAll:
  566. if policy.Allowlist != nil {
  567. return fmt.Errorf("misconfigured credential plugin allowlist: plugin policy is %q but allowlist is non-nil", policy.PolicyType)
  568. }
  569. return nil
  570. case api.PluginPolicyAllowlist:
  571. return validateAllowlist(policy.Allowlist)
  572. default:
  573. return fmt.Errorf("unknown plugin policy: %q", policy.PolicyType)
  574. }
  575. }
  576. var emptyAllowlistEntry api.AllowlistEntry
  577. func validateAllowlist(list []api.AllowlistEntry) error {
  578. // This will be the case if the user has misspelled the field name for the
  579. // allowlist. Because this is a security knob, fail immediately rather than
  580. // proceed when the user has made a mistake.
  581. if list == nil {
  582. return fmt.Errorf("credential plugin policy set to %q, but allowlist is unspecified", api.PluginPolicyAllowlist)
  583. }
  584. if len(list) == 0 {
  585. return fmt.Errorf("credential plugin policy set to %q, but allowlist is empty; use %q policy instead", api.PluginPolicyAllowlist, api.PluginPolicyDenyAll)
  586. }
  587. for i, item := range list {
  588. if item == emptyAllowlistEntry {
  589. return fmt.Errorf("misconfigured credential plugin allowlist: empty allowlist entry #%d", i+1)
  590. }
  591. if cleaned := filepath.Clean(item.Name); cleaned != item.Name {
  592. return fmt.Errorf("non-normalized file path: %q vs %q", item.Name, cleaned)
  593. } else if item.Name == "" {
  594. return fmt.Errorf("empty file path: %q", item.Name)
  595. }
  596. }
  597. return nil
  598. }