Skip to content

Commit 549a422

Browse files
committed
Split generate and rotate secrets
1 parent 5c26513 commit 549a422

File tree

9 files changed

+550
-107
lines changed

9 files changed

+550
-107
lines changed

cmd/account/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func NewCmdAccount(streams genericclioptions.IOStreams, flags *genericclioptions
2727
accountCmd.AddCommand(newCmdCleanVeleroSnapshots(streams))
2828
accountCmd.AddCommand(newCmdVerifySecrets(streams, flags))
2929
accountCmd.AddCommand(newCmdRotateSecret(streams, flags))
30+
accountCmd.AddCommand(newCmdGenerateSecret(streams, flags))
3031

3132
return accountCmd
3233
}

cmd/account/generate-secret.go

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package account
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io/ioutil"
7+
"path/filepath"
8+
9+
"github.com/aws/aws-sdk-go/aws"
10+
"github.com/aws/aws-sdk-go/service/iam"
11+
"github.com/aws/aws-sdk-go/service/sts"
12+
awsv1alpha1 "github.com/openshift/aws-account-operator/pkg/apis/aws/v1alpha1"
13+
"github.com/spf13/cobra"
14+
15+
corev1 "k8s.io/api/core/v1"
16+
"k8s.io/apimachinery/pkg/types"
17+
"k8s.io/cli-runtime/pkg/genericclioptions"
18+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
19+
"sigs.k8s.io/controller-runtime/pkg/client"
20+
21+
"github.com/openshift/osd-utils-cli/cmd/common"
22+
"github.com/openshift/osd-utils-cli/pkg/k8s"
23+
awsprovider "github.com/openshift/osd-utils-cli/pkg/provider/aws"
24+
)
25+
26+
// newCmdGenerateSecret implements the generate-secret command which generates an new set of IAM User credentials
27+
func newCmdGenerateSecret(streams genericclioptions.IOStreams, flags *genericclioptions.ConfigFlags) *cobra.Command {
28+
ops := newGenerateSecretOptions(streams, flags)
29+
generateSecretCmd := &cobra.Command{
30+
Use: "generate-secret <IAM User name>",
31+
Short: "Generate IAM credentials secret",
32+
DisableAutoGenTag: true,
33+
Run: func(cmd *cobra.Command, args []string) {
34+
cmdutil.CheckErr(ops.complete(cmd, args))
35+
cmdutil.CheckErr(ops.run())
36+
},
37+
Aliases: []string{"generate-secrets"},
38+
}
39+
40+
generateSecretCmd.Flags().StringVarP(&ops.accountName, "account-name", "a", "", "AWS Account CR name")
41+
generateSecretCmd.Flags().StringVar(&ops.accountNamespace, "account-namespace", common.AWSAccountNamespace,
42+
"The namespace to keep AWS accounts. The default value is aws-account-operator.")
43+
generateSecretCmd.Flags().StringVarP(&ops.accountID, "account-id", "i", "", "AWS Account ID")
44+
generateSecretCmd.Flags().StringVarP(&ops.profile, "aws-profile", "p", "", "specify AWS profile")
45+
generateSecretCmd.Flags().StringVar(&ops.secretName, "secret-name", "", "Specify name of the generated secret")
46+
generateSecretCmd.Flags().StringVar(&ops.secretNamespace, "secret-namespace", "aws-account-operator", "Specify namespace of the generated secret")
47+
generateSecretCmd.Flags().BoolVar(&ops.quiet, "quiet", false, "Suppress logged output")
48+
generateSecretCmd.Flags().BoolVar(&ops.ccs, "ccs", false, "Only generate specific secret for osdCcsAdmin. Requires Account CR name")
49+
50+
return generateSecretCmd
51+
}
52+
53+
// generateSecretOptions defines the struct for running generate-secret command
54+
type generateSecretOptions struct {
55+
accountName string
56+
accountID string
57+
accountNamespace string
58+
iamUsername string
59+
60+
secretName string
61+
secretNamespace string
62+
quiet bool
63+
ccs bool
64+
outputPath string
65+
66+
// AWS config
67+
region string
68+
profile string
69+
cfgFile string
70+
71+
flags *genericclioptions.ConfigFlags
72+
genericclioptions.IOStreams
73+
kubeCli client.Client
74+
}
75+
76+
func newGenerateSecretOptions(streams genericclioptions.IOStreams, flags *genericclioptions.ConfigFlags) *generateSecretOptions {
77+
return &generateSecretOptions{
78+
flags: flags,
79+
IOStreams: streams,
80+
}
81+
}
82+
83+
func (o *generateSecretOptions) complete(cmd *cobra.Command, args []string) error {
84+
85+
// Opinionated config for CCS rotation
86+
if o.ccs {
87+
// If no account CR name is provided via the -a option, try and get it via argument
88+
if o.accountName == "" {
89+
return cmdutil.UsageErrorf(cmd, "Account CR name argument is required")
90+
}
91+
92+
return nil
93+
}
94+
95+
if len(args) != 1 {
96+
return cmdutil.UsageErrorf(cmd, "IAM User name argument is required")
97+
}
98+
o.iamUsername = args[0]
99+
100+
// account CR name and account ID cannot be empty at the same time
101+
if o.accountName == "" && o.accountID == "" {
102+
return cmdutil.UsageErrorf(cmd, "AWS account CR name and AWS account ID cannot be empty at the same time")
103+
}
104+
105+
if o.accountName != "" && o.accountID != "" {
106+
return cmdutil.UsageErrorf(cmd, "AWS account CR name and AWS account ID cannot be set at the same time")
107+
}
108+
109+
// only initialize kubernetes client when account name is set
110+
if o.accountName != "" {
111+
var err error
112+
o.kubeCli, err = k8s.NewClient(o.flags)
113+
if err != nil {
114+
return err
115+
}
116+
}
117+
118+
return nil
119+
}
120+
121+
func (o *generateSecretOptions) run() error {
122+
123+
if o.ccs {
124+
err := o.generateCcsSecret()
125+
126+
return err
127+
}
128+
129+
ctx := context.TODO()
130+
var err error
131+
awsSetupClient, err := awsprovider.NewAwsClient(o.profile, o.region, o.cfgFile)
132+
if err != nil {
133+
return err
134+
}
135+
136+
// Get the accountID
137+
var accountID string
138+
if o.accountName != "" {
139+
account, err := k8s.GetAWSAccount(ctx, o.kubeCli, o.accountNamespace, o.accountName)
140+
if err != nil {
141+
return err
142+
}
143+
if account.Spec.AwsAccountID != "" {
144+
accountID = account.Spec.AwsAccountID
145+
} else {
146+
return fmt.Errorf("Account CR is missing AWS Account ID")
147+
}
148+
} else {
149+
accountID = o.accountID
150+
}
151+
152+
// Ensure creds are valid
153+
callerIdentityOutput, err := awsSetupClient.GetCallerIdentity(&sts.GetCallerIdentityInput{})
154+
if err != nil {
155+
return err
156+
}
157+
158+
// Assume
159+
roleArn := aws.String(fmt.Sprintf("arn:aws:iam::%s:role/%s", accountID, awsv1alpha1.AccountOperatorIAMRole))
160+
credentials, err := awsprovider.GetAssumeRoleCredentials(awsSetupClient, aws.Int64(900),
161+
callerIdentityOutput.UserId, roleArn)
162+
if err != nil {
163+
return err
164+
}
165+
166+
awsClient, err := awsprovider.NewAwsClientWithInput(&awsprovider.AwsClientInput{
167+
AccessKeyID: *credentials.AccessKeyId,
168+
SecretAccessKey: *credentials.SecretAccessKey,
169+
SessionToken: *credentials.SessionToken,
170+
Region: o.region,
171+
})
172+
if err != nil {
173+
return err
174+
}
175+
176+
username := aws.String(o.iamUsername)
177+
ok, err := awsprovider.CheckIAMUserExists(awsClient, username)
178+
if err != nil {
179+
return err
180+
}
181+
182+
// if the specified user does not exist, create one
183+
if !ok {
184+
policyArn := aws.String("arn:aws:iam::aws:policy/AdministratorAccess")
185+
if err := awsprovider.CreateIAMUserAndAttachPolicy(awsClient,
186+
username, policyArn); err != nil {
187+
return err
188+
}
189+
} else {
190+
fmt.Fprintf(o.IOStreams.Out, "User %s exists, deleting existing access keys now.\n", o.iamUsername)
191+
if err := awsprovider.DeleteUserAccessKeys(awsClient, username); err != nil {
192+
return err
193+
}
194+
}
195+
196+
newKey, err := awsClient.CreateAccessKey(&iam.CreateAccessKeyInput{
197+
UserName: username,
198+
})
199+
if err != nil {
200+
return err
201+
}
202+
203+
secret := k8s.NewAWSSecret(
204+
o.secretName,
205+
o.secretNamespace,
206+
*newKey.AccessKey.AccessKeyId,
207+
*newKey.AccessKey.SecretAccessKey,
208+
)
209+
if !o.quiet {
210+
fmt.Fprintln(o.IOStreams.Out, secret)
211+
}
212+
213+
if o.outputPath != "" {
214+
outputPath, err := filepath.Abs(o.outputPath)
215+
if err != nil {
216+
return err
217+
}
218+
return ioutil.WriteFile(outputPath, []byte(secret), 0644)
219+
}
220+
221+
return nil
222+
}
223+
224+
func (o *generateSecretOptions) generateCcsSecret() error {
225+
226+
ctx := context.TODO()
227+
var err error
228+
awsSetupClient, err := awsprovider.NewAwsClient(o.profile, o.region, o.cfgFile)
229+
if err != nil {
230+
return err
231+
}
232+
233+
account, err := k8s.GetAWSAccount(ctx, o.kubeCli, common.AWSAccountNamespace, o.accountName)
234+
if err != nil {
235+
return err
236+
}
237+
238+
// Ensure AWS calls are succesful with client
239+
callerIdentityOutput, err := awsSetupClient.GetCallerIdentity(&sts.GetCallerIdentityInput{})
240+
if err != nil {
241+
return err
242+
}
243+
244+
accountIDSuffixLabel, ok := account.Labels["iamUserId"]
245+
if !ok {
246+
return fmt.Errorf("No label on Account CR for IAM User")
247+
}
248+
249+
// Get the aws-account-operator configmap
250+
cm := &corev1.ConfigMap{}
251+
cmErr := o.kubeCli.Get(context.TODO(), types.NamespacedName{Namespace: common.AWSAccountNamespace, Name: common.DefaultConfigMap}, cm)
252+
if cmErr != nil {
253+
return fmt.Errorf("There was an error getting the ConfigMap to get the SRE Access Role %s", cmErr)
254+
}
255+
// Get the ARN value
256+
SREAccessARN := cm.Data["CCS-Access-Arn"]
257+
if SREAccessARN == "" {
258+
return fmt.Errorf("SRE Access ARN is missing from configmap")
259+
}
260+
261+
// Assume the ARN
262+
srepRoleCredentials, err := awsprovider.GetAssumeRoleCredentials(awsSetupClient, aws.Int64(900), callerIdentityOutput.UserId, &SREAccessARN)
263+
if err != nil {
264+
return err
265+
}
266+
267+
// Create client with the SREP role
268+
srepRoleClient, err := awsprovider.NewAwsClientWithInput(&awsprovider.AwsClientInput{
269+
AccessKeyID: *srepRoleCredentials.AccessKeyId,
270+
SecretAccessKey: *srepRoleCredentials.SecretAccessKey,
271+
SessionToken: *srepRoleCredentials.SessionToken,
272+
Region: "us-east-1",
273+
})
274+
if err != nil {
275+
return err
276+
}
277+
278+
// Role chain to assume BYOCAdminAccessRole-{uid}
279+
roleArn := aws.String(fmt.Sprintf("arn:aws:iam::%s:role/%s", account.Spec.AwsAccountID, "BYOCAdminAccess-"+accountIDSuffixLabel))
280+
credentials, err := awsprovider.GetAssumeRoleCredentials(srepRoleClient, aws.Int64(900),
281+
callerIdentityOutput.UserId, roleArn)
282+
if err != nil {
283+
return err
284+
}
285+
286+
// Create client with the chain assumed role
287+
awsAssumedRoleClient, err := awsprovider.NewAwsClientWithInput(&awsprovider.AwsClientInput{
288+
AccessKeyID: *credentials.AccessKeyId,
289+
SecretAccessKey: *credentials.SecretAccessKey,
290+
SessionToken: *credentials.SessionToken,
291+
Region: "us-east-1",
292+
})
293+
294+
// Create new set of Access Keys for osdCcsAdmin
295+
296+
newKey, err := awsAssumedRoleClient.CreateAccessKey(&iam.CreateAccessKeyInput{
297+
UserName: aws.String(common.OSDCcsAdminIAM),
298+
})
299+
if err != nil {
300+
return err
301+
}
302+
303+
secret := k8s.NewAWSSecret(
304+
o.secretName,
305+
o.secretNamespace,
306+
*newKey.AccessKey.AccessKeyId,
307+
*newKey.AccessKey.SecretAccessKey,
308+
)
309+
310+
if !o.quiet {
311+
fmt.Fprintln(o.IOStreams.Out, secret)
312+
}
313+
314+
return nil
315+
316+
}

0 commit comments

Comments
 (0)