Just in time Access
Access when you need it!
Just in time access
Introduction
Bruce is a lightweight build server designed for efficient iteration and seamless integration with your workflows. In this tutorial, we'll explore the concepts of enabling just-in-time access to your systems and applications.
Requirements & Getting Started
Setting up at least a basic Bruce installation, or following the Build Server guides, will provide a foundation for understanding the concepts in this guide. For reference, visit the Build Server, Part 1 guide. Key points that we will focus on include the ability to make use of the Bruce workspace actions to trigger specific events that enable just in time access to your systems.
Theory and architecture behind this problem
Just in time access implies the ability to grant access to a system or application when it is needed, and to revoke that access when it is no longer required. This is a common problem in many organizations, where access to systems is granted for a specific purpose, but the access is not revoked after the task is completed. This can lead to security vulnerabilities and potential data breaches. Bruce provides a solution to this problem by enabling you to create events that can trigger a door opening from the inside of your network, allowing access to a system or application for a specific period of time. This access can be revoked automatically after the task is completed, ensuring that your systems remain secure. The notion of opening the door from the inside ensures you have a guard internal that can open the door which is something that is extremely hard to do with traditional systems. Let’s dive into how Bruce can help solve this problem.
To reduce the complexity of the problem, we will focus on a simple use case of fully disabling ssh on a linux instance, and only enabling it when access is required, then shortly afterwards disabling it again ensuring that the access is removed once a time window has passed.
Lets build it
To get started, we first need to create a new agent and action on the system. We can borrow from the build server guides to bootstrap the beginning of this process. The first step is to create a systemd unit file that starts the agent on boot and ensures that it is always running. This is a simple process that can be done by creating a new file in /etc/systemd/system/bruce-agent.service with the following contents:
[Unit]
Description=Bruce Build Server
After=docker.service
Requires=docker.service
[Service]
User=bruce
Group=bruce
TimeoutStartSec=5
Restart=always
ExecStart=/usr/local/bin/bruce server /etc/bruce/node.yml
[Install]
WantedBy=multi-user.target
As with before the unit file starts bruce in server mode and points to a local file (can be remote also, for the root configuration). An example of this base configuration looks like below:
---
endpoint: <Agent Endpoint>
runner-id: <Agent ID>
authorization: <Auth Key>
execution:
- name: node action
action: default
type: event
target: scp://secure-access-server.yourdomain.com/$(hostname -f)/agent.yml
- name: just in time ssh
action: jit-ssh-on
type: event
target: /etc/bruce/just-in-time-ssh-on.yml
- name: just in time ssh off
action: jit-ssh-off
type: event
target: /etc/bruce/just-in-time-ssh-off.yml
Some key items to review here which is slightly different from previous guides include the use of the scp:// protocol to pull the agent configuration from a remote server. This is a powerful feature of Bruce that allows you to centralize your agent configurations and ensure that they are always up to date. This allows you to dynamically adapt the agent configuration for your systems as time goes on. As of this writing the configuration can be read from a local file, s3, scp, http or https locations, giving a massive amount of flexibility not only in how you configure your agents but also in how you manage and maintain them.
The new action that was created include a custom actions called jit-ssh-on and jit-ssh-off which will be used to enable and disable ssh on the system. So lets take a look at what these manifests may look like below.
Keeping it Simple
For illustration purposes we will keep this very short and simple, for simplicity sake we'll assume bruce is running as root here. The first manifest enables SSH on the system, and the second disables it:
/etc/bruce/just-in-time-ssh-on.yml:
---
steps:
- cmd: /usr/bin/systemctl start sshd
/etc/bruce/just-in-time-ssh-off.yml:
---
steps:
- cmd: /usr/bin/systemctl stop sshd
Let’s Review
While this is an extremely simple example, in reality you may want to set SELinux contexts, update the firewall or updating additional things, the key takeaway here is that even in this simple example we can ensure that SSH is only enabled when it is needed and disabled when it is not. This capability ensures your systems are secure by granting access only when required and revoking it once it's no longer needed.
Make It Actionable
The final part of this guide is to make it actionable from outside of the system. Something sends our man on the inside a message to open the door. This is done in the bruce.tools workspace. As before we create 2 new actions in the workspace that map to the jit-ssh-on and jit-ssh-off actions. Once these are created you can trigger them by clicking the play button in the workspace. You can also trigger them via their respective endpoints, be sure to enable a security key for these actions to ensure that only authorized users and systems can trigger them.
For brevity the process of creating the actions is not shown here, but it is the same as in the build server guides. The key takeaway is that you can create custom actions to enable and disable SSH on your systems, triggered securely from external sources within your control.
Endpoint Security
To ensure that these capabilities do not get misused you should consider adding a security context to your actions and agents. This ensures that you make use of a secure token that will be able to trigger the endpoint and no other means will be allowed, preventing a regular person from doing a curl command to gain access to your box without the appropriate entitlements or access.
Once you apply a Security Context (Checkbox when creating & editing your action) you will be presented by a Secret Key input box that you can either use the generate button to generate a key or use your own provided key. The key will be used in order to generate the bearer token that will be evaluated on the server side. Examples of how to create a sha-256 bearer token is below:
Bash
#!/bin/bash
# Variables
ENDPOINT="https://bruce.tools/your-endpoint"
SECURITY_KEY="your-security-key"
TIMESTAMP=$(date +%s)
# Create payload and token
PAYLOAD="${\"key\":\"$SECURITY_KEY\",\"iat\":$TIMESTAMP}"
TOKEN=$(echo -n $PAYLOAD | openssl dgst -sha256 -hmac $SECURITY_KEY | awk '${print $2}')
# Make the GET request
curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" $ENDPOINT
Golang
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"time"
)
func main() ${
endpoint := "https://bruce.tools/your-endpoint"
securityKey := "your-security-key"
// Generate Bearer Token
timestamp := time.Now().Unix()
payload := fmt.Sprintf(`${"key":"%s","iat":%d}`, securityKey, timestamp)
token := generateHMACSHA256(payload, securityKey)
// Make GET request
client := &http.Client${}
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil ${
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil ${
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
fmt.Println("Response Status:", resp.Status)
}
// Function to create an HMAC-SHA256 signature
func generateHMACSHA256(payload, key string) string ${
h := hmac.New(sha256.New, []byte(key))
h.Write([]byte(payload))
return hex.EncodeToString(h.Sum(nil))
}
Python
import hashlib
import hmac
import time
import requests
# Variables
ENDPOINT = "https://bruce.tools/your-endpoint"
SECURITY_KEY = "your-security-key"
# Generate Bearer Token
timestamp = int(time.time())
payload = f'{{"key":"{SECURITY_KEY}","iat":{timestamp}}}'
token = hmac.new(SECURITY_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest()
# Make GET request
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.get(ENDPOINT, headers=headers)
print("Response Status:", response.status_code)
print("Response Body:", response.text)
JavaScript
const crypto = require("crypto");
const https = require("https");
// Variables
const endpoint = "https://bruce.tools/your-endpoint";
const securityKey = "your-security-key";
// Generate Bearer Token
const timestamp = Math.floor(Date.now() / 1000);
const payload = `{"key":"${securityKey}","iat":${timestamp}}`;
const token = crypto.createHmac("sha256", securityKey).update(payload).digest("hex");
// Make GET request
const options = {
hostname: "bruce.tools",
path: "/your-endpoint",
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
}
};
const req = https.request(options, (res) => {
console.log(`Status Code: ${res.statusCode}`);
res.on("data", (data) => {
process.stdout.write(data);
});
});
req.on("error", (e) => {
console.error(`Error: ${e.message}`);
});
req.end();
Conclusion
In this guide we have looked at how to enable just in time access to your systems and applications using Bruce. By creating custom actions that can be triggered from the outside, you can ensure that access is only granted when it is needed and that it is revoked when it is no longer required. This is a powerful capability that can be used to ensure that your systems are secure and that access is only granted when it is needed. Additionally, this capability can extend to running automated playbooks and other tasks, eliminating the need for manual system access. We’ll explore this further in an upcoming NOC/SOC runbook guide.