-
Notifications
You must be signed in to change notification settings - Fork 464
/
awssecret.go
215 lines (173 loc) · 5.74 KB
/
awssecret.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package awssecret
import (
"context"
"crypto"
"crypto/x509"
"errors"
"fmt"
"os"
"sync"
"time"
"github.com/andres-erbsen/clock"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/hcl"
"github.com/spiffe/spire/pkg/common/catalog"
"github.com/spiffe/spire/pkg/common/pemutil"
"github.com/spiffe/spire/pkg/common/x509svid"
"github.com/spiffe/spire/pkg/common/x509util"
"github.com/spiffe/spire/pkg/server/plugin/upstreamauthority"
spi "github.com/spiffe/spire/proto/spire/common/plugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
pluginName = "awssecret"
)
func BuiltIn() catalog.Plugin {
return builtin(New())
}
func builtin(p *Plugin) catalog.Plugin {
return catalog.MakePlugin(pluginName,
upstreamauthority.PluginServer(p),
)
}
type Config struct {
Region string `hcl:"region" json:"region"`
CertFileARN string `hcl:"cert_file_arn" json:"cert_file_arn"`
KeyFileARN string `hcl:"key_file_arn" json:"key_file_arn"`
AccessKeyID string `hcl:"access_key_id" json:"access_key_id"`
SecretAccessKey string `hcl:"secret_access_key" json:"secret_access_key"`
SecurityToken string `hcl:"secret_token" json:"secret_token"`
AssumeRoleARN string `hcl:"assume_role_arn" json:"assume_role_arn"`
}
type Plugin struct {
log hclog.Logger
mtx sync.RWMutex
cert *x509.Certificate
upstreamCA *x509svid.UpstreamCA
hooks struct {
clock clock.Clock
getenv func(string) string
newClient func(config *Config, region string) (secretsManagerClient, error)
}
}
func New() *Plugin {
return newPlugin(newSecretsManagerClient)
}
func newPlugin(newClient func(config *Config, region string) (secretsManagerClient, error)) *Plugin {
p := &Plugin{}
p.hooks.clock = clock.New()
p.hooks.getenv = os.Getenv
p.hooks.newClient = newClient
return p
}
func (m *Plugin) SetLogger(log hclog.Logger) {
m.log = log
}
func (m *Plugin) Configure(ctx context.Context, req *spi.ConfigureRequest) (*spi.ConfigureResponse, error) {
config, err := m.validateConfig(req)
if err != nil {
return nil, err
}
// set the AWS configuration and reset clients +
// Set local vars from config struct
sm, err := m.hooks.newClient(config, config.Region)
if err != nil {
return nil, err
}
key, cert, err := fetchFromSecretsManager(ctx, config, sm)
if err != nil {
return nil, err
}
m.mtx.Lock()
defer m.mtx.Unlock()
m.cert = cert
m.upstreamCA = x509svid.NewUpstreamCA(
x509util.NewMemoryKeypair(cert, key),
req.GlobalConfig.TrustDomain,
x509svid.UpstreamCAOptions{
Clock: m.hooks.clock,
})
return &spi.ConfigureResponse{}, nil
}
func (*Plugin) GetPluginInfo(context.Context, *spi.GetPluginInfoRequest) (*spi.GetPluginInfoResponse, error) {
return &spi.GetPluginInfoResponse{}, nil
}
// MintX509CA mints an X509CA by signing presented CSR with root CA fetched from AWS Secrets Manager
func (m *Plugin) MintX509CA(request *upstreamauthority.MintX509CARequest, stream upstreamauthority.UpstreamAuthority_MintX509CAServer) error {
ctx := stream.Context()
m.mtx.RLock()
defer m.mtx.RUnlock()
if m.upstreamCA == nil {
return errors.New("invalid state: not configured")
}
cert, err := m.upstreamCA.SignCSR(ctx, request.Csr, time.Second*time.Duration(request.PreferredTtl))
if err != nil {
return err
}
return stream.Send(&upstreamauthority.MintX509CAResponse{
X509CaChain: [][]byte{cert.Raw},
UpstreamX509Roots: [][]byte{m.cert.Raw},
})
}
func fetchFromSecretsManager(ctx context.Context, config *Config, sm secretsManagerClient) (crypto.PrivateKey, *x509.Certificate, error) {
keyPEMstr, err := readARN(ctx, sm, config.KeyFileARN)
if err != nil {
return nil, nil, fmt.Errorf("unable to read %s: %s", config.KeyFileARN, err)
}
key, err := pemutil.ParsePrivateKey([]byte(keyPEMstr))
if err != nil {
return nil, nil, err
}
certPEMstr, err := readARN(ctx, sm, config.CertFileARN)
if err != nil {
return nil, nil, fmt.Errorf("unable to read %s: %s", config.CertFileARN, err)
}
cert, err := pemutil.ParseCertificate([]byte(certPEMstr))
if err != nil {
return nil, nil, err
}
// Validate cert matches private key
matched, err := x509util.CertificateMatchesPrivateKey(cert, key)
if err != nil {
return nil, nil, err
}
if !matched {
return nil, nil, errors.New("certificate and private key does not match")
}
return key, cert, nil
}
func (m *Plugin) validateConfig(req *spi.ConfigureRequest) (*Config, error) {
// Parse HCL config payload into config struct
config := new(Config)
if err := hcl.Decode(&config, req.Configuration); err != nil {
return nil, err
}
if req.GlobalConfig == nil {
return nil, errors.New("global configuration is required")
}
if req.GlobalConfig.TrustDomain == "" {
return nil, errors.New("trust_domain is required")
}
// Set defaults from the environment
if config.SecurityToken == "" {
config.SecurityToken = m.hooks.getenv("AWS_SESSION_TOKEN")
}
switch {
case config.CertFileARN != "" && config.KeyFileARN != "":
case config.CertFileARN != "" && config.KeyFileARN == "":
return nil, errors.New("configuration missing key ARN")
case config.CertFileARN == "" && config.KeyFileARN != "":
return nil, errors.New("configuration missing cert ARN")
case config.CertFileARN == "" && config.KeyFileARN == "":
return nil, errors.New("configuration missing both cert ARN and key ARN")
}
return config, nil
}
// PublishJWTKey is not implemented by the wrapper and returns a codes.Unimplemented status
func (m *Plugin) PublishJWTKey(*upstreamauthority.PublishJWTKeyRequest, upstreamauthority.UpstreamAuthority_PublishJWTKeyServer) error {
return makeError(codes.Unimplemented, "publishing upstream is unsupported")
}
func makeError(code codes.Code, format string, args ...interface{}) error {
return status.Errorf(code, "aws-secret: "+format, args...)
}