The Internet of Things on AWS – Official Blog
How to remote access devices from a web browser using secure tunneling
Using firewalls is a common way to protect and secure access to IoT devices. Yet, it’s challenging to access and manage devices deployed at remote sites, behind firewalls that block all inbound traffic. Troubleshooting devices can involve sending technicians onsite to connect to those devices. This increases the complexity and the cost of device management.
Secure Tunneling is a feature of AWS IoT Device Management that helps customers accessing remote devices over a secure connection that is managed by AWS IoT. Secure Tunneling does not require updates to your existing inbound firewall rules, so you can keep the same security level provided by firewall rules at a remote site.
In this post, you learn how to use secure tunneling to start a Secure Shell (SSH) session to remote devices from web application. This connection can be used for configuration, troubleshooting, and to complete other operational tasks.
You can download the source code of this implementation from GitHub.
Solution overview
I will walk you through the steps for building a web based local proxy to gain access to remote devices using secure tunneling.
The local proxy is a software proxy that runs on the source, and destination devices. The local proxy relays a data stream over a WebSocket secure connection between the Secure tunneling service and the device application.
The local proxy can work in source
, or destination
mode. The source is usually the laptop or the desktop computer you use to initiate a session with the destination device. The destination device is the remote device you want to access.
For an overview of the process, review the following diagram.
When you create a tunnel, a pair of tokens (one for the source and one for the destination) is created. The source and destination devices use these tokens to connect to the secure tunneling service.
The local proxy establishes a secure WebSocket connection with the tunneling feature using the source or the destination token, depending on the mode used. The token is specified in the request either via cookie, named awsiot-tunnel-token
, or an HTTP request header, named access-token
.
The implementation of WebSockets inside web browsers doesn’t support custom headers. So you must set an awsiot-tunnel-token
cookie using the instruction in the secure tunneling protocol guide.
For security reasons, a website can only set a cookie for its own domain, or any higher-level DNS domain it belongs to. For example, if the domain name of a web application is mylocalproxy.com
, it could not set a cookie for the secure tunneling endpoint named data.tunneling.iot.{aws-region}.amazonaws.com
.
You will use Amazon API Gateway with AWS Lambda proxy integration to set the cookie for the .amazonaws.com domain
.
The cookie is shared across the setting domain and all sibling and child domains including data.tunneling.iot.{aws-region}.amazonaws.com
.
Web browsers might not send the cookie to the domain us-east-1.amazonaws.com
as it is in the public suffix list. This list is used in browsers to limit the scope of a cookie. A manual workaround for us-east-1
Region is to set the cookie in the console of the web browser.
Solution architecture
The following diagram gives an overview of the major steps involved in starting an SSH session from a web application using Secure Tunneling:
- Set a cookie named
awsiot-tunnel-token
with the value of the source token. - Open a secure WebSocket connection between your web application and the tunneling feature.
- Transfer the data using Protocol Buffers library.
In this blog, I describe these three steps in detail starting with how to open a tunnel. Once the tunnel is open, I walk you through how to open a secure WebSocket connection, first from a local machine, setting the source access token via HTTP header.
Then, I explain how to use Protocol Buffers library to transfer data between a source and a destination.
Finally, I describe the solution to set a cookie for the .amazonaws.com domain so the web application can open a secure WebSocket connection passing this cookie.
Prerequisites
This post assumes you have completed the following:
- Read the AWS IoT Secure Tunneling tutorials.
- Complete the open a tunnel and start SSH session to remote device section. This section guides you on how to use secure tunneling, and configure a remote device with the local proxy.
- Node.js and npm installed on your local machine.
Walkthrough
Step 1: Connecting to Secure Tunneling
The first step is to open a tunnel and download the access tokens for the source and destination as described in open a tunnel and start SSH session to remote device.
- a) Create a folder in your local machine. Navigate to this folder, and create a file named
connect.js
. - b) Copy the following Node.js script in your newly created
connect.js
file. Replace the value oftoken
with the access token for the source you have downloaded. Replace the value ofaws_region
with the AWS Region in which the tunnel is open. The access token for the source is used to open a WebSocket connection between your local machine and the tunneling service.
- c) Install the Node.js library ws, with the following command:
npm i --save ws
- d) Run the script:
node connect.js
You see Source is connected to the tunneling service
in your terminal.
- e) In the AWS IoT console, select your tunnel and check that the source is connected.
- f) To connect the destination to the tunneling service, repeat this step. Replace the value of the variable
mode
withdestination
. Replace the value oftoken
with the access token for the destination.
Step 2: Transmitting data through the tunnel
Now that you know how to connect the source and the destination to the tunneling feature, you can transmit data. Secure Tunneling uses protocol buffers to transfer data between the source and the destination.
Protocol Buffers is a mechanism for serializing structured data. Protocol Buffers enables you to specify a schema for your data in a .proto
file.
- a) In the folder created in Step 1, create a file named
schema.proto
Copy the following content into the file:
The previous schema defines a message format for the data with six fields: type
, streamId
, ignorable
, payload
, serviceId
and availableServiceIds
.
The payload
field contains a binary blob of the data to transfer. For more information, review the reference implementation guide V2WebSocketProtocolGuide.
- b) In the same folder, install the library protobufjs that you will use to load the schema and encode/decode the messages:
npm i --save protobufjs
- c) Create two files. Name one file
source.js.
Name the other filedestination.js
. You connect the destination to the tunneling feature and decode the incoming message in the filedestination.js
. You connect the source to the tunneling feature and send a message to the destination with the filesource.js
. - d) Copy the following content in the
destination.js
file. Replace the values fortoken
andaws_region
:
- e) Open the
source.js
file and copy the following code. Replace the values fortoken
andaws_region
.
- f) Open a terminal for the destination. In destination terminal, run the
destination.js
script:
node destination.js
- g) Open an additional terminal for the source. In the source terminal, run the
source.js
script:
node source.js
You see the message Hello from the source
sent by the source (see the variable hello
) received by the destination.
In this step, you transferred simple text between the source and the destination. If there was an SSH session, the payload of the protobuf message would contain an SSH stream.
Step 3: Create the REST API that sets the cookie
Now that you know how to connect and transfer data, the last step is to connect to the tunneling service from a web browser. The implementation of WebSockets inside web browsers doesn’t support custom headers, so you must set a cookie, as described in the Secure Tunneling protocol guide.
To set a cookie to pass the source token for authentication when creating a new WebSocket connection, you create a REST API with Amazon API Gateway with AWS Lambda proxy integration.
The web application sends an HTTP POST request providing the token to the API Gateway endpoint. The Lambda function creates the cookie with the provided token. It responds to the POST API request with the Set-Cookie
HTTP response header to send the cookie to the web application.
The endpoint of the API you create, and the endpoint to connect to the tunneling service are both subdomains of .amazonaws.com
.
Step 3.1: Create the Lambda function to set the cookie
You create a Node.js Lambda function using the Lambda console.
- a) Open the Functions page on the Lambda console.
- b) Choose Create function.
- c) Under Basic information, do the following:
- For Function name, enter
set_cookie_lambda
. - For Runtime, confirm that Node.js 14.x is selected.
- For Function name, enter
- d) Choose Create function.
- e) Under Function code, in the inline code editor, copy/paste the following code:
- f) Choose Deploy.
Step 3.2: Create the Lambda function to enable CORS
For the API to be able to set the cookie, you must enable cross-origin resource sharing (CORS). CORS is a browser security feature that restricts cross-origin HTTP requests that are initiated from scripts running in the browser.
For a CORS request with credentials, you can’t use the wildcard “*
” in the value of Access-Control-Allow-Origin
header. Instead, you must specify the origin.
To support CORS, therefore, a REST API resource must implement an OPTIONS
method that can respond to the OPTIONS
preflight request with at least the following response headers: Access-Control-Request-Method
, Access-Control-Request-Headers
, and the Origin
header.
To do that you will create another Lambda function that will get the origin of the web application from the OPTIONS
method of the API and enable CORS for this specific origin.
Repeat the steps described in the Step 3.1 to create a Node.js Lambda function named enable_cors_lambda.
You create a Node.js Lambda function using the Lambda console.
- a) Open the Functions page on the Lambda console.
- b) Choose Create function.
- c) Under Basic information, do the following:
- For Function name, enter
set_cookie_lambda
. - For Runtime, confirm that Node.js 14.x is selected.
- For Function name, enter
- d) Choose Create function.
- e) Under Function code, in the inline code editor, copy/paste the following code:
- f) Choose Deploy.
Step 3.3: Creating the REST API to set the cookie
Now, you can create the REST API with POST
and OPTIONS
methods that will invoke the Lambda functions set_cookie_lambda
and enable_cors_lambda
respectively.
- a) In the API Gateway console, create a REST API named
SetCookieApi
. - b) Create a method POST.
- Leave the Integration type set to Lambda Function.
- Choose Use Lambda Proxy integration.
- From the Lambda Region dropdown menu, choose the region where you created the
set_cookie_lambda
Lambda function. - In the Lambda Function field, type any character and choose
set_cookie_lambda
from the dropdown menu. - Choose Save.
- Choose OK when prompted with Add Permission to Lambda Function.
- c) Create a method OPTIONS.
- Leave the Integration type set to Lambda Function.
- Choose Use Lambda Proxy integration.
- From the Lambda Region dropdown menu, choose the region where you created the
enable_cors_lambda
Lambda function. - In the Lambda Function field, type any character and choose
enable_cors_lambda
from the dropdown menu. - Choose Save.
- Choose OK when prompted with Add Permission to Lambda Function.
Step 3.4: Deploy the API
- Choose Deploy API from the Actions dropdown menu.
- For Deployment stage, choose
[new stage]
. - For Stage name, enter api.
- Choose Deploy.
- Note the API’s Invoke URL.
You can send a POST request providing the token in the body using the Invoke URL.
The API sends the cookie in the response. When you open the WebSocket connection with the tunneling service, the cookie will be used to authenticate with the tunneling service.
Step 4: Connect to the tunneling feature from a web application
You can now use the SetCookieApi
API in your web application to connect to the tunneling feature.
The following code snippet of an Angular web application shows how to use the REST API to set the cookie:
- You send an HTTP POST request to the SetCookieApi API with the token in the body.
- The API sets the cookie in the response.
- Finally, you open a secure WebSocket connection with the tunneling feature.
Once the WebSocket connection is established, you can transfer data like SSH stream directly from your web application.
You can find an implementation of a web based local proxy in the aws-iot-securetunneling-web-ssh GitHub repository.
You can also test using an online demonstration. The demo user name and the password are both iotcore
.
Cleaning up
To avoid incurring future charges, delete the resources created during this walkthrough.
Conclusion
Secure Tunneling provides a secure, remote access solution that directly integrates with AWS IoT to allow you to access your IoT devices remotely from anywhere.
In this blog, you learned how to use this AWS IoT Device Management feature to gain access to remote devices from a web application. This can simplify the configuration, and reduce the time for troubleshooting devices that are behind firewalls.
You can use this implementation to build or enhance a device management web application to view, interact, and connect to your fleet of devices. You can customize the implementation provided in the aws-iot-securetunneling-web-ssh GitHub repository to build a solution that fits your needs.
You can also test using an online demonstration. The demo user name and the password are both iotcore
.