Grant temporary SSH access to a server, with approval
This guide builds a workflow for just-in-time SSH access to a bastion host on AWS. An engineer requests access through a form, an on-call lead approves the request by email, and the workflow adds an inbound rule for the engineer’s IP address to the bastion’s security group. When the time the engineer asked for is up, the workflow removes the rule again, so access never stays open because someone forgot to close it.
The workflow also posts each change to a Slack channel. That gives you a record of who had access, from which address, for how long, and who approved it.
The finished workflow has these steps:
Before you start
Section titled “Before you start”- You need the Developer role in a KloudMate workspace whose plan includes workflows.
- You need an AWS connection with the Workflows capability, in the account that holds the bastion.
- You need a Slack connection with the Workflows capability, and a channel for the access records. Invite the KloudMate bot to that channel.
- Copy the ID of the bastion’s security group, such as
sg-0123456789abcdef0, from the EC2 console.
Step 1: Let the AWS connection change the security group
Section titled “Step 1: Let the AWS connection change the security group”Add this policy to the IAM role that your AWS connection uses. Replace REGION, ACCOUNT_ID, and the security group ID with your own values:
The policy lets the workflow add and remove inbound rules on that one security group, and nothing else.
Step 2: Save the security group ID as a variable
Section titled “Step 2: Save the security group ID as a variable”Open Workflows → Variables and click Add variable. Create a Constant named bastion_security_group, with the security group ID as its value.
The workflow reads the ID from this variable. To point it at another security group later, you change the variable instead of editing the workflow.
Step 3: Import the workflow
Section titled “Step 3: Import the workflow”- Copy the YAML below.
- Open Workflows, click Import, paste the YAML, and click Import.
- Click Open workflow, and set these before you publish:
- On Open SSH to the address and Close SSH again, pick your AWS connection.
- On Post in the audit channel and Reply in the audit thread, pick your Slack connection. On Post in the audit channel, also pick the channel.
- On Approve the access, add the people who can approve a request under Approvers.
The security group must be in the AWS connection’s default region. If it’s in another region, set Region on both AWS steps.
How each step works
Section titled “How each step works”The form
Section titled “The form”The Form submission trigger has Sign-in required turned on, so only members of your workspace can submit it. Steps read who submitted it from {{ trigger.submitted_by.name }} and {{ trigger.submitted_by.email }}, which means the requester doesn’t type their own name, and can’t type someone else’s.
The form’s fields are the workflow’s input parameters:
| Field | Type | What it’s for |
|---|---|---|
ip_address | Text | The public IPv4 address the requester connects from. |
duration | Choice | How long access stays open: 1 hour, 4 hours, or 8 hours. |
reason | Long text | Why they need access. It appears in the approval email. |
Is it an IPv4 address?
Section titled “Is it an IPv4 address?”A Branch checks that trigger.inputs.ip_address matches regex ^([0-9]{1,3}\.){3}[0-9]{1,3}$, which accepts four numbers separated by dots, such as 203.0.113.7. If the requester typed anything else, such as a range or a host name, the Else block emails them and the run ends without asking anyone to approve it.
Approve the access
Section titled “Approve the access”An Approval step emails the approvers a link to approve or reject the request. Its Message names the requester, the address, the duration, and the reason:
Timeout is 1h, because a request that nobody looks at within an hour is usually stale. On timeout is Continue, so a timed-out request goes on to the next step instead of failing the run.
If an approver rejects the request, the run ends as Rejected and none of the later steps run.
Approved in time?
Section titled “Approved in time?”A Branch checks that steps.approve.output.approved is not empty. An approval sets it to true, and a timeout leaves it empty. When the request timed out, the Else block emails the requester that nobody approved it.
Open SSH to the address
Section titled “Open SSH to the address”An AWS API Call step calls EC2 → AuthorizeSecurityGroupIngress. Its Parameters set GroupId to {{ vars.bastion_security_group }} and IpPermissions to one rule for TCP port 22:
/32 limits the rule to the one address. The rule’s description names the requester, so anyone who looks at the security group in the EC2 console can tell who the rule is for.
AWS returns the new rule in result.SecurityGroupRules, including its ID, which Close SSH again uses to remove it.
Post in the audit channel
Section titled “Post in the audit channel”A Slack Post Message step posts who got access, from which address, for how long, and who approved it. {{ steps.approve.output.decidedBy.name }} is the approver.
The message leaves out the requester’s reason. Slack reads some text in a message as a mention, so free text from a form could notify a whole channel. The reason stays in the approval email and in run history instead.
Tell the requester access is open
Section titled “Tell the requester access is open”A Send Email step emails the requester at {{ trigger.submitted_by.email }}. Values from the form are passed through escape, so the email shows them as plain text. See Escape values in an email.
How long?
Section titled “How long?”A Route picks the wait that matches the requester’s choice. The one_hour path runs when trigger.inputs.duration equals 1 hour, and the four_hours path when it equals 4 hours. Otherwise covers 8 hours.
Each path holds a Wait / Delay step. Wait for takes a fixed duration, such as 4h, rather than a template, which is why the workflow chooses between steps instead of reading the duration from the form. A wait of a minute or longer doesn’t count toward the runs a workspace can have running at the same time, so a run that waits for 8 hours doesn’t hold anything up.
Close SSH again
Section titled “Close SSH again”An AWS API Call step calls EC2 → RevokeSecurityGroupIngress with the rule’s ID:
Removing the rule by its ID removes exactly the rule this run added. If the same address also has a permanent rule on the security group, that rule stays.
Reply in the audit thread
Section titled “Reply in the audit thread”A Slack Post Message step replies in the thread of the first message. Channel is {{ steps.announce.output.channel }} and Reply to thread is {{ steps.announce.output.ts }}, both from Slack’s response to the first post, so the opening and the closing of each request stay together.
Step 4: Publish and try it
Section titled “Step 4: Publish and try it”The form works only when the workflow is published, so try it on the live form:
- Click Publish. The first publish also switches the workflow on.
- Open the trigger and copy the Public form URL. Open it, sign in if you’re asked to, and request access for your own IP address, with duration set to
1 hour. - Approve the request from the email that the approvers receive.
- Check that the security group has a new inbound rule for your address, and that you can connect over SSH.
An hour later, the workflow removes the rule and replies in the Slack thread. To see the whole cycle without waiting an hour, set Wait 1 hour to 2m while you try it, and set it back to 1h afterwards. Publish after each change.
To follow a request, open Workflows → Runs. A run shows Waiting approval until someone decides, and Waiting while the access is open. See Run history.
Adapt the workflow
Section titled “Adapt the workflow”- Open another port. Change
FromPortandToPorton Open SSH to the address, for example to5432for PostgreSQL. Close SSH again removes the rule by its ID, so it needs no change. - Offer other durations. Add an option to the
durationinput, then add a Route path with a matching condition and a Wait / Delay step. Wait for can be up to24h. - Protect more than one server. Add a Choice input that names the servers, and a Route that picks the security group for each one. Keep each security group ID in its own variable, and add each group to the IAM policy.
- Send the record somewhere else. Replace the two Slack steps with a Microsoft Teams message or an email to your security team, using the same text.
Related
Section titled “Related”- Approvals and collected input for the approval email and its timeout.
- Triggers for the form trigger and Sign-in required.
- AWS API Call for the services a step can call.
- Variables for constants such as the security group ID.