Vault 3992 ToB Config and Plugins Permissions (#14817)

* updating changes from ent PR

* adding changelog

* fixing err

* fixing semgrep error
This commit is contained in:
akshya96 2022-04-04 09:45:41 -07:00 committed by GitHub
parent 9f03f86077
commit 796003ddda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 443 additions and 12 deletions

3
changelog/14817.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
core : check uid and permissions of config dir, config file, plugin dir and plugin binaries
```

View File

@ -2518,6 +2518,8 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical.
ClusterName: config.ClusterName,
CacheSize: config.CacheSize,
PluginDirectory: config.PluginDirectory,
PluginFileUid: config.PluginFileUid,
PluginFilePermissions: config.PluginFilePermissions,
EnableUI: config.EnableUI,
EnableRaw: config.EnableRawEndpoint,
DisableSealWrap: config.DisableSealWrap,
@ -2535,6 +2537,7 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical.
LicensePath: config.LicensePath,
DisableSSCTokens: config.DisableSSCTokens,
}
if c.flagDev {
coreConfig.EnableRaw = true
coreConfig.DevToken = c.flagDevRootTokenID

View File

@ -16,7 +16,9 @@ import (
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/vault/helper/osutil"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/sdk/helper/consts"
)
var entConfigValidate = func(_ *Config, _ string) []configutil.ConfigError {
@ -54,6 +56,11 @@ type Config struct {
PluginDirectory string `hcl:"plugin_directory"`
PluginFileUid int `hcl:"plugin_file_uid"`
PluginFilePermissions int `hcl:"-"`
PluginFilePermissionsRaw interface{} `hcl:"plugin_file_permissions,alias:PluginFilePermissions"`
EnableRawEndpoint bool `hcl:"-"`
EnableRawEndpointRaw interface{} `hcl:"raw_storage_endpoint,alias:EnableRawEndpoint"`
@ -127,7 +134,6 @@ telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
enable_raw_endpoint = true
storage "%s" {
@ -276,6 +282,17 @@ func (c *Config) Merge(c2 *Config) *Config {
result.PluginDirectory = c2.PluginDirectory
}
result.PluginFileUid = c.PluginFileUid
if c2.PluginFileUid != 0 {
result.PluginFileUid = c2.PluginFileUid
}
result.PluginFilePermissions = c.PluginFilePermissions
if c2.PluginFilePermissionsRaw != nil {
result.PluginFilePermissions = c2.PluginFilePermissions
result.PluginFilePermissionsRaw = c2.PluginFilePermissionsRaw
}
result.DisablePerformanceStandby = c.DisablePerformanceStandby
if c2.DisablePerformanceStandby {
result.DisablePerformanceStandby = c2.DisablePerformanceStandby
@ -350,6 +367,13 @@ func LoadConfig(path string) (*Config, error) {
}
if fi.IsDir() {
// check permissions on the config directory
if os.Getenv(consts.VaultDisableFilePermissionsCheckEnv) != "true" {
err = osutil.OwnerPermissionsMatch(path, 0, 0)
if err != nil {
return nil, err
}
}
return CheckConfig(LoadConfigDir(path))
}
return CheckConfig(LoadConfigFile(path))
@ -385,6 +409,21 @@ func LoadConfigFile(path string) (*Config, error) {
return nil, err
}
if os.Getenv(consts.VaultDisableFilePermissionsCheckEnv) != "true" {
// check permissions of the config file
err = osutil.OwnerPermissionsMatch(path, 0, 0)
if err != nil {
return nil, err
}
// check permissions of the plugin directory
if conf.PluginDirectory != "" {
err = osutil.OwnerPermissionsMatch(conf.PluginDirectory, conf.PluginFileUid, conf.PluginFilePermissions)
if err != nil {
return nil, err
}
}
}
return conf, nil
}
@ -459,6 +498,18 @@ func ParseConfig(d, source string) (*Config, error) {
}
}
if result.PluginFilePermissionsRaw != nil {
octalPermissionsString, err := parseutil.ParseString(result.PluginFilePermissionsRaw)
if err != nil {
return nil, err
}
pluginFilePermissions, err := strconv.ParseInt(octalPermissionsString, 8, 64)
if err != nil {
return nil, err
}
result.PluginFilePermissions = int(pluginFilePermissions)
}
if result.DisableSentinelTraceRaw != nil {
if result.DisableSentinelTrace, err = parseutil.ParseBool(result.DisableSentinelTraceRaw); err != nil {
return nil, err
@ -838,6 +889,10 @@ func (c *Config) Sanitized() map[string]interface{} {
"plugin_directory": c.PluginDirectory,
"plugin_file_uid": c.PluginFileUid,
"plugin_file_permissions": c.PluginFilePermissions,
"raw_storage_endpoint": c.EnableRawEndpoint,
"api_addr": c.APIAddr,

View File

@ -694,6 +694,8 @@ func testConfig_Sanitized(t *testing.T) {
"disable_indexing": false,
"disable_mlock": true,
"disable_performance_standby": false,
"plugin_file_uid": 0,
"plugin_file_permissions": 0,
"disable_printable_check": false,
"disable_sealwrap": true,
"raw_storage_endpoint": true,
@ -855,6 +857,7 @@ func testParseSockaddrTemplate(t *testing.T) {
api_addr = <<EOF
{{- GetAllInterfaces | include "flags" "loopback" | include "type" "ipv4" | attr "address" -}}
EOF
listener "tcp" {
address = <<EOF
{{- GetAllInterfaces | include "flags" "loopback" | include "type" "ipv4" | attr "address" -}}:443

66
helper/osutil/fileinfo.go Normal file
View File

@ -0,0 +1,66 @@
package osutil
import (
"fmt"
"io/fs"
"os"
)
func IsWriteGroup(mode os.FileMode) bool {
return mode&0o20 != 0
}
func IsWriteOther(mode os.FileMode) bool {
return mode&0o02 != 0
}
func checkPathInfo(info fs.FileInfo, path string, uid int, permissions int) error {
err := FileUidMatch(info, path, uid)
if err != nil {
return err
}
err = FilePermissionsMatch(info, path, permissions)
if err != nil {
return err
}
return nil
}
func FilePermissionsMatch(info fs.FileInfo, path string, permissions int) error {
if permissions != 0 && int(info.Mode().Perm()) != permissions {
return fmt.Errorf("path %q does not have permissions %o", path, permissions)
}
if permissions == 0 && (IsWriteOther(info.Mode()) || IsWriteGroup(info.Mode())) {
return fmt.Errorf("path %q has insecure permissions %o. Vault expects no write permissions for group or others", path, info.Mode().Perm())
}
return nil
}
// OwnerPermissionsMatch checks if vault user is the owner and permissions are secure for input path
func OwnerPermissionsMatch(path string, uid int, permissions int) error {
if path == "" {
return fmt.Errorf("could not verify permissions for path. No path provided ")
}
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("error stating %q: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 {
symLinkInfo, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("error stating %q: %w", path, err)
}
err = checkPathInfo(symLinkInfo, path, uid, permissions)
if err != nil {
return err
}
}
err = checkPathInfo(info, path, uid, permissions)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,84 @@
package osutil
import (
"io/fs"
"os"
"os/user"
"runtime"
"strconv"
"testing"
)
func TestCheckPathInfo(t *testing.T) {
currentUser, err := user.Current()
if err != nil {
t.Errorf("failed to get details of current process owner. The error is: %v", err)
}
uid, err := strconv.ParseInt(currentUser.Uid, 0, 64)
if err != nil {
t.Errorf("failed to convert uid to int64. The error is: %v", err)
}
uid2, err := strconv.ParseInt(currentUser.Uid+"1", 0, 64)
if err != nil {
t.Errorf("failed to convert uid to int64. The error is: %v", err)
}
testCases := []struct {
uid int
filepermissions fs.FileMode
permissions int
expectError bool
}{
{
uid: 0,
filepermissions: 0o700,
permissions: 0,
expectError: false,
},
{
uid: int(uid2),
filepermissions: 0o700,
permissions: 0,
expectError: true,
},
{
uid: int(uid),
filepermissions: 0o700,
permissions: 0,
expectError: false,
},
{
uid: 0,
filepermissions: 0o777,
permissions: 744,
expectError: true,
},
}
for _, tc := range testCases {
err := os.Mkdir("testFile", tc.filepermissions)
if err != nil {
t.Fatal(err)
}
info, err := os.Stat("testFile")
if err != nil {
t.Errorf("error stating %q: %v", "testFile", err)
}
if tc.uid != 0 && runtime.GOOS == "windows" && tc.expectError == true {
t.Skip("Skipping test in windows environment as no error will be returned in this case")
}
err = checkPathInfo(info, "testFile", tc.uid, int(tc.permissions))
if tc.expectError && err == nil {
t.Errorf("invalid result. expected error")
}
if !tc.expectError && err != nil {
t.Errorf(err.Error())
}
err = os.RemoveAll("testFile")
if err != nil {
t.Fatal(err)
}
}
}

View File

@ -0,0 +1,53 @@
//go:build !windows
package osutil
import (
"fmt"
"io/fs"
"os/user"
"strconv"
"syscall"
)
func FileUIDEqual(info fs.FileInfo, uid int) bool {
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
path_uid := int(stat.Uid)
if path_uid == uid {
return true
}
}
return false
}
func FileGIDEqual(info fs.FileInfo, gid int) bool {
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
path_gid := int(stat.Gid)
if path_gid == gid {
return true
}
}
return false
}
func FileUidMatch(info fs.FileInfo, path string, uid int) (err error) {
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get details of current process owner. The error is: %w", err)
}
switch uid {
case 0:
currentUserUid, err := strconv.Atoi(currentUser.Uid)
if err != nil {
return fmt.Errorf("failed to convert uid %q to int. The error is: %w", currentUser.Uid, err)
}
if !FileUIDEqual(info, currentUserUid) {
return fmt.Errorf("path %q is not owned by my uid %s", path, currentUser.Uid)
}
default:
if !FileUIDEqual(info, uid) {
return fmt.Errorf("path %q is not owned by uid %d", path, uid)
}
}
return err
}

View File

@ -0,0 +1,100 @@
//go:build !windows
package osutil
import (
"os"
"os/user"
"strconv"
"testing"
)
func TestFileUIDEqual(t *testing.T) {
currentUser, err := user.Current()
if err != nil {
t.Errorf("failed to get details of current process owner. The error is: %v", err)
}
uid, err := strconv.Atoi(currentUser.Uid)
if err != nil {
t.Errorf("failed to convert uid to int. The error is: %v", err)
}
testCases := []struct {
uid int
expected bool
}{
{
uid: uid,
expected: true,
},
{
uid: uid + 1,
expected: false,
},
}
for _, tc := range testCases {
err := os.Mkdir("testFile", 0o777)
if err != nil {
t.Fatal(err)
}
info, err := os.Stat("testFile")
if err != nil {
t.Errorf("error stating %q: %v", "testFile", err)
}
result := FileUIDEqual(info, tc.uid)
if result != tc.expected {
t.Errorf("invalid result. expected %t for uid %v", tc.expected, tc.uid)
}
err = os.RemoveAll("testFile")
if err != nil {
t.Fatal(err)
}
}
}
func TestFileGIDEqual(t *testing.T) {
currentUser, err := user.Current()
if err != nil {
t.Errorf("failed to get details of current process owner. The error is: %v", err)
}
gid, err := strconv.Atoi(currentUser.Gid)
if err != nil {
t.Errorf("failed to convert gid to int. The error is: %v", err)
}
testCases := []struct {
gid int
expected bool
}{
{
gid: gid,
expected: true,
},
{
gid: gid + 1,
expected: false,
},
}
for _, tc := range testCases {
err := os.Mkdir("testFile", 0o777)
if err != nil {
t.Fatal(err)
}
info, err := os.Stat("testFile")
if err != nil {
t.Errorf("error stating %q: %v", "testFile", err)
}
result := FileGIDEqual(info, tc.gid)
if result != tc.expected {
t.Errorf("invalid result. expected %t for gid %v", tc.expected, tc.gid)
}
err = os.RemoveAll("testFile")
if err != nil {
t.Fatal(err)
}
}
}

View File

@ -0,0 +1,11 @@
//go:build windows
package osutil
import (
"io/fs"
)
func FileUidMatch(info fs.FileInfo, path string, uid int) error {
return nil
}

View File

@ -31,6 +31,7 @@ func getPluginClusterAndCore(t testing.TB, logger log.Logger) (*vault.TestCluste
if err != nil {
t.Fatal(err)
}
os.Setenv(consts.VaultDisableFilePermissionsCheckEnv, "true")
coreConfig := &vault.CoreConfig{
Physical: inm,

View File

@ -46,6 +46,8 @@ func TestSysConfigState_Sanitized(t *testing.T) {
"max_lease_ttl": json.Number("0"),
"pid_file": "",
"plugin_directory": "",
"plugin_file_uid": json.Number("0"),
"plugin_file_permissions": json.Number("0"),
"enable_response_header_hostname": false,
"enable_response_header_raft_node_id": false,
"log_requests_level": "",

View File

@ -32,4 +32,6 @@ const (
// ReplicationResolverALPN is the negotiated protocol used for
// resolving replicaiton addresses
ReplicationResolverALPN = "replication_resolver_v1"
VaultDisableFilePermissionsCheckEnv = "VAULT_DISABLE_FILE_PERMISSIONS_CHECK"
)

View File

@ -915,7 +915,7 @@ func (c *Core) newCredentialBackend(ctx context.Context, entry *MountEntry, sysV
f, ok := c.credentialBackends[t]
if !ok {
f = plugin.Factory
f = wrapFactoryCheckPerms(c, plugin.Factory)
}
// Set up conf to pass in plugin_name

View File

@ -37,6 +37,7 @@ import (
"github.com/hashicorp/vault/command/server"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/osutil"
"github.com/hashicorp/vault/physical/raft"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/consts"
@ -487,6 +488,12 @@ type Core struct {
// pluginDirectory is the location vault will look for plugin binaries
pluginDirectory string
// pluginFileUid is the uid of the plugin files and directory
pluginFileUid int
// pluginFilePermissions is the permissions of the plugin files and directory
pluginFilePermissions int
// pluginCatalog is used to manage plugin configurations
pluginCatalog *PluginCatalog
@ -684,6 +691,10 @@ type CoreConfig struct {
PluginDirectory string
PluginFileUid int
PluginFilePermissions int
DisableSealWrap bool
RawConfig *server.Config
@ -998,6 +1009,13 @@ func NewCore(conf *CoreConfig) (*Core, error) {
}
}
if conf.PluginFileUid != 0 {
c.pluginFileUid = conf.PluginFileUid
}
if conf.PluginFilePermissions != 0 {
c.pluginFilePermissions = conf.PluginFilePermissions
}
createSecondaries(c, conf)
if conf.HAPhysical != nil && conf.HAPhysical.HAEnabled() {
@ -3201,3 +3219,18 @@ func (c *Core) GetHAPeerNodesCached() []PeerNode {
}
return nodes
}
func (c *Core) CheckPluginPerms(pluginName string) (err error) {
if c.pluginDirectory != "" && os.Getenv(consts.VaultDisableFilePermissionsCheckEnv) != "true" {
err = osutil.OwnerPermissionsMatch(c.pluginDirectory, c.pluginFileUid, c.pluginFilePermissions)
if err != nil {
return err
}
fullPath := filepath.Join(c.pluginDirectory, pluginName)
err = osutil.OwnerPermissionsMatch(fullPath, c.pluginFileUid, c.pluginFilePermissions)
if err != nil {
return err
}
}
return err
}

View File

@ -4,17 +4,17 @@ package diagnose
import (
"io/fs"
"syscall"
"github.com/hashicorp/vault/helper/osutil"
)
// IsOwnedByRoot checks if a file is owned by root
func IsOwnedByRoot(info fs.FileInfo) bool {
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid := int(stat.Uid)
gid := int(stat.Gid)
if uid == 0 && gid == 0 {
return true
}
if !osutil.FileUIDEqual(info, 0) {
return false
}
return false
if !osutil.FileGIDEqual(info, 0) {
return false
}
return true
}

View File

@ -450,6 +450,10 @@ func (b *SystemBackend) handlePluginCatalogUpdate(ctx context.Context, req *logi
return logical.ErrorResponse("missing command value"), nil
}
if err = b.Core.CheckPluginPerms(command); err != nil {
return nil, err
}
// For backwards compatibility, also accept args as part of command. Don't
// accepts args in both command and args.
args := d.Get("args").([]string)

View File

@ -530,6 +530,8 @@ func testSystemBackendMock(t *testing.T, numCores, numMounts int, backendType lo
},
}
os.Setenv(consts.VaultDisableFilePermissionsCheckEnv, "true")
// Create a tempdir, cluster.Cleanup will clean up this directory
tempDir, err := ioutil.TempDir("", "vault-test-cluster")
if err != nil {
@ -602,7 +604,7 @@ func testSystemBackend_SingleCluster_Env(t *testing.T, env []string) *vault.Test
"test": plugin.Factory,
},
}
os.Setenv(consts.VaultDisableFilePermissionsCheckEnv, "true")
// Create a tempdir, cluster.Cleanup will clean up this directory
tempDir, err := ioutil.TempDir("", "vault-test-cluster")
if err != nil {

View File

@ -1395,7 +1395,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView
f, ok := c.logicalBackends[t]
if !ok {
f = plugin.Factory
f = wrapFactoryCheckPerms(c, plugin.Factory)
}
// Set up conf to pass in plugin_name

View File

@ -81,6 +81,15 @@ type pluginClient struct {
plugin.ClientProtocol
}
func wrapFactoryCheckPerms(core *Core, f logical.Factory) logical.Factory {
return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
if err := core.CheckPluginPerms(conf.Config["plugin_name"]); err != nil {
return nil, err
}
return f(ctx, conf)
}
}
func (c *Core) setupPluginCatalog(ctx context.Context) error {
c.pluginCatalog = &PluginCatalog{
builtinRegistry: c.builtinRegistry,