Skip to content

Commit 7da0995

Browse files
adds support for kubernetes mounted private keys
1 parent 006a3f4 commit 7da0995

File tree

3 files changed

+188
-9
lines changed

3 files changed

+188
-9
lines changed

conn.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ import (
2828

2929
// Common error types
3030
var (
31-
ErrNotSupported = errors.New("pq: Unsupported command")
32-
ErrInFailedTransaction = errors.New("pq: Could not complete operation in a failed transaction")
33-
ErrSSLNotSupported = errors.New("pq: SSL is not enabled on the server")
34-
ErrSSLKeyHasWorldPermissions = errors.New("pq: Private key file has group or world access. Permissions should be u=rw (0600) or less")
35-
ErrCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly")
31+
ErrNotSupported = errors.New("pq: Unsupported command")
32+
ErrInFailedTransaction = errors.New("pq: Could not complete operation in a failed transaction")
33+
ErrSSLNotSupported = errors.New("pq: SSL is not enabled on the server")
34+
ErrSSLKeyUnknownOwnership = errors.New("pq: Could not get owner information for private key, may not be properly protected")
35+
ErrSSLKeyHasUnacceptableUserPermissions = errors.New("pq: Private key file has group or world access. Permissions should be u=rw (0600) or less")
36+
ErrSSLKeyHasUnacceptableRootPermissions = errors.New("pq: Private key owned by root has world access. Permissions should be u=rw (0640) or less")
37+
ErrSSLKeyHasWorldPermissions = errors.New("pq: Private key has world access. Permissions should be u=rw,g=r (0640) if owned by root, or u=rw (0600), or less")
38+
39+
ErrCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly")
3640

3741
errUnexpectedReady = errors.New("unexpected ReadyForQuery")
3842
errNoRowsAffected = errors.New("no RowsAffected available after the empty statement")

ssl_permissions.go

+69-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,22 @@
33

44
package pq
55

6-
import "os"
6+
import (
7+
"os"
8+
"syscall"
9+
)
10+
11+
const (
12+
rootUserID = uint32(0)
13+
14+
// The maximum permissions that a private key file owned by a regular user
15+
// is allowed to have. This translates to u=rw.
16+
maxUserOwnedKeyPermissions os.FileMode = 0600
17+
18+
// The maximum permissions that a private key file owned by root is allowed
19+
// to have. This translates to u=rw,g=r.
20+
maxRootOwnedKeyPermissions os.FileMode = 0640
21+
)
722

823
// sslKeyPermissions checks the permissions on user-supplied ssl key files.
924
// The key file should have very little access.
@@ -14,8 +29,58 @@ func sslKeyPermissions(sslkey string) error {
1429
if err != nil {
1530
return err
1631
}
17-
if info.Mode().Perm()&0077 != 0 {
18-
return ErrSSLKeyHasWorldPermissions
32+
33+
err = hasCorrectPermissions(info)
34+
35+
// simplify the error for output purposes
36+
if err == ErrSSLKeyHasUnacceptableUserPermissions || err == ErrSSLKeyHasUnacceptableRootPermissions {
37+
err = ErrSSLKeyHasWorldPermissions
38+
}
39+
return err
40+
}
41+
42+
// hasCorrectPermissions checks the file info (and the unix-specific stat_t
43+
// output) to verify that the permissions on the file are correct.
44+
//
45+
// If the file is owned by the same user the process is running as,
46+
// the file should only have 0600 (u=rw). If the file is owned by root,
47+
// and the group matches the group that the process is running in, the
48+
// permissions cannot be more than 0640 (u=rw,g=r). The file should
49+
// never have world permissions.
50+
//
51+
// Returns an error when the permission check fails.
52+
func hasCorrectPermissions(info os.FileInfo) error {
53+
// if file's permission matches 0600, allow access.
54+
userPermissionMask := (os.FileMode(0777) ^ maxUserOwnedKeyPermissions)
55+
56+
// regardless of if we're running as root or not, 0600 is acceptable,
57+
// so we return if we match the regular user permission mask.
58+
if info.Mode().Perm()&userPermissionMask == 0 {
59+
return nil
60+
}
61+
62+
// We need to pull the Unix file information to get the file's owner.
63+
// If we can't access it, there's some sort of operating system level error
64+
// and we should fail rather than attempting to use faulty information.
65+
sysInfo := info.Sys()
66+
if sysInfo == nil {
67+
return ErrSSLKeyUnknownOwnership
1968
}
20-
return nil
69+
70+
unixStat, ok := sysInfo.(*syscall.Stat_t)
71+
if !ok {
72+
return ErrSSLKeyUnknownOwnership
73+
}
74+
75+
// if the file is owned by root, we allow 0640 (u=rw,g=r) to match what
76+
// Postgres does.
77+
if unixStat.Uid == rootUserID {
78+
rootPermissionMask := (os.FileMode(0777) ^ maxRootOwnedKeyPermissions)
79+
if info.Mode().Perm()&rootPermissionMask != 0 {
80+
return ErrSSLKeyHasUnacceptableRootPermissions
81+
}
82+
return nil
83+
}
84+
85+
return ErrSSLKeyHasUnacceptableUserPermissions
2186
}

ssl_permissions_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
package pq
5+
6+
import (
7+
"io/fs"
8+
"os"
9+
"syscall"
10+
"testing"
11+
"time"
12+
)
13+
14+
type stat_t_wrapper struct {
15+
stat syscall.Stat_t
16+
}
17+
18+
func (stat_t *stat_t_wrapper) Name() string {
19+
return "pem.key"
20+
}
21+
22+
func (stat_t *stat_t_wrapper) Size() int64 {
23+
return int64(100)
24+
}
25+
26+
func (stat_t *stat_t_wrapper) Mode() fs.FileMode {
27+
return fs.FileMode(stat_t.stat.Mode)
28+
}
29+
30+
func (stat_t *stat_t_wrapper) ModTime() time.Time {
31+
return time.Now()
32+
}
33+
34+
func (stat_t *stat_t_wrapper) IsDir() bool {
35+
return true
36+
}
37+
38+
func (stat_t *stat_t_wrapper) Sys() interface{} {
39+
return &stat_t.stat
40+
}
41+
42+
func TestHasCorrectRootGroupPermissions(t *testing.T) {
43+
currentUID := uint32(os.Getuid())
44+
currentGID := uint32(os.Getgid())
45+
46+
testData := []struct {
47+
expectedError error
48+
stat syscall.Stat_t
49+
}{
50+
{
51+
expectedError: nil,
52+
stat: syscall.Stat_t{
53+
Mode: 0600,
54+
Uid: currentUID,
55+
Gid: currentGID,
56+
},
57+
},
58+
{
59+
expectedError: nil,
60+
stat: syscall.Stat_t{
61+
Mode: 0640,
62+
Uid: 0,
63+
Gid: currentGID,
64+
},
65+
},
66+
{
67+
expectedError: ErrSSLKeyHasUnacceptableUserPermissions,
68+
stat: syscall.Stat_t{
69+
Mode: 0666,
70+
Uid: currentUID,
71+
Gid: currentGID,
72+
},
73+
},
74+
{
75+
expectedError: ErrSSLKeyHasUnacceptableRootPermissions,
76+
stat: syscall.Stat_t{
77+
Mode: 0666,
78+
Uid: 0,
79+
Gid: currentGID,
80+
},
81+
},
82+
}
83+
84+
for _, test := range testData {
85+
wrapper := &stat_t_wrapper{
86+
stat: test.stat,
87+
}
88+
89+
if test.expectedError != hasCorrectPermissions(wrapper) {
90+
if test.expectedError == nil {
91+
t.Errorf(
92+
"file owned by %d:%d with %s should not have failed check with error \"%s\"",
93+
test.stat.Uid,
94+
test.stat.Gid,
95+
wrapper.Mode(),
96+
hasCorrectPermissions(wrapper),
97+
)
98+
continue
99+
}
100+
t.Errorf(
101+
"file owned by %d:%d with %s, expected \"%s\", got \"%s\"",
102+
test.stat.Uid,
103+
test.stat.Gid,
104+
wrapper.Mode(),
105+
test.expectedError,
106+
hasCorrectPermissions(wrapper),
107+
)
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)