Protecting AWS API Gateway with SGNL

Introduction

Protected Systems are applications, services, or infrastructure that you want to protect with SGNL. In this guide, we’ll enhance access control for your APIs by integrating SGNL with AWS API Gateway to enable continuous access evaluations. If you’re not sure if an AWS API Gateway and SGNL Integration is right for your enterprise, visit our AWS API Gateway Blog Post or watch the video below to learn more.

With this integration, AWS API Gateway need not know about the policies, systems of record, or any of the data in SGNL - it simply needs to pass to SGNL:

  • Who/What is requesting the access (The Principal)
  • (Optional) What is attempting to be accessed (The Asset)
  • (Optional) What operation is being attempted on the asset (The Action)
  • An access token that ensures the Protected System is a legitimate caller into SGNL

Prerequisites

  • An existing AWS API Gateway deployment
  • A SGNL Client
  • A SGNL User Account with Admin privileges
  • At least 1 system of record integrated to SGNL, containing principals and (optionally) assets that will be evaluated in access evaluations
  • (Optional) 1 or more policies that you want assigned to the integration

Creating a Protected System in SGNL

  1. Log-In to your SGNL Client with an Admin Account
  2. From the left navigation pane, select Protected Systems and Add, or simply click Add from the SGNL Dashboard
  3. Select ‘AWS API Gateway’ from the API Gateways category in the list of integrations
  4. Give your integration a descriptive display name and description
  5. Specify the Default Policy to be applied to AWS API Gateway
    • Allow: If no policies provide a decision for an access request, SGNL will respond to the access request with an Allow decision
    • Deny: If no policies provide a decision for an access request, SGNL will respond to the access request with a Deny decision
  6. Next, you’ll need to configure which identifier AWS API Gateway is using to describe your user/principal
    • This may be an email address or Username that can be found in your IdP, or an EmployeeID in your HRIS system. This should be in the format of the Principal ID of the user that will request access to the Protected System.
    • e.g. If an Okta user will be requesting access to this Protected System, AWS API Gateway, the principal identifier should be the Okta email address.
  7. You’ll also need to define the types of Assets that AWS API Gateway is protecting
    • This might be customer identifiers sourced your CRM system, or products that your customers are buying
  8. Once configured, click Continue to save your AWS API Gateway configuration and move on to other configuration steps

Configuring Authentication

  1. Authentication ensures that only authorized systems can make requests into SGNL, as well as verifying the identity of an integration in order to effectively evaluate Policies - to access Authentication settings, open your AWS API Gateway protected system and select the Authentication tab

    SGNL - Authentication

  2. Click Generate Token

  3. Give your token a descriptive name so that you know how it’s being used in the future and click to Generate Token

    SGNL - Generate Token

  4. On the next screen, copy the token - this will be used by AWS API Gateway to make access requests to SGNL using the SGNL Access Service API

    Note: The value of this token is not available again after this screen, so ensure you securely store it for steps later in this guide

    SGNL - Token

Integrating AWS API Gateway with SGNL

Below is an example sequence diagram for an API request, and having that request be authorized by the SGNL policy engine.

Flow Diagram

The steps:

  1. An API client (e.g. application, microservice) makes a request to the AWS API Gateway containing the request context (e.g. principal (e.g. user), API resource, and HTTP verb “POST”).
  2. The AWS API Gateway instantiates the configured Lambda authorizer, and passes the request context information and sends an authorization query request to the SGNL access service API.
  3. The Lambda authorizer sends an authorization request to the SGNL policy engine.
  4. The SGNL policy engine calculates the human-readable policies associated with the authorization request in milliseconds and determines a final decision. (A) The policy calculation results in an allow. (B) The policy calculation results in a denial.
  5. (A) The Lambda authorizer returns an allow IAM policy to the AWS API Gateway. (B) The Lambda authorizer returns a deny IAM policy to the AWS API Gateway.
  6. (A) If the decision returned by the Lambda authorizer is denied, the AWS API Gateway denies the request for the API resource. (B) If the decision is allowed, the AWS API Gateway proxies the request to the backend API.
  7. The backend API service sends a response back to the AWS API Gateway.
  8. The AWS API Gateway sends the response to the API client along with an HTTP 200 status.

The AWS API Gateway uses a Lambda Authorizer to call out to SGNL.

The following code snippet is an example implementation of the Lambda authorizer handler. This is the function the AWS API Gateway calls for every protected API request. The request context is passed in the function’s request parameter.

You can download the full Lambda authorizer example from the SGNL examples repository.

Code snippet from Lambda authorizer

// Lambda Authorizer Handler

