-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PWX-38064 Update ca.crt in the PX ServiceAccount Secret if updated #1606
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package component | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"os" | ||
|
@@ -19,7 +20,6 @@ | |
"k8s.io/apimachinery/pkg/types" | ||
"k8s.io/apimachinery/pkg/util/intstr" | ||
"k8s.io/client-go/tools/record" | ||
"k8s.io/kubernetes/pkg/apis/core" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
pxutil "github.com/libopenstorage/operator/drivers/storage/portworx/util" | ||
|
@@ -557,23 +557,28 @@ | |
return err | ||
} | ||
} | ||
needRefresh, err := isTokenRefreshRequired(secret) | ||
caCrtUpdated, err := updateCaCrtIfNeeded(secret) | ||
if err != nil { | ||
return err | ||
} | ||
if needRefresh { | ||
if err := c.refreshTokenSecret(secret, cluster, ownerRef); err != nil { | ||
needRefreshToken, err := isTokenRefreshRequired(secret) | ||
if err != nil { | ||
return err | ||
} | ||
if needRefreshToken { | ||
if err := c.refreshToken(secret, cluster); err != nil { | ||
return fmt.Errorf("failed to refresh the token secret for px container: %w", err) | ||
} | ||
} | ||
if caCrtUpdated || needRefreshToken { | ||
if err := k8sutil.CreateOrUpdateSecret(c.k8sClient, secret, ownerRef); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (c *portworxBasic) createTokenSecret(cluster *corev1.StorageCluster, ownerRef *metav1.OwnerReference) (*v1.Secret, error) { | ||
rootCaCrt, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") | ||
if err != nil && !os.IsNotExist(err) { | ||
return nil, fmt.Errorf("error reading k8s cluster certificate located inside the pod at /var/run/secrets/kubernetes.io/serviceaccount/ca.crt: %w", err) | ||
} | ||
secret := &v1.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: pxutil.PortworxServiceAccountTokenSecretName, | ||
|
@@ -582,8 +587,7 @@ | |
}, | ||
Type: v1.SecretTypeOpaque, | ||
Data: map[string][]byte{ | ||
core.ServiceAccountRootCAKey: rootCaCrt, | ||
core.ServiceAccountNamespaceKey: []byte(cluster.Namespace), | ||
v1.ServiceAccountNamespaceKey: []byte(cluster.Namespace), | ||
}, | ||
} | ||
if err := k8sutil.CreateOrUpdateSecret(c.k8sClient, secret, ownerRef); err != nil { | ||
|
@@ -592,7 +596,33 @@ | |
return secret, nil | ||
} | ||
|
||
func (c *portworxBasic) refreshTokenSecret(secret *v1.Secret, cluster *corev1.StorageCluster, ownerRef *metav1.OwnerReference) error { | ||
func updateCaCrtIfNeeded(secret *v1.Secret) (bool, error) { | ||
rootCaCrt, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") | ||
if err != nil && !os.IsNotExist(err) { | ||
return false, fmt.Errorf("error reading k8s cluster certificate located inside the pod at /var/run/secrets/kubernetes.io/serviceaccount/ca.crt: %w", err) | ||
} | ||
if len(secret.Data) == 0 || len(secret.Data[v1.ServiceAccountRootCAKey]) == 0 || !bytes.Equal(secret.Data[v1.ServiceAccountRootCAKey], rootCaCrt) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: are the len() checks really needed? bytes.Equal() should probably handle it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah the equal should be able to |
||
secret.Data[v1.ServiceAccountRootCAKey] = rootCaCrt | ||
return true, nil | ||
} | ||
return false, nil | ||
} | ||
|
||
func isTokenRefreshRequired(secret *v1.Secret) (bool, error) { | ||
if len(secret.Data) == 0 || len(secret.Data[v1.ServiceAccountTokenKey]) == 0 { | ||
return true, nil | ||
} | ||
expirationTime, err := time.Parse(time.RFC3339, string(secret.Data[PxSaTokenRefreshTimeKey])) | ||
if err != nil { | ||
return false, fmt.Errorf("error parsing expiration time: %w", err) | ||
} | ||
if time.Now().UTC().After(expirationTime) { | ||
return true, nil | ||
} | ||
return false, nil | ||
} | ||
|
||
func (c *portworxBasic) refreshToken(secret *v1.Secret, cluster *corev1.StorageCluster) error { | ||
expirationSeconds, err := getPxSaTokenExpirationSeconds(cluster) | ||
if err != nil { | ||
return err | ||
|
@@ -601,15 +631,11 @@ | |
if err != nil { | ||
return err | ||
} | ||
secret.Data[core.ServiceAccountTokenKey] = []byte(newToken.Status.Token) | ||
secret.Data[v1.ServiceAccountTokenKey] = []byte(newToken.Status.Token) | ||
// ServiceAccount token expiration time we defined might get overwritten by the maxExpirationSeconds defined by the k8s token RESTful server, | ||
// so our token refresh machanism has to honor this server limit. | ||
// https://github.com/kubernetes/kubernetes/blob/79fee524e65ddc7c1448d5d2554c6f91233cf98d/pkg/registry/core/serviceaccount/storage/token.go#L208 | ||
secret.Data[PxSaTokenRefreshTimeKey] = []byte(time.Now().UTC().Add(time.Duration(*newToken.Spec.ExpirationSeconds/2) * time.Second).Format(time.RFC3339)) | ||
err = k8sutil.CreateOrUpdateSecret(c.k8sClient, secret, ownerRef) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
|
@@ -626,20 +652,6 @@ | |
return tokenResp, nil | ||
} | ||
|
||
func isTokenRefreshRequired(secret *v1.Secret) (bool, error) { | ||
if len(secret.Data) == 0 || len(secret.Data[v1.ServiceAccountTokenKey]) == 0 { | ||
return true, nil | ||
} | ||
expirationTime, err := time.Parse(time.RFC3339, string(secret.Data[PxSaTokenRefreshTimeKey])) | ||
if err != nil { | ||
return false, fmt.Errorf("error parsing expiration time: %w", err) | ||
} | ||
if time.Now().UTC().After(expirationTime) { | ||
return true, nil | ||
} | ||
return false, nil | ||
} | ||
|
||
func (c *portworxBasic) createPortworxKVDBService( | ||
cluster *corev1.StorageCluster, | ||
ownerRef *metav1.OwnerReference, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,9 @@ import ( | |
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io/fs" | ||
"math/rand" | ||
"os" | ||
"strconv" | ||
"strings" | ||
"testing" | ||
|
@@ -1248,12 +1250,6 @@ func TestServiceAccountTokenRefreshOnExpire(t *testing.T) { | |
Name: "px-cluster", | ||
Namespace: "kube-test", | ||
}, | ||
Spec: corev1.StorageClusterSpec{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not necessary |
||
Stork: &corev1.StorkSpec{ | ||
Enabled: true, | ||
}, | ||
Image: "portworx/image:2.10.1", | ||
}, | ||
} | ||
err = driver.PreInstall(cluster) | ||
require.NoError(t, err) | ||
|
@@ -1277,6 +1273,49 @@ func TestServiceAccountTokenRefreshOnExpire(t *testing.T) { | |
assert.True(t, updatedExpirationTime.Sub(fakeExpirationTime) > 6*time.Hour) | ||
} | ||
|
||
func TestUpdateServiceAccountTokenSecretCaCrt(t *testing.T) { | ||
caCrtDir := "/var/run/secrets/kubernetes.io/serviceaccount" | ||
caCrtPath := "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" | ||
err := os.MkdirAll(caCrtDir, fs.ModePerm) | ||
require.NoError(t, err) | ||
file, err := os.Create(caCrtPath) | ||
require.NoError(t, err) | ||
file.Close() | ||
|
||
mockCtrl := gomock.NewController(t) | ||
setUpMockCoreOps(mockCtrl, fakek8sclient.NewSimpleClientset()) | ||
reregisterComponents() | ||
k8sClient := testutil.FakeK8sClient() | ||
driver := portworx{} | ||
err = driver.Init(k8sClient, runtime.NewScheme(), record.NewFakeRecorder(0)) | ||
require.NoError(t, err) | ||
|
||
cluster := &corev1.StorageCluster{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: "px-cluster", | ||
Namespace: "kube-test", | ||
}, | ||
} | ||
err = driver.PreInstall(cluster) | ||
require.NoError(t, err) | ||
|
||
saTokenSecret := &v1.Secret{} | ||
err = testutil.Get(k8sClient, saTokenSecret, pxutil.PortworxServiceAccountTokenSecretName, cluster.Namespace) | ||
require.NoError(t, err) | ||
oldCaCrt := saTokenSecret.Data[v1.ServiceAccountRootCAKey] | ||
|
||
err = os.WriteFile(caCrtPath, []byte("test"), 0644) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the test is ran in a k8s pod, this is effectively removing the root certificate, which I think is very dangerous. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about making caCrtPath a variable and allow the test to set a non-default value for this unit test? |
||
require.NoError(t, err) | ||
|
||
err = driver.PreInstall(cluster) | ||
require.NoError(t, err) | ||
err = testutil.Get(k8sClient, saTokenSecret, pxutil.PortworxServiceAccountTokenSecretName, cluster.Namespace) | ||
require.NoError(t, err) | ||
newCaCrt := saTokenSecret.Data[v1.ServiceAccountRootCAKey] | ||
require.NotEqual(t, oldCaCrt, newCaCrt) | ||
require.Equal(t, string(newCaCrt), "test") | ||
} | ||
|
||
func TestDefaultStorageClassesWithStork(t *testing.T) { | ||
mockCtrl := gomock.NewController(t) | ||
versionClient := fakek8sclient.NewSimpleClientset() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add a updateTokenIfNeeded() wrapper to be consistent with updateCaCrtIfNeeded?