Implementing 2-Factor Auth For Workflows In Stackstorm Using Inquires

A common request I have seen on the StackStorm Slack Channel is the ability to utilize 2-factor auth before executing a workflow. Users sometimes have very powerful workflows that require extra care, or a second set of eyes before executing. Inquiries are an experimental feature in StackStorm 2.5 (changelog) that allow the workflow to pause and wait for input from a user before proceeding. In this post we’re going to use inquiries to secure a workflow execution using Duo 2-factor authentication (2FA).

Getting Started

The code for this post can be downloaded from the EncoreTechnologies/stackstorm-demo_inquiries GitHub repo. It can be installed using the following command:

st2 pack install https://github.com/EncoreTechnologies/stackstorm-demo_inquiries.git

Workflow V1

The basic workflow we’re going to implement will be restarting a service on the StackStorm box:

actions/service_restart_v1.yaml

---
description: "Restarts a service on the system (without 2FA)"
enabled: true
runner_type: "mistral-v2"
entry_point: workflows/service_restart_v1.yaml
name: service_restart_v1
pack: demo_inquiries
parameters:
  service:
    type: string
    description: "Name of the service to restart."
    required: true

actions/workflows/service_restart_v1.yaml

version: '2.0'

demo_inquiries.service_restart_v1:
  description: Restarts a service on the system (without 2FA)
  type: direct
  input:
    - service

  output:
    result: "{{ _.result }}"
    
  tasks:
    
    service_restart:
      action: core.local_sudo
      input:
        cmd: "systemctl restart {{ _.service }}"
      publish:
        result: "{{ task('service_restart').result }}"

Lets test out our workflow by using it to restart our crond service:

$ st2 run demo_inquiries.service_restart_v1 service=crond
..
id: 59f73f65032fb64ea8a99b6b
action.ref: demo_inquiries.service_restart_v1
parameters: 
  service: crond
status: succeeded
result_task: service_restart
result: 
  failed: false
  return_code: 0
  stderr: ''
  stdout: ''
  succeeded: true
start_timestamp: 2017-10-30T15:04:05.821158Z
end_timestamp: 2017-10-30T15:04:09.121116Z
+--------------------------+------------------------+-----------------+-----------------+-----------------+
| id                       | status                 | task            | action          | start_timestamp |
+--------------------------+------------------------+-----------------+-----------------+-----------------+
| 59f73f66032fb64ea8a99b6e | succeeded (1s elapsed) | service_restart | core.local_sudo | Mon, 30 Oct     |
|                          |                        |                 |                 | 2017 15:04:06   |
|                          |                        |                 |                 | UTC             |
+--------------------------+------------------------+-----------------+-----------------+-----------------+

This action is very powerful and could potentially cause harm if the user restarted a more system critical service. In the following sections we’ll detail how to add 2-factor authentication to this workflow so that services aren’t accidentally restarted without proper approval.

Hard Coded 2-Factor

Our service restart action definitely should not be allowed to run arbitrarily and we would like to confirm this action prior to executing. To accomplish this we’re going to utilize the StackStorm inquiry feature by inserting a new task in the workflow. This task will execute the core.ask action which pauses the workflow and waits for the user to input an approval string.

actions/service_restart_v2.yaml

---
description: "Restarts a service on the system (with a hard coded 2FA check)"
enabled: true
runner_type: "mistral-v2"
entry_point: workflows/service_restart_v2.yaml
name: service_restart_v2
pack: demo_inquiries
parameters:
  service:
    type: string
    description: "Name of the service to restart."
    required: true

actions/workflows/service_restart_v2.yaml

version: '2.0'

demo_inquiries.service_restart_v2:
  description: Restarts a service on the system (with hard coded 2FA)
  type: direct
  input:
    - service

  output:
    result: "{{ _.result }}"
    
  tasks:
    inquiry_2fa:
       action: core.ask
       input:
         route: developers
         schema:
           type: object
           properties:
             secondfactor:
               type: string
               description: "Please enter second factor for restarting the '{{ _.service }}' service"
               required: True
       on-success:
         - service_restart: "{{ task('inquiry_2fa').result.response.secondfactor == 'approved' }}"
              
    service_restart:
      action: core.local_sudo
      input:
        cmd: "systemctl restart {{ _.service }}"
      publish:
        result: "{{ task('service_restart').result }}"