func sgnlAuthorizer(ctx context.Context, request events.APIGatewayCustomAuthorizerRequestTypeRequest) (events.APIGatewayCustomAuthorizerResponse, error) {

	// Print MethodArn for debug and testing
	log.Println("SGNL Authorizer: Method ARN: " + request.MethodArn)

	// Get bearer token for calling the SGNL API from the configured "token" environment variable. In production it's advisable to get the toekn from a key vault.
	token := os.Getenv("token")

	// Get the SGNL Access Service URL from the Lambda environment variables.
	sgnlUrl := os.Getenv("sgnl_url")

	// Get the principal id from the request. In production implementations it's advisable to get the principal from an encrypted and signed IDP JWT.

	principalID := request.QueryStringParameters["principal"]

	// Parse MethodArn
	tmp := strings.Split(request.MethodArn, ":")
	apiGatewayArnTmp := strings.Split(tmp[5], "/")
	awsAccountID := tmp[4]
	method := request.HTTPMethod
	path := request.Path

	//Initialize response document

	resp := NewAuthorizerResponse(principalID, awsAccountID)
	resp.Region = tmp[3]
	resp.APIID = apiGatewayArnTmp[0]
	resp.Stage = apiGatewayArnTmp[1]

	// you can send a 401 Unauthorized response to the client by failing like so:

	if len(principalID) == 0 {
		log.Println("SGNL Authorizer: Error, empty principal id.")
		return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized")
	}

	// Construct the query string. You can build this string from any input source such as an HTTP(S) request context or API client.

	principalJson := `{"Id":"` + principalID + `"}`
	queriesJson := `[{"assetID":"` + path + `","action":"` + method + `"}]`

        // Log for test purposes
        log.Println("Principal ID:", principalID)
	log.Println("AssetID:", path)

	// Initialize the query variable as an array of SGNL queries, based on the queries struct.
	var queries []Queries
	var principal Principal

	// Unmarshall the query string into the query array struct.

	json.Unmarshal([]byte(queriesJson), &queries)

	// Unmarshall the principal string into the principal struct.

	json.Unmarshal([]byte(principalJson), &principal)

	// Build final JSON request body using the SGNL request struct.

	sgnlReq1 := &sgnlRequest{
		Principal: principal,
		Queries:   queries}

	// Finally marshall the JSON into a final request variable containing the JSON request body.

	sgnlReq2, _ := json.Marshal(sgnlReq1)

	// Log final request body for test purposes
log.Println("SGNL Authorizer: SGNL Request:", string(sgnlReq2))

       // Initialize http client

	client := &http.Client{}

	// Initialize request

	req, err := http.NewRequest(method, sgnlUrl, bytes.NewBuffer(sgnlReq2))

	if err != nil {
		fmt.Print("Error while initializing request.")
		return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized")
	}

	// Add headers to request

	req.Header.Add("Authorization", token)
	req.Header.Add("Content-Type", "application/json")

	// Make the request to the SGNL Access Service API
	response, err := client.Do(req)

	if err != nil {
		fmt.Print("Error when making SGNL Request.")
		return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized")
	}

	defer response.Body.Close()
	body, err := io.ReadAll(response.Body)

	//Print the JSON response body in case of error. Return unauthorized to the AWS API Gateway.
	if err != nil {
		fmt.Print("Error while reading response body.")
		return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized")
	}

	// Check response from SGNL access service API
	var jsonResponse sgnlResponse

	json.Unmarshal([]byte(body), &jsonResponse)

       // If error, make sure to return unauthorized to the AWS API Gateway.
	if err != nil {
		log.Println("could not unmarshal json: %s\n", err)
		return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized")
	}

	// Prepare policy document based on SGNL access service API.

      // Check decision from the SGNL access service API.
	if jsonResponse.Decisions[0].Decision == "Allow" {
		log.Println("SGNL Authorizer Decision:", jsonResponse.Decisions[0].Decision)
		resp.AllowMethod(method, path)
		return resp.APIGatewayCustomAuthorizerResponse, nil

	} else {
		// Deny request
    log.Println("SGNL Authorizer Decision:",       jsonResponse.Decisions[0].Decision)
		resp.DenyAllMethods()
		return resp.APIGatewayCustomAuthorizerResponse, nil

	}
}

The full configuration details for running this example are available in the SGNL Examples Repo on GitHub

Assigning Policies

  1. Once the Integration is created, you can start assigning versions of Policies to the integration - to get started, select Policies from the tabs in your newly created integration

    SGNL - Policies

  2. Select ‘Assign Policies’

  3. Select:

    • The Policies you want to apply to the integration with the check box
    • The version of the Policy you want applied

    SGNL - Select Policies

  4. Click Next once you have the Policies and Versions configured as is appropriate

  5. Select the Enforcement mode for the Policies you chose in the previous step

    • Simulated: Policy Versions that are being simulated will only log their access decision in the SGNL logs and will not impact the access decision that SGNL hands back to an integration. Simulated policies are useful for performing what-if analysis of new policy versions as well as debugging policy changes.

      Note: It’s considered best practice to start with policies in Simulated mode, to verify that policies have been created an applied as expected

    • Enforced: Policy Versions that are being enforced will impact the access decisions that SGNL hands back to an integration. Enforced Policies will determine access for an integration

    SGNL - Set Enforcement

  6. Select your desired Enforcement mode and select Assign

  7. Versions of Policies will now be Assigned to your integration

    SGNL - Policy Assignments