awsparamstore.go 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. // Copyright (c) Tailscale Inc & contributors
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !ts_omit_aws
  4. // Package awsparamstore registers support for fetching secret values from AWS
  5. // Parameter Store.
  6. package awsparamstore
  7. import (
  8. "context"
  9. "fmt"
  10. "strings"
  11. "github.com/aws/aws-sdk-go-v2/aws"
  12. "github.com/aws/aws-sdk-go-v2/aws/arn"
  13. "github.com/aws/aws-sdk-go-v2/config"
  14. "github.com/aws/aws-sdk-go-v2/service/ssm"
  15. "tailscale.com/feature"
  16. "tailscale.com/internal/client/tailscale"
  17. )
  18. func init() {
  19. feature.Register("awsparamstore")
  20. tailscale.HookResolveValueFromParameterStore.Set(ResolveValue)
  21. }
  22. // parseARN parses and verifies that the input string is an
  23. // ARN for AWS Parameter Store, returning the region and parameter name if so.
  24. //
  25. // If the input is not a valid Parameter Store ARN, it returns ok==false.
  26. func parseARN(s string) (region, parameterName string, ok bool) {
  27. parsed, err := arn.Parse(s)
  28. if err != nil {
  29. return "", "", false
  30. }
  31. if parsed.Service != "ssm" {
  32. return "", "", false
  33. }
  34. parameterName, ok = strings.CutPrefix(parsed.Resource, "parameter/")
  35. if !ok {
  36. return "", "", false
  37. }
  38. // NOTE: parameter names must have a leading slash
  39. return parsed.Region, "/" + parameterName, true
  40. }
  41. // ResolveValue fetches a value from AWS Parameter Store if the input
  42. // looks like an SSM ARN (e.g., arn:aws:ssm:us-east-1:123456789012:parameter/my-secret).
  43. //
  44. // If the input is not a Parameter Store ARN, it returns the value unchanged.
  45. //
  46. // If the input is a Parameter Store ARN and fetching the parameter fails, it
  47. // returns an error.
  48. func ResolveValue(ctx context.Context, valueOrARN string) (string, error) {
  49. // If it doesn't look like an ARN, return as-is
  50. region, parameterName, ok := parseARN(valueOrARN)
  51. if !ok {
  52. return valueOrARN, nil
  53. }
  54. // Load AWS config with the region from the ARN
  55. cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
  56. if err != nil {
  57. return "", fmt.Errorf("loading AWS config in region %q: %w", region, err)
  58. }
  59. // Create SSM client and fetch the parameter
  60. client := ssm.NewFromConfig(cfg)
  61. output, err := client.GetParameter(ctx, &ssm.GetParameterInput{
  62. // The parameter to fetch.
  63. Name: aws.String(parameterName),
  64. // If the parameter is a SecureString, decrypt it.
  65. WithDecryption: aws.Bool(true),
  66. })
  67. if err != nil {
  68. return "", fmt.Errorf("getting SSM parameter %q: %w", parameterName, err)
  69. }
  70. if output.Parameter == nil || output.Parameter.Value == nil {
  71. return "", fmt.Errorf("SSM parameter %q has no value", parameterName)
  72. }
  73. return strings.TrimSpace(*output.Parameter.Value), nil
  74. }