package hostfence import ( "context" "encoding/json" "errors" "io" "os" "regexp" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) type fenceCredentials struct { Role string `json:"role"` Region string `json:"region"` Bucket string `json:"bucket"` AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` } // OpenRootFenceStore uses only the dedicated root bootstrap identity. AWS // environment credentials, IMDS and shared profiles are never consulted. func OpenRootFenceStore(ctx context.Context, bucket string) (release.Store, error) { if os.Geteuid() != 0 { return nil, os.ErrPermission } if err := ctx.Err(); err != nil { return nil, err } f, err := hostfloor.OpenProtected("/etc/carlos/fence-writer/credentials.json") if err != nil { return nil, err } defer f.Close() st, err := f.Stat() if err != nil { return nil, err } if st.Mode().Perm() != 0400 && st.Mode().Perm() != 0600 { return nil, os.ErrPermission } var reader io.Reader = f if st.Mode().Perm() == 0600 { reader, err = activationCredentialReader(f) if err != nil { return nil, err } } c, err := decodeFenceCredentials(reader, bucket) if err != nil { return nil, err } cfg := aws.Config{Region: c.Region, Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(c.AccessKey, c.SecretKey, ""))} provider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), c.Role, func(o *stscreds.AssumeRoleOptions) { o.RoleSessionName = "carlos-root-fence" }) cfg.Credentials = aws.NewCredentialsCache(provider) return &release.S3Store{Client: s3.NewFromConfig(cfg), Bucket: c.Bucket}, nil } func decodeFenceCredentials(r io.Reader, bucket string) (fenceCredentials, error) { var c fenceCredentials d := json.NewDecoder(io.LimitReader(r, 16385)) d.DisallowUnknownFields() if err := d.Decode(&c); err != nil { return c, err } var extra any if err := d.Decode(&extra); err != io.EOF { return c, errors.New("host fences: invalid credential file") } if c.Bucket != bucket || bucket == "" || c.Region == "" || c.AccessKey == "" || c.SecretKey == "" || !regexp.MustCompile(`^arn:aws:iam::[0-9]{12}:role/[A-Za-z0-9+=,.@_/-]+$`).MatchString(c.Role) { return c, errors.New("host fences: incomplete dedicated credential scope") } return c, nil }