We added a new task to this workflow inquiry_2fa that executes the core.ask action. The action takes an input parameter schema that defines the format/schema expected as a response from the inquiry (for other parameter information please see the core.ask documentation here. This tells StackStorm what information to prompt the user for on the CLI and also allows StackStorm to validate inquiry responses received via the API against the defined schema.

In this workflow we’ve hard coded checking the response of our inquiry against a static string approved:

on-success:
         - service_restart: "{{ task('inquiry_2fa').result.response.secondfactor == 'approved' }}"

Lets test out our new workflow:

$ st2 run demo_inquiries.service_restart_v2 service=crond
.
id: 59f75932032fb64ea8a99b87
action.ref: demo_inquiries.service_restart_v2
parameters: 
  service: crond
status: pausing
start_timestamp: 2017-10-30T16:54:10.727011Z
end_timestamp: None
+--------------------------+---------+-------------+----------+-----------------+
| id                       | status  | task        | action   | start_timestamp |
+--------------------------+---------+-------------+----------+-----------------+
| 59f75933032fb64ea8a99b8a | pending | inquiry_2fa | core.ask | Mon, 30 Oct     |
|                          |         |             |          | 2017 16:54:11   |
|                          |         |             |          | UTC             |
+--------------------------+---------+-------------+----------+-----------------+

As you can see this executed our workflow but put it into a pending state. The core.ask action paused the workflow and is awaiting a response to the inquiry we’ve defined. In order to respond to the inquiry we use the st2 inquiry command.

List all of the pending inquiries:

$ st2 inquiry list
+--------------------------+-------+-------+------------+------+
| id                       | roles | users | route      | ttl  |
+--------------------------+-------+-------+------------+------+
| 59f75933032fb64ea8a99b8a |       |       | developers | 1440 |
+--------------------------+-------+-------+------------+------+

Now we need to respond to our inquiry by entering the string approved:

Note: The inquiry ID is the same as the task ID in the workflow

$ st2 inquiry respond 59f75933032fb64ea8a99b8a
secondfactor: approved

 Response accepted. Successful response data to follow...
+----------+--------------------------------+
| Property | Value                          |
+----------+--------------------------------+
| id       | 59f75933032fb64ea8a99b8a       |
| response | {                              |
|          |     "secondfactor": "approved" |
|          | }                              |
+----------+--------------------------------+

Responding will have resumed our workflow, so lets check on the status:

$ st2 execution get 59f75932032fb64ea8a99b87
id: 59f75932032fb64ea8a99b87
action.ref: demo_inquiries.service_restart_v2
parameters: 
  service: crond
status: succeeded (133s elapsed)
result_task: service_restart
result: 
  failed: false
  return_code: 0
  stderr: ''
  stdout: ''
  succeeded: true
start_timestamp: 2017-10-30T16:54:10.727011Z
end_timestamp: 2017-10-30T16:56:23.626183Z
+--------------------------+------------------------+-----------------+-----------------+-----------------+
| id                       | status                 | task            | action          | start_timestamp |
+--------------------------+------------------------+-----------------+-----------------+-----------------+
| 59f75933032fb64ea8a99b8a | succeeded (1s elapsed) | inquiry_2fa     | core.ask        | Mon, 30 Oct     |
|                          |                        |                 |                 | 2017 16:54:11   |
|                          |                        |                 |                 | UTC             |
| 59f759b5032fb64ea8a99b8c | succeeded (1s elapsed) | service_restart | core.local_sudo | Mon, 30 Oct     |
|                          |                        |                 |                 | 2017 16:56:21   |
|                          |                        |                 |                 | UTC             |
+--------------------------+------------------------+-----------------+-----------------+-----------------+

Our workflow finished successfully and the service has been restarted. This is definitely not the most robust approach as our 2-factor check is comparing against a hard coded string within the workflow. Instead what we’d like to do is use Duo for our 2-factor authentication mechanism.

Duo

Duo is a cloud-based 2-factor authentication service that’s fairly simple and easy to use.

Duo - Setup

To get started we need to sign up for an account on their signup page: https://signup.duo.com.

Duo - Protect An Application

Once you’ve completed the signup and activation process you will be brought to a screen where you’ll add a new application to protect using Duo. In this cas we want to protect a Web SDK application. Type Web SDK into the search bar (1) and click Protect this Application (2):

You will then be brought to a screen containing your new credentials for this application. Save these off, we’ll be using them later for the Duo pack config.

Duo - Adding A User

Duo, by default, does not have any users configured for authentication. To add a new user click Users (1), Add User (2), enter the username (3) and press the Add User button (4).

Now that the user is added we need to add a phone to their account. Scroll down on the users page to the Phones section and click Add Phone (2):

Fill out the phone details and click the Add Phone(1) button:

Once the phone is added it needs to be added, click the Activate Duo Mobile(1):

Click Generate Duo Mobile Activation Code(1):

Click Send Instructions by SMS(1):

At this time, go to your phone and install the Duo app via your phone’s native application store. Once installed click on the link received via SMS, this will add the account to your phone. You should now be ready to authenticate to Duo.

Duo Pack

The next step in our process is integrating StackStorm with Duo. To accomplish this we’ll utilize the stackstorm-duo pack available on exchange. First, we must install the pack:

$ st2 pack install duo

Next, we need to configure the pack to point at our Duo account we just setup. Earlier in this process when we setup the “Protected Application” for Web SDK there were a set of credentials created that we wrote down, we’ll be utilizing them here.

If you forgot to write them down, you can retrieve them in the Duo admin console by going to Applications (1), Web SDK (2):

Credentials are composed of three pieces of information. The table below provides a mapping between the name on the Duo page and the name in the st2 config.

Duo Name

Integration key
Secret key
API hostname

Pack Config

auth_ikey
auth_skey
auth_host

Using this information, configure the Duo pack:

$ st2 pack config duo
[root@nor1devssd01 ~]# st2 pack config duo
admin_host: 
auth_skey (secret): ****************************************
admin_skey (secret): 
auth_host: api-xxx.duosecurity.com
admin_ikey (secret): 
auth_ikey (secret): ********************
---
Do you want to preview the config in an editor before saving? [y]: n
---
Do you want me to save it? [y]: y
+----------+---------------------------------------------------+
| Property | Value                                             |
+----------+---------------------------------------------------+
| id       | 59f7621d032fb64ea8a99b9c                          |
| pack     | duo                                               |
| values   | {                                                 |
|          |     "admin_host": null,                           |
|          |     "auth_skey": "********",                      |
|          |     "admin_skey": "********",                     |
|          |     "auth_host": "api-xxx.duosecurity.com",       |
|          |     "admin_ikey": "********",                     |
|          |     "auth_ikey": "********"                       |
|          | }                                                 |
+----------+---------------------------------------------------+

Make sure to reload the pack config so StackStorm has the latest values in its cache:

$ st2ctl reload --register-configs
Registering content...[flags = --config-file /etc/st2/st2.conf --register-configs]
2017-10-30 13:32:51,369 INFO [-] Connecting to database "st2" @ "127.0.0.1:27017" as user "stackstorm".
2017-10-30 13:32:51,897 INFO [-] =========================================================
2017-10-30 13:32:51,898 INFO [-] ############## Registering configs ######################
2017-10-30 13:32:51,898 INFO [-] =========================================================
2017-10-30 13:32:52,496 INFO [-] Registered 1 configs.
##### st2 components status #####
st2actionrunner PID: 20122
st2actionrunner PID: 20126
st2actionrunner PID: 20133
st2actionrunner PID: 20137
st2actionrunner PID: 20141
st2actionrunner PID: 20146
st2actionrunner PID: 20154
st2actionrunner PID: 20157
st2actionrunner PID: 20160
st2actionrunner PID: 20164
st2api PID: 20069
st2api PID: 20136
st2stream PID: 20082
st2stream PID: 20156
st2auth PID: 20315
st2auth PID: 20388
st2garbagecollector PID: 20200
st2notifier PID: 20056
st2resultstracker PID: 20098
st2rulesengine PID: 20285
st2sensorcontainer PID: 20234
st2chatops PID: 21248
mistral-server PID: 19978
mistral-api PID: 19977
mistral-api PID: 20009
mistral-api PID: 20010

To validate that the config we entered works properly we can check our authentication credentials using the duo.auth_check action:

$ st2 run duo.auth_check
..
id: 59f7626b032fb64ea8a99b9e
status: succeeded
parameters: None
result: 
  exit_code: 0
  result:
    time: 1509384814
  stderr: ''
  stdout: ''

Finally we’ll test out actually authenticating using the duo.auth_auth action. We’ll be authenticating using a passcode, to generate a passcode open up your Duo mobile app and click the key icon (1) next to your account name. This will pop up a 6-digit number (2), that is your passcode.

Using the passcode you just generated run the duo.auth_auth action. The username parameter is the username for additional account we added testuser, not your admin account:

$ st2 run duo.auth_auth username=testuser factor=passcode passcode="1234"
.
id: 59f76368032fb64ea8a99ba7
status: succeeded
parameters: 
  factor: passcode
  passcode: 1234
  username: testuser
result: 
  exit_code: 0
  result:
    result: allow
    status: allow
    status_msg: Success. Logging you in...
  stderr: ''
  stdout: ''

Duo 2-Factor Workflow

Now that we have Duo setup we’re going to use it for our 2FA instead of comparing against our static string.

actions/service_restart_v3.yaml

---
description: "Restarts a service on the system (with duo integrated 2FA)"
enabled: true
runner_type: "mistral-v2"
entry_point: workflows/service_restart_v3.yaml
name: service_restart_v3
pack: demo_inquiries
parameters:
  service:
    type: string
    description: "Name of the service to restart."
    required: true

actions/workflows/service_restart_v3.yaml

version: '2.0'

demo_inquiries.service_restart_v3:
  description: Restarts a service on the system (with duo integrated 2FA)
  type: direct
  input:
    - service

  output:
    result: "{{ _.result }}"
    
  tasks:
    inquiry_2fa:
       action: core.ask
       input:
         route: developers
         schema:
           type: object
           properties:
             username:
               type: string
               description: "Please enter Duo username for restarting the '{{ _.service }}' service"
               required: true
             passcode:
               type: string
               description: "Please enter Duo passcode (generated on the mobile app) for restarting the '{{ _.service }}' service"
               required: true
               secret: true
       on-success:
         - duo_2fa

    duo_2fa:
      action: duo.auth_auth
      input:
        username: "{{ task('inquiry_2fa').result.response.username }}"
        factor: passcode
        passcode: "{{ task('inquiry_2fa').result.response.passcode }}"
      on-success:
        - service_restart
              
    service_restart:
      action: core.local_sudo
      input:
        cmd: "systemctl restart {{ _.service }}"
      publish:
        result: "{{ task('service_restart').result }}"

Now we’ll test out our workflow (v3).

$ st2 run demo_inquiries.service_restart_v3 service=crond
.
id: 59f772a1032fb64ea8a99bd0
action.ref: demo_inquiries.service_restart_v3
parameters: 
  service: crond
status: pausing
start_timestamp: 2017-10-30T18:42:41.554471Z
end_timestamp: None
+--------------------------+----------------------+-------------+----------+-----------------+
| id                       | status               | task        | action   | start_timestamp |
+--------------------------+----------------------+-------------+----------+-----------------+
| 59f772a2032fb64ea8a99bd3 | running (2s elapsed) | inquiry_2fa | core.ask | Mon, 30 Oct     |
|                          |                      |             |          | 2017 18:42:42   |
|                          |                      |             |          | UTC             |
+--------------------------+----------------------+-------------+----------+-----------------+

This time, when responding to the inquiry, we are prompted for both a username and a passcode. The username is your Duo authentication username and passcode is the 6-digit number generated from the Duo mobile app on your phone:

$ st2 inquiry respond 59f772a2032fb64ea8a99bd3
passcode (secret): ******
username: testuser

 Response accepted. Successful response data to follow...
+----------+----------------------------+
| Property | Value                      |
+----------+----------------------------+
| id       | 59f772a2032fb64ea8a99bd3   |
| response | {                          |
|          |     "passcode": "123456",  |
|          |     "username": "testuser" |
|          | }                          |
+----------+----------------------------+

Now that we’ve responded, lets check on our execution and ensure that it finished successfully.

$ st2 execution get 59f772a1032fb64ea8a99bd0
id: 59f772a1032fb64ea8a99bd0
action.ref: demo_inquiries.service_restart_v3
parameters: 
  service: crond
status: succeeded (41s elapsed)
result_task: service_restart
result: 
  failed: false
  return_code: 0
  stderr: ''
  stdout: ''
  succeeded: true
start_timestamp: 2017-10-30T18:42:41.554471Z
end_timestamp: 2017-10-30T18:43:22.038051Z
+--------------------------+------------------------+-----------------+-----------------+-----------------+
| id                       | status                 | task            | action          | start_timestamp |
+--------------------------+------------------------+-----------------+-----------------+-----------------+
| 59f772a2032fb64ea8a99bd3 | succeeded (1s elapsed) | inquiry_2fa     | core.ask        | Mon, 30 Oct     |
|                          |                        |                 |                 | 2017 18:42:42   |
|                          |                        |                 |                 | UTC             |
| 59f772c4032fb64ea8a99bd5 | succeeded (2s elapsed) | duo_2fa         | duo.auth_auth   | Mon, 30 Oct     |
|                          |                        |                 |                 | 2017 18:43:16   |
|                          |                        |                 |                 | UTC             |
| 59f772c6032fb64ea8a99bd7 | succeeded (1s elapsed) | service_restart | core.local_sudo | Mon, 30 Oct     |
|                          |                        |                 |                 | 2017 18:43:18   |
|                          |                        |                 |                 | UTC             |
+--------------------------+------------------------+-----------------+-----------------+-----------------+

Conclusion

This blog post has demonstrated how to utilize the new inquiries feature in StackStorm 2.5 to secure sensitive workflows with 2-factor authentication using Duo. In a future post we’ll be looking at how to respond to inquiries using ChatOps.