Skip to content
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

Added SecretFileMode for specifying mounted secrets file permissions. #7908

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package api

import (
"os"

"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
Expand Down Expand Up @@ -450,6 +452,16 @@ type GitRepoVolumeSource struct {
type SecretVolumeSource struct {
// Name of the secret in the pod's namespace to use
SecretName string `json:"secretName"`
// Modes describes access permissions to be applied per Secret's data key while mounting
Modes []SecretFileMode `json:"modes"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this really need to be per-key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to mount each data's value with different permissions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create multiple secrets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we provide users with option to put multiple data we should allow them to define different permissions as well. On the other hand I can think of that as a grouping factor, where you'd group kind of "similar" data into one secret. Before doing any changes I'll wait for the outcome from #7925 @erictune mentioned.

}

// SecretFileMode sets up access permissions per Secret's data key
type SecretFileMode struct {
// Name is the name of the Secret's data key
Name string `json:"name"`
// Mode is os.FileMode to be applied
Mode os.FileMode `json:"mode"`
}

// NFSVolumeSource represents an NFS Mount that lasts the lifetime of a pod
Expand Down
16 changes: 16 additions & 0 deletions pkg/api/v1beta1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -1574,10 +1574,26 @@ func init() {
},
func(in *newer.SecretVolumeSource, out *SecretVolumeSource, s conversion.Scope) error {
out.Target.ID = in.SecretName
if in.Modes != nil {
out.Modes = make([]SecretFileMode, len(in.Modes))
for i := range in.Modes {
if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil {
return err
}
}
}
return nil
},
func(in *SecretVolumeSource, out *newer.SecretVolumeSource, s conversion.Scope) error {
out.SecretName = in.Target.ID
if in.Modes != nil {
out.Modes = make([]newer.SecretFileMode, len(in.Modes))
for i := range in.Modes {
if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil {
return err
}
}
}
return nil
},
)
Expand Down
12 changes: 12 additions & 0 deletions pkg/api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package v1beta1

import (
"os"

"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
Expand Down Expand Up @@ -363,6 +365,16 @@ type SecretVolumeSource struct {
// Reference to a Secret to use. Only the ID field of this reference is used; a
// secret can only be used by pods in its namespace.
Target ObjectReference `json:"target" description:"target is a reference to a secret"`
// Modes describes access permissions to be applied per Secret's data key while mounting
Modes []SecretFileMode `json:"modes"`
}

// SecretFileMode sets up access permissions per Secret's data key
type SecretFileMode struct {
// Name is the name of the Secret's data key
Name string `json:"name"`
// Mode is os.FileMode to be applied
Mode os.FileMode `json:"mode"`
}

// ContainerPort represents a network port in a single container
Expand Down
16 changes: 16 additions & 0 deletions pkg/api/v1beta2/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -1490,10 +1490,26 @@ func init() {
},
func(in *newer.SecretVolumeSource, out *SecretVolumeSource, s conversion.Scope) error {
out.Target.ID = in.SecretName
if in.Modes != nil {
out.Modes = make([]SecretFileMode, len(in.Modes))
for i := range in.Modes {
if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil {
return err
}
}
}
return nil
},
func(in *SecretVolumeSource, out *newer.SecretVolumeSource, s conversion.Scope) error {
out.SecretName = in.Target.ID
if in.Modes != nil {
out.Modes = make([]newer.SecretFileMode, len(in.Modes))
for i := range in.Modes {
if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil {
return err
}
}
}
return nil
},
)
Expand Down
12 changes: 12 additions & 0 deletions pkg/api/v1beta2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package v1beta2

import (
"os"

"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
Expand Down Expand Up @@ -249,6 +251,16 @@ type SecretVolumeSource struct {
// Reference to a Secret to use. Only the ID field of this reference is used; a
// secret can only be used by pods in its namespace.
Target ObjectReference `json:"target" description:"target is a reference to a secret"`
// Modes describes access permissions to be applied per Secret's data key while mounting
Modes []SecretFileMode `json:"modes"`
}

// SecretFileMode sets up access permissions per Secret's data key
type SecretFileMode struct {
// Name is the name of the Secret's data key
Name string `json:"name"`
// Mode is os.FileMode to be applied
Mode os.FileMode `json:"mode"`
}

// Protocol defines network protocols supported for things like conatiner ports.
Expand Down
40 changes: 40 additions & 0 deletions pkg/api/v1beta3/conversion_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -3377,6 +3377,24 @@ func convert_api_Secret_To_v1beta3_Secret(in *newer.Secret, out *Secret, s conve
return nil
}

func convert_v1beta3_SecretFileMode_To_api_SecretFileMode(in *SecretFileMode, out *newer.SecretFileMode, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*SecretFileMode))(in)
}
out.Name = in.Name
out.Mode = in.Mode
return nil
}

func convert_api_SecretFileMode_To_v1beta3_SecretFileMode(in *newer.SecretFileMode, out *SecretFileMode, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*newer.SecretFileMode))(in)
}
out.Name = in.Name
out.Mode = in.Mode
return nil
}

func convert_v1beta3_SecretList_To_api_SecretList(in *SecretList, out *newer.SecretList, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*SecretList))(in)
Expand Down Expand Up @@ -3428,6 +3446,16 @@ func convert_v1beta3_SecretVolumeSource_To_api_SecretVolumeSource(in *SecretVolu
defaulting.(func(*SecretVolumeSource))(in)
}
out.SecretName = in.SecretName
if in.Modes != nil {
out.Modes = make([]newer.SecretFileMode, len(in.Modes))
for i := range in.Modes {
if err := convert_v1beta3_SecretFileMode_To_api_SecretFileMode(&in.Modes[i], &out.Modes[i], s); err != nil {
return err
}
}
} else {
out.Modes = nil
}
return nil
}

Expand All @@ -3436,6 +3464,16 @@ func convert_api_SecretVolumeSource_To_v1beta3_SecretVolumeSource(in *newer.Secr
defaulting.(func(*newer.SecretVolumeSource))(in)
}
out.SecretName = in.SecretName
if in.Modes != nil {
out.Modes = make([]SecretFileMode, len(in.Modes))
for i := range in.Modes {
if err := convert_api_SecretFileMode_To_v1beta3_SecretFileMode(&in.Modes[i], &out.Modes[i], s); err != nil {
return err
}
}
} else {
out.Modes = nil
}
return nil
}

Expand Down Expand Up @@ -4186,6 +4224,7 @@ func init() {
convert_api_ResourceQuota_To_v1beta3_ResourceQuota,
convert_api_ResourceRequirements_To_v1beta3_ResourceRequirements,
convert_api_SELinuxOptions_To_v1beta3_SELinuxOptions,
convert_api_SecretFileMode_To_v1beta3_SecretFileMode,
convert_api_SecretList_To_v1beta3_SecretList,
convert_api_SecretVolumeSource_To_v1beta3_SecretVolumeSource,
convert_api_Secret_To_v1beta3_Secret,
Expand Down Expand Up @@ -4291,6 +4330,7 @@ func init() {
convert_v1beta3_ResourceQuota_To_api_ResourceQuota,
convert_v1beta3_ResourceRequirements_To_api_ResourceRequirements,
convert_v1beta3_SELinuxOptions_To_api_SELinuxOptions,
convert_v1beta3_SecretFileMode_To_api_SecretFileMode,
convert_v1beta3_SecretList_To_api_SecretList,
convert_v1beta3_SecretVolumeSource_To_api_SecretVolumeSource,
convert_v1beta3_Secret_To_api_Secret,
Expand Down
12 changes: 12 additions & 0 deletions pkg/api/v1beta3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package v1beta3

import (
"os"

"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
Expand Down Expand Up @@ -455,6 +457,16 @@ type GitRepoVolumeSource struct {
type SecretVolumeSource struct {
// Name of the secret in the pod's namespace to use
SecretName string `json:"secretName" description:"secretName is the name of a secret in the pod's namespace"`
// Modes describes access permissions to be applied per Secret's data key while mounting
Modes []SecretFileMode `json:"modes"`
}

// SecretFileMode sets up access permissions per Secret's data key
type SecretFileMode struct {
// Name is the name of the Secret's data key
Name string `json:"name"`
// Mode is os.FileMode to be applied
Mode os.FileMode `json:"mode"`
}

// NFSVolumeSource represents an NFS mount that lasts the lifetime of a pod
Expand Down
5 changes: 5 additions & 0 deletions pkg/api/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,11 @@ func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) errs.Valid
if secretSource.SecretName == "" {
allErrs = append(allErrs, errs.NewFieldRequired("secretName"))
}
for _, mode := range secretSource.Modes {
if mode.Mode|0444 != 0444 {
allErrs = append(allErrs, errs.NewFieldForbidden("modes.mode", "Not allowed mode set"))
}
}
return allErrs
}

Expand Down
4 changes: 3 additions & 1 deletion pkg/api/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func TestValidateVolumes(t *testing.T) {
{Name: "awsebs", VolumeSource: api.VolumeSource{AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{"my-PD", "ext4", 1, false}}},
{Name: "gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{"my-repo", "hashstring"}}},
{Name: "iscsidisk", VolumeSource: api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{"127.0.0.1", "iqn.2015-02.example.com:test", 1, "ext4", false}}},
{Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret"}}},
{Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret", []api.SecretFileMode{}}}},
{Name: "glusterfs", VolumeSource: api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{"host1", "path", false}}},
}
names, errs := validateVolumes(successCase)
Expand All @@ -534,6 +534,7 @@ func TestValidateVolumes(t *testing.T) {
emptyIQN := api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{"127.0.0.1", "", 1, "ext4", false}}
emptyHosts := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{"", "path", false}}
emptyPath := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{"host", "", false}}
wrongFileMode := api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret", []api.SecretFileMode{{"data", 0700}}}}
errorCases := map[string]struct {
V []api.Volume
T errors.ValidationErrorType
Expand All @@ -547,6 +548,7 @@ func TestValidateVolumes(t *testing.T) {
"empty iqn": {[]api.Volume{{Name: "badiqn", VolumeSource: emptyIQN}}, errors.ValidationErrorTypeRequired, "[0].source.iscsi.iqn"},
"empty hosts": {[]api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.endpoints"},
"empty path": {[]api.Volume{{Name: "badpath", VolumeSource: emptyPath}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.path"},
"wrong fileMode": {[]api.Volume{{Name: "badfilemode", VolumeSource: wrongFileMode}}, errors.ValidationErrorTypeForbidden, "[0].source.secret.modes.mode"},
}
for k, v := range errorCases {
_, errs := validateVolumes(v.V)
Expand Down
36 changes: 22 additions & 14 deletions pkg/volume/secret/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package secret
import (
"fmt"
"io/ioutil"
"os"
"path"

"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
Expand Down Expand Up @@ -61,26 +62,26 @@ func (plugin *secretPlugin) NewBuilder(spec *volume.Spec, podRef *api.ObjectRefe
}

func (plugin *secretPlugin) newBuilderInternal(spec *volume.Spec, podRef *api.ObjectReference, opts volume.VolumeOptions, mounter mount.Interface) (volume.Builder, error) {
return &secretVolume{spec.Name, *podRef, plugin, spec.VolumeSource.Secret.SecretName, &opts, mounter}, nil
return &secretVolume{spec.Name, *podRef, plugin, spec.VolumeSource.Secret, &opts, mounter}, nil
}

func (plugin *secretPlugin) NewCleaner(volName string, podUID types.UID, mounter mount.Interface) (volume.Cleaner, error) {
return plugin.newCleanerInternal(volName, podUID, mounter)
}

func (plugin *secretPlugin) newCleanerInternal(volName string, podUID types.UID, mounter mount.Interface) (volume.Cleaner, error) {
return &secretVolume{volName, api.ObjectReference{UID: podUID}, plugin, "", nil, mounter}, nil
return &secretVolume{volName, api.ObjectReference{UID: podUID}, plugin, nil, nil, mounter}, nil
}

// secretVolume handles retrieving secrets from the API server
// and placing them into the volume on the host.
type secretVolume struct {
volName string
podRef api.ObjectReference
plugin *secretPlugin
secretName string
opts *volume.VolumeOptions
mounter mount.Interface
volName string
podRef api.ObjectReference
plugin *secretPlugin
secret *api.SecretVolumeSource
opts *volume.VolumeOptions
mounter mount.Interface
}

func (sv *secretVolume) SetUp() error {
Expand Down Expand Up @@ -114,24 +115,31 @@ func (sv *secretVolume) SetUpAt(dir string) error {
return fmt.Errorf("Cannot setup secret volume %v because kube client is not configured", sv)
}

secret, err := kubeClient.Secrets(sv.podRef.Namespace).Get(sv.secretName)
secret, err := kubeClient.Secrets(sv.podRef.Namespace).Get(sv.secret.SecretName)
if err != nil {
glog.Errorf("Couldn't get secret %v/%v", sv.podRef.Namespace, sv.secretName)
glog.Errorf("Couldn't get secret %v/%v", sv.podRef.Namespace, sv.secret.SecretName)
return err
} else {
totalBytes := totalSecretBytes(secret)
glog.V(3).Infof("Received secret %v/%v containing (%v) pieces of data, %v total bytes",
sv.podRef.Namespace,
sv.secretName,
sv.secret.SecretName,
len(secret.Data),
totalBytes)
}

customFileModes := make(map[string]os.FileMode)
for _, mode := range sv.secret.Modes {
customFileModes[mode.Name] = mode.Mode
}
for name, data := range secret.Data {
hostFilePath := path.Join(dir, name)
glog.V(3).Infof("Writing secret data %v/%v/%v (%v bytes) to host file %v", sv.podRef.Namespace, sv.secretName, name, len(data), hostFilePath)
err := ioutil.WriteFile(hostFilePath, data, 0444)
if err != nil {
glog.V(3).Infof("Writing secret data %v/%v/%v (%v bytes) to host file %v", sv.podRef.Namespace, sv.secret.SecretName, name, len(data), hostFilePath)
var fileMode os.FileMode = 0444
if customFileMode, ok := customFileModes[name]; ok {
fileMode = customFileMode
}
if err := ioutil.WriteFile(hostFilePath, data, fileMode); err != nil {
glog.Errorf("Error writing secret data to host path: %v, %v", hostFilePath, err)
return err
}
Expand Down
15 changes: 14 additions & 1 deletion pkg/volume/secret/secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ func TestPlugin(t *testing.T) {
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: testName,
Modes: []api.SecretFileMode{
{"data-1", 0400},
{"data-2", 0404},
},
},
},
}
Expand Down Expand Up @@ -124,7 +128,7 @@ func TestPlugin(t *testing.T) {

for key, value := range secret.Data {
secretDataHostPath := path.Join(volumePath, key)
if _, err := os.Stat(secretDataHostPath); err != nil {
if fileInfo, err := os.Stat(secretDataHostPath); err != nil {
t.Fatalf("SetUp() failed, couldn't find secret data on disk: %v", secretDataHostPath)
} else {
actualSecretBytes, err := ioutil.ReadFile(secretDataHostPath)
Expand All @@ -136,6 +140,15 @@ func TestPlugin(t *testing.T) {
if string(value) != actualSecretValue {
t.Errorf("Unexpected value; expected %q, got %q", value, actualSecretValue)
}
var expectedFileMode os.FileMode = 0444
for _, customFileMode := range volumeSpec.VolumeSource.Secret.Modes {
if customFileMode.Name == key {
expectedFileMode = customFileMode.Mode
}
}
if fileInfo.Mode() != expectedFileMode {
t.Errorf("Unexpected secret access permissions; expected %v, got %v", expectedFileMode, fileInfo.Mode())
}
}
}

Expand Down