A couple of weeks ago, I was working on automating my home with Smart IoT Devices. One of the devices I bought was a set of smart bulbs that could be controlled with voice assistants like Alexa. But I couldn’t control all the features of the smart bulb with it. So I started writing my own Alexa Skill to support those features by using the APIs provided by the manufacturer of the smart device.

Building the skill itself was a trivial task, a few methods that invoked a specific API calls on saying an (invocation) phrase and returning the results to the user. All these methods could be invoked by leveraging an AWS Lambda function. This is the backend for all of the Alexa skills.

This inherently does not mean that the functions and skills are secure. Topics like role and permission management for lambda functions, storing the logs for functions when they crash and accessing other native or external service securely are some of the crucial aspects that seem to be often missed. This blog focuses on addressing these security issues.

I will discuss the following topics: 1. Concepts of Alexa Skill
2. Building an Alexa Skill
3. Creating and Configuring a Lambda function to attach to an Alexa skill
4. Securing the Lambda function

Concepts of Alexa Skill

Alexa analyzes the speech form the user to understand the intent. Here is an example of an interaction with Alexa skill.

alexa_speech

This intent has various parts to it.

1. Wake Word - This is the word the user says to wake up the device to start listening. The instructions can be taken in follow up mode as well. In this mode, the results from previous interactions are stored and feels more like a conversation.

2. Launch - This word indicates Alexa to launch the skill.

3. Invocation Name - This is the keyword that indicates the name of the skill to be launched.

4. Utterance - These are the words/phrases the users will say when requesting a particular skill. Alexa then extracts the intent from the given phrase and responds accordingly.

Once the user intent is recorded, the request is then sent for processing. In the next section, we will build a basic Alexa skill.

Building an Alexa Skill

Pre-Requisites:

  1. Amazon Echo
  2. Smart Device (Optional, you can choose to build a service that simply responds by saying hello)
  3. Alexa Developer Account
  4. AWS Account

Steps:

1. Login to the Alexa developer portal
2. Click on create skill to get started.
3. Provide the skill name and choose a model to add to your skill.
4. You can choose a model to add to your skill. Either you can custom build it or use one of the pre-built models for interactions. Also, choose a method to host your backend.
Here you can choose between, Provision your own, Alexa Hosted Nodejs or Alexa hosted Python. With Alexa hosted options for backend, you as end-user don’t have to manage the provisioning of lambda functions and maintaining the infra. It comes with 5GB of media storage, 15 GB of data transfer and a table for session storage.

You can also choose to select the self-provisioning model.

In this blog, I will be using the self-provisioned model.

create_new_skill_1

There are limits on Alexa hosted skills. So if you think that your skills might be used more than the free tier or if you want to use any other programming language apart from Nodejs and Python (as of Nov ‘19) then the self-provisioning model is the best way to go.

5. Build the interaction model that includes Invocation, intents, and utterances. Invocation is the term used to interact with a particular skill. For ex: The user can say - “daily forecaster”.

invocation_name

6. Next, we need to add intent that handles certain use cases. This depends on the kind of skill you are building. Amazon provides some out of the box intents like HelpIntent, StopIntent, etc.

To create a custom intent, click on Add sign on the Intents tab.

create_custom_intent

7. Next, we add utterance in the Sample Utterance field. For ex: “weather in {}". Now, “Create a new slot” field pops up. Enter “location” and click on “Add”.

create_utterance

In the Intent Slot section, specify the SLOT TYPE for location as AMAZON.AT_CITY. This lets it know that the phrase is of type CITY. You can also choose to add custom SLOT TYPE and provide default values for it.

8. Now save the model and then build the model from options provided on the top of the page.

9. Once it is built, you can then look at JSON schema for the interaction model.

json_schema_model

10. The next step is to set up the endpoint. The endpoint is a service that will process the data from the user. It can be either a Lambda function or an HTTPS endpoint as long as it satisfies these requirements

endpoint

I will use the Lambda function as my endpoint. This is the recommended way.

11. Copy the SKILL ID on this page. It will be used when setting up the trigger for Lambda Function in the next section.

aws_lambda_skill_id

Create and Configure Lambda Function

