summaryrefslogtreecommitdiffhomepage
path: root/feature/awsparamstore/awsparamstore.go
blob: f63f546ed70ede6adef4a563c809205c097bf8f5 (plain)
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
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build !ts_omit_aws

// Package awsparamstore registers support for fetching secret values from AWS
// Parameter Store.
package awsparamstore

import (
	"context"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/aws/arn"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ssm"
	"tailscale.com/feature"
	"tailscale.com/internal/client/tailscale"
)

func init() {
	feature.Register("awsparamstore")
	tailscale.HookResolveValueFromParameterStore.Set(ResolveValue)
}

// parseARN parses and verifies that the input string is an
// ARN for AWS Parameter Store, returning the region and parameter name if so.
//
// If the input is not a valid Parameter Store ARN, it returns ok==false.
func parseARN(s string) (region, parameterName string, ok bool) {
	parsed, err := arn.Parse(s)
	if err != nil {
		return "", "", false
	}

	if parsed.Service != "ssm" {
		return "", "", false
	}
	parameterName, ok = strings.CutPrefix(parsed.Resource, "parameter/")
	if !ok {
		return "", "", false
	}

	// NOTE: parameter names must have a leading slash
	return parsed.Region, "/" + parameterName, true
}

// ResolveValue fetches a value from AWS Parameter Store if the input
// looks like an SSM ARN (e.g., arn:aws:ssm:us-east-1:123456789012:parameter/my-secret).
//
// If the input is not a Parameter Store ARN, it returns the value unchanged.
//
// If the input is a Parameter Store ARN and fetching the parameter fails, it
// returns an error.
func ResolveValue(ctx context.Context, valueOrARN string) (string, error) {
	// If it doesn't look like an ARN, return as-is
	region, parameterName, ok := parseARN(valueOrARN)
	if !ok {
		return valueOrARN, nil
	}

	// Load AWS config with the region from the ARN
	cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
	if err != nil {
		return "", fmt.Errorf("loading AWS config in region %q: %w", region, err)
	}

	// Create SSM client and fetch the parameter
	client := ssm.NewFromConfig(cfg)
	output, err := client.GetParameter(ctx, &ssm.GetParameterInput{
		// The parameter to fetch.
		Name: aws.String(parameterName),

		// If the parameter is a SecureString, decrypt it.
		WithDecryption: aws.Bool(true),
	})
	if err != nil {
		return "", fmt.Errorf("getting SSM parameter %q: %w", parameterName, err)
	}

	if output.Parameter == nil || output.Parameter.Value == nil {
		return "", fmt.Errorf("SSM parameter %q has no value", parameterName)
	}

	return strings.TrimSpace(*output.Parameter.Value), nil
}