customink / cookiecutter-ruby

Demo AWS SAM Lambda Cookiecutter Ruby Project
https://dev.to/metaskills/using-aws-sam-cookiecutter-project-templates-37le
6 stars 0 forks source link

Handling secrets #4

Open synth opened 3 years ago

synth commented 3 years ago

The best way to handle secrets in the AWS world, to my knowledge, is using Systems Manager > Parameter Store.

When getting started, connecting to PS was pretty much my first task as it will link me into the rest of our AWS world basically: api endpoints, tokens, database endpoints/credentials, configuration settings, etc.

It would be great if there was first class support for accessing parameters in Parameter Store. From the AWS side, I figured out how to add ParameterStore following this article: https://aws.amazon.com/blogs/compute/sharing-secrets-with-aws-lambda-using-aws-systems-manager-parameter-store/. It wasn't too hard, but also is fairly boilerplate and so Imagine it can be baked into the cookie cutter project fairly easily.

In general, I imagine cookiecutter-ruby asking us for what modules to include - ParameterStore being one of them. It then puts in the requisite Cfn into the template and also gives you a ruby starter class to query PS for variables.

synth commented 3 years ago

Looks like this is handled by Lamby! https://github.com/customink/lamby/blob/master/lib/lamby/ssm_parameter_store.rb

zhuqing662k commented 3 years ago

I agree using SSM Parameter Store is a good way to handle secrets. However, there is a cost associated with accessing it. And in a lesser sense it may take a bit time each time we access the Parameter Store. I know the service is lightening fast. But still it is a web service request.

On the other hand, I hope we can put the secrets in CI/CD protected variables and during building/deployment we substitute the place-holders with the real secret values. I thought it earlier when I used this project's sister project for python. But I did not investigate further. So this is just a thought. I am not sure if this is feasible. Just my 2 cents.

synth commented 3 years ago

Fyi parameter store is free as long as you are using standard parameters and need a throughput of 40 queries/s or less.

In any case, you make good points. I think a hybrid approach of being able to use parameter store to set the env at build/deploy time could also be good.

zhuqing662k commented 3 years ago

@synth Thank you so much for the info! Yes, I looked it up - using the SSM Parameter Store in standard tier is indeed free. Googling reveals a similar service - "AWS Secrets Manager". That is not free. But I don't think I need the advanced features there. While reading your comments again, I found the link to an AWS blog. I will bookmark it and read it again. I think I will modify my project to use the SSM Parameter Store. Trying to figuring out how to use github secrets to replace the place-holders in code might be a bit more involved. Kudos!

synth commented 3 years ago

Below is our template for anyone interested (it took me a little while to figure it out correctly). Two notes:

  JobsManagerLambda:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: lib/jobs_manager.handler
      Runtime: ruby2.7
      Timeout: 60
      MemorySize: 512
      FunctionName: !Sub jobs-manager-${StageEnv}
      Environment:
        Variables:
          STAGE_ENV: !Ref StageEnv
          RUBYOPT: '-W0'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Policies:
        -
          Version: '2012-10-17'
          Statement:
            -
              Effect: Allow
              Action:
                - 'ssm:GetParameter*'
              Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${StageEnv}/*'
            -
              Effect: Allow
              Action:
                - 'kms:Decrypt'
              Resource: '*' # FIXME: need to limit once there is a convention with KMS keys across the environments

Then our Ruby handler code looks like this:

require 'json'
require 'dotenv'

require_relative './jobs_manager/env'
require_relative './jobs_manager/ssm_parameter_store' # copied from https://github.com/customink/lamby/blob/master/lib/lamby/ssm_parameter_store.rb

def handler(event:, context:)

  path = "/recognize/#{ENV['STAGE_ENV']}/"
  envs = JobsManager::SsmParameterStore.new path
  envs.get!

  # sample code to output what name and env look like (without values)
  envs.params.each do |param|
    puts param.name, param.env
  end

  puts event
  { statusCode: 200,
    headers: [{'Content-Type' => 'application/json'}],
    body: JSON.dump(event) }
end
metaskills commented 3 years ago

Thanks Qing! So a few thoughts. I'll try to be brief on each.

Simple rake take to take all ENVs in a path and write out a Dotenv file. I've sued this method recently in the Environment section of build file (https://github.com/customink/lamby-cookiecutter/blob/master/%7B%7Bcookiecutter.project_name%7D%7D/bin/build-rails#L17) vs that CLI usage.

./bin/rake -rlamby lamby:ssm:dotenv \
  LAMBY_SSM_PARAMS_PATH="/config/${RAILS_ENV}/myapp/env" \

As you pointed out, there is a need for a Policies. I like how yours also has KMS encrypted. Here is what I had in the old guides that will come back. I'll pull in your KMS one too when I do.

Policies:
  - Version: "2012-10-17"
    Statement:
      - Effect: Allow
        Action:
          - ssm:GetParameter
          - ssm:GetParameters
          - ssm:GetParametersByPath
          - ssm:GetParameterHistory
        Resource:
          - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/config/myapp/*

REMINDER: SAM has policy templates (syntactic sugar) for common things that make this easier. https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html So an alternative would be this:

Policies:
  - SSMParameterReadPolicy: { ParameterName: '/config/myapp/*' }
  - KMSDecryptPolicy: { KeyId: '...' }

IMPORTANT! If you do not use Dotenv to write a .env.production file during your build phase with the rake task... DO NOT do SSM calls in your handler. This function is called for each request. Instead put envs = JobsManager::SsmParameterStore.new path out of the handler. But please do check out Dotevn and the old guides https://github.com/customink/lamby_site/blob/c5b661d9ffe8c4c0f3fed4317a7f58c47b2894de/app/views/docs/environment_and_configuration.erbmd

synth commented 3 years ago

IMPORTANT! If you do not use Dotenv to write a .env.production file during your build phase with the rake task... DO NOT do SSM calls in your handler.

Good point. This is a quick fix 👍

envs = nil
def handler(event:, context:)

  envs ||= begin
    path = "/recognize/#{ENV['STAGE_ENV']}/"
    ps = JobsManager::SsmParameterStore.new path
    ps = envs.get!.to_env
  end
 # ... the rest
end

That said, I think you are right that using dotenv is still the best approach

metaskills commented 3 years ago

Maybe this at the top of your app.rb file.

require 'lamby/ssm_parameter_store'
Lamby::SsmParameterStore.new("/recognize/#{ENV['STAGE_ENV']}").to_env

Then everything will be ENV as expected.

synth commented 3 years ago

better, thanks! UPDATE: I think it needs the .get!, so Lamby::SsmParameterStore.new("/recognize/#{ENV['STAGE_ENV']}").get!.to_env but yea :) UPDATE2: For some reason needs the trailing /, otherwise I get a strange policy exception, ha.

metaskills commented 3 years ago

CORRECT! And thank you! I knew I left this code in for a reason. I'll resolve this issue when I restore all the documentation.