Next, we will log in into the AWS account and create a lambda function. Follow this link to create the lambda function.

In the add trigger section, you would use the SKILL ID.

Here are some sample lambda functions that you could use for Alexa Skill

While this sounds simple enough, it leaves certain gaps that need to be fixed to ensure that your functions are secured. We will discuss this in the next section.

Securing Lambda Function

1. Enable Lambda Functions to have write access to CloudWatch Logs

It’s always a good practice to store all the Lambda logs to CloudWatch Logs for debugging and troubleshooting if there are any failures. To ensure that Lambda functions have access to write logs to CloudWatch, add this policy to the Lambda execution role.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:<ACCOUNT_ID>:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:<ACCOUNT_ID>:log-group:/aws/lambda/smart_lambda:*"
                ]
        }
    ]
}

2. Least Privilege to Lambda Execution Role

The principle of least privilege applies to even lambda functions. For ex: If a lambda function requires only read access to S3, it should be just given that. Furthermore, it should be restricted to a particular bucket or object. Always AVOID giving full access to any resource, unless it is necessary.

In our scenario, we can choose to enable X-Ray. For this, I am going to add this policy to the lambda execution role (from the previous step) to allow to only work with AWS X-RAY.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ForXRay",
            "Effect": "Allow",
            "Action": [
                "xray:UpdateSamplingRule",
                "xray:PutTelemetryRecords",
                "xray:UpdateGroup",
                "xray:CreateGroup",
                "xray:CreateSamplingRule",
                "xray:PutTraceSegments"
            ],
            "Resource": "*"
        }
    ]
}

3. Permissions boundary to assign privilege to User/Developer

To create a lambda function, the user creating the function needs to have the permission to do so. If you are following the least privilege model, the user should only be allowed with the following permission.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "LambdaPermission",
            "Effect": "Allow",
            "Action": [
                "lambda:*"
            ],
            "Resource": "*"
        },
        {
            "Sid": "PassExecutionRole",
            "Effect": "Allow",
            "Action": [
                "iam:ListRolePolicies",
                "iam:ListAttachedRolePolicies",
                "iam:GetRole",
                "iam:PassRole",
                "iam:ListRoles"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ReadOnlyPermissions",
            "Effect": "Allow",
            "Action": [
                "cloudformation:DescribeStackResources",
                "iam:List*",
                "iam:Get*"
            ],
            "Resource": "*"
        },
        {
            "Sid": "IAMRoleManage",
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:CreatePolicy",
                "iam:PutRolePolicy",
                "iam:AttachRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:ListRolePolicies",
                "iam:ListAttachedRolePolicies"
            ],
            "Resource": "*"
        }
    ]
}

However, with this policy, the user can attach ANY role/policy to the lambda execute role, in some cases even ADMINISTRATOR ACCESS. So even though the user has “least privilege” with access to create lambda function and attaches a role to it, the lambda function might end up with full access. This is a potential security hole.

There are a couple of ways to handle the delegation of roles to developers/non-admin users. a. Granular Policy - Create a granular policy for every single lambda function that is created and attach it to the user’s policy. This could get cumbersome if you are managing a lot of lambda functions and restricts the user/developer from creating lambda functions with the least friction. b. Permissions Boundary - Assign a permissions boundary to the user that defines the maximum allowed permissions. This means that the user can attach only those roles which are within the permissions boundary even though their Identity-based policy might allow to get all roles and attach any role to the lambda function.

The permissions boundary themselves do not grant access on their own. But the intersection of the identity-based policy and permissions boundary is the maximum policy that grants access to the resource.

For ex:

Identity policy for the user is similar to the one in the previous step but with a condition to work with permissions boundary.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "LambdaPermission",
            "Effect": "Allow",
            "Action": [
                "lambda:*"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ReadOnlyPermissions",
            "Effect": "Allow",
            "Action": [
                "cloudformation:DescribeStackResources",
                "iam:List*",
                "iam:Get*"
            ],
            "Resource": "*"
        },
        {
            "Sid": "IAMRoleManageWithBoundary",
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:PutRolePolicy",
                "iam:AttachRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:ListRolePolicies",
                "iam:ListAttachedRolePolicies"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:PermissionsBoundary": "arn:aws:iam::<ACCOUNT ID>:policy/LambdaBoundaries"
                }
            }
        },
        {
            "Sid": "PassExecutionRole",
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*"
        },
        {
            "Sid": "ViewLogs",
            "Effect": "Allow",
            "Action": [
                "logs:*"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/*"
        },
        {
            "Sid": "DenyRolePermissionBoundaryDelete",
            "Effect": "Deny",
            "Action": [
                "iam:DeleteRolePermissionsBoundary",
                "iam:CreatePolicyVersion",
                "iam:DeletePolicyVersion",
                "iam:DeletePolicy"
            ],
            "Resource": "*"
        }
    ]
}

In the policy above, we have added a condition for the user to Create IAM role but it needs to have IAM Permission boundary attached to it.

 "Condition": {
 "StringEquals": {"iam:PermissionsBoundary": "arn:aws:iam::<ACCOUNT_ID>:policy/LambdaBoundaries"}
 }

Along with this, there is a Deny permission for Deleting the permissions boundary. According to this policy, the user can create an IAM role for lambda function but it NEEDS to have a permissions boundary attached with it.

The Permission boundary policy looks something like this with ARN similar to arn:aws:iam::<ACCOUNT_ID>:policy/LambdaBoundaries

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowedActionsForCloudWatch",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Sid": "FullAccessLambda",
            "Effect": "Allow",
            "Action": "lambda:*",
            "Resource": "*"
        },
        {
            "Sid": "CreateLogGroup",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:*:*"
        },
        {
            "Sid": "ForXRay",
            "Effect": "Allow",
            "Action": [
                "xray:UpdateSamplingRule",
                "xray:PutTelemetryRecords",
                "xray:UpdateGroup",
                "xray:CreateGroup",
                "xray:CreateSamplingRule",
                "xray:PutTraceSegments"
            ],
            "Resource": "*"
        },
        {
            "Sid": "IAMRoleManage",
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:ListRoles",
                "iam:GetRole",
                "iam:GetPolicy",
                "iam:GetPolicyVersion",
                "iam:PutRolePolicy",
                "iam:AttachRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:ListRolePolicies",
                "iam:ListAttachedRolePolicies",
                "iam:ListPolicies"
            ],
            "Resource": "*"
        },
        {
            "Sid": "PassExecutionRole",
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*"
        },
        {
            "Sid": "ReadOnlyPermissions",
            "Effect": "Allow",
            "Action": "cloudformation:DescribeStackResources",
            "Resource": "*"
        }
    ]
}

With this permission boundary in place, whenever the developer creates a new role for lambda execution, the role must have the permissions boundary attached to it. Without that the role creation would fail. Now, even if the lambda execution role has permissions to other services like S3 or RDS, it cannot access them because it is not part of the permissions boundary. This is a secure way to manage resource access to lambda.

I will discuss Permissions Boundary in detail in another blog.

4. Do not share IAM role with Lambda functions

Every Lambda function has a purpose and a set of resources it needs access to it. If in the future, one of the Lambda functions needs more access then the IAM role is augmented with additional policies. If the IAM roles are shared across Lambda functions, then some functions may have more access to resources than they should have. Now, the process of refactoring these roles can get very tedious. Hence, it’s always a best practice to maintain a separate Role for each function.

5. Continuously monitor Lambda configuration changes

This is a very important aspect when it comes to the Public Cloud environment. Configuration changes can result in Lambda functions, databases, EC2 instances and other resources being exposed directly on to the internet. To manage this, the ideal way is to use a cloud posture security management tool. There are Open-Source tools like Cloud Custodian or you can use commercial tools like VMware Secure State.

This process can also be achieved via your pipeline. Here is a blog on Continuous Security demonstrating the implementation.

Conclusion

Building a custom Alexa skill involves building the skills interface, the interaction model and a Lambda function to handle requests from the user. The developers need to have the appropriate access to avoid any assigning any excessive permissions to Lambda functions than required. This can be controlled with the Permissions boundary. A combination of these methods along with Continuous monitoring for changes in configuration can help you build secure Alexa skills.