A common question in the #Kolide channel in the Osquery Slack is how to use the Kolide Fleet API. Kolide Fleet is written in GoLang and utilizes the GoKit framework to build the application. Therefore, almost every action that can be performed via the WebGUI is an API call. This blog post is going to demonstrate how to use the Kolide Fleet API to perform actions such as creating a live query and obtaining the results using Python websockets, obtaining the Osquery enroll secret, and creating a saved query. In addition to the API, this blog post will demonstrate how to use the Fleetctl command-line tool to perform the same actions. Lastly, this blog post includes an Ansible playbook to automate deploying Oquery agents and registering them with Kolide.
WARNING
This blog post is demonstrating how to use the Kolde API but it does not supersede the official documentation released by Kolide. Please use this blog post as a guide and not as official documentation.
WARNING
Background
What is GoKit?
Go kit is a programming toolkit for building microservices (or elegant monoliths) in Go. We solve common problems in distributed systems and application architecture so you can focus on delivering business value.
Spin up Kolide with Docker
git clone https://github.com/CptOfEvilMinions/Kolide-Docker.git
cd Kolide-Docker
docker-compose build
docker-compose run --rm kolide fleet prepare db --config /etc/kolide/kolide.yml
docker-compose up -d
- For the remainder of this setup please refer to this README
API overview
Since Kolide Fleet uses the GoKit framework, you can think of the handler.go file as your API guide. At first, this file looks intimidating but once you understand how to read the code it becomes pretty simple. To start, scroll down to line 418 which is the start of the following function: func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers)
. In this function Kolide inits every API URL endpoint.
On line 419 there is the HTTP handler for the login function (h.Login
), which requires a POST request (Methods("POST")
), and the HTTP request should be sent to the following URI: /api/v1/kolide/login
. At line 454 (screenshot below) is the HTTP handler to get information about a query (h.GetQuery
), which requires a GET request (Methods("GET")
), the HTTP request should be sent to the following URL: /api/v1/kolide/queries/{id}
, and the URL requires an ID to be specified. If you click h.GetQuery
it will generate a pop-up like the screenshot below and you want to select the section that has a GoLang function that starts with func (c *Client)
.
This will redirect your browser to the GoLang code (screenshot below) responsible for handling requests for the following URI: /api/v1/kolide/queries/{id}
. The GetQuery function takes in a string parameter which is the ID specified in the URL. Next, the Kolide API generates the URL and queries that endpoint. Before the request makes it to the specified URI, the request is first authenticated. If the request is successful, the result of the request is returned to the user: return responseBody.Spec, nil
. Hopefully, this provided a high overview of how to navigate the handler.go file, which is your guide to the Kolide API.
Authenticating to Kolide
Intro
Kolide implements JSON web tokens (JWT) to authenticate each request to perform an action with the Kolide Fleet API. This blog post is not going to cover how JWTs work but essentially think of it as an “API key”. Retrieving a JWT is the first roadblock users have when they want to use the Kolide API. Below, I am going to demonstrate how to obtain a JWT with a CURL request.
Request a JWT
curl -k -X POST https://<Kolide FQDN>:<Port>/api/v1/kolide/login -d '{"Username": "<admin username>", "Password": "<admin password>"}'
export KOLIDE_TOKEN=$(curl -k -X POST https://<Kolide FQDN>:<Port>/api/v1/kolide/login -d '{"Username": "<admin username>", "Password": "<admin password>"}' | jq -r '.token')
- Save the Kolide token to an environment variable
echo $KOLIDE_TOKEN
Kolide API actions
Request Osquery enroll secret
curl -k -X GET https://<Kolide FQDN>:<Port>/api/v1/kolide/spec/enroll_secret -H "Authorization: Bearer ${KOLIDE_TOKEN}"
Create a query with Kolide API
curl -k -X POST https://<Kolide FQDN>:<port>/api/v1/kolide/queries -H "Authorization: Bearer ${KOLIDE_TOKEN}" -d '{"query": "SELECT * FROM osquery_info", "name": "Test"}'
Execute saved query with Kolide API
The first CURL statement will request a saved query to be executed by using Kolide label IDs and Kolide hosts IDs. The second CURL statement will request a saved query to be executed by using Kolide label-friendly names and Kolide hosts friendly names.
curl -k -X POST https://<Kolide FQDN>:<port>/api/v1/kolide/queries/run_by_names -H "Authorization: Bearer ${KOLIDE_TOKEN}" -d '{"query": "SELECT * FROM osquery_info", "selected": {"Labels": ["<Kolide label friendly names comma seperated>"], "Hosts": ["<Kolide label friendly names comma seperated>"] }}'
- Using IDs
curl -k -X POST https://<Kolide FQDN>:<port>/api/v1/kolide/queries/run -H "Authorization: Bearer ${KOLIDE_TOKEN}" -d '{"query": "<Name of saved query>", "selected": {"Labels": [<Kolide label IDS commaa seperated>], "Hosts": [<Kolide host IDS comma seperated>] }}'
- Using Kolide friendly names
Execute live/distributed query with Kolide API
The first CURL statement will create a live query by using Kolide label IDs and Kolide hosts IDs. The second CURL statement will create a live query by using Kolide label-friendly names and Kolide hosts friendly names.
curl -k -X POST https://<Kolide FQDN>:<port>/api/v1/kolide/queries/run -H "Authorization: Bearer ${KOLIDE_TOKEN}" -d '{"query": "SELECT * FROM osquery_info", "selected": {"Labels": [<Kolide label IDS commaa seperated>], "Hosts": [<Kolide host IDS comma seperated>] }}'
- Using IDs
curl -k -X POST https://<Kolide FQDN>:<port>/api/v1/kolide/queries/run_by_names -H "Authorization: Bearer ${KOLIDE_TOKEN}" -d '{"query": "SELECT * FROM osquery_info", "selected": {"Labels": ["<Kolide label friendly names comma seperated>"], "Hosts": ["<Kolide label friendly names comma seperated>"] }}'
- Using Kolide friendly names
Get query results with Kolide API
Getting the results is not as simple as sending an HTTP GET request because the endpoint for retrieving the results is a WebSocket. At first, it didn’t make sense why Kolide did it this way but once you understand the reasoning it makes sense. When you submit a query to Kolide all the endpoints are checking in at different times to get the latest tasks therefore all the results will come in at different times. A WebSocket allows the client to continually “poll” the Kolide server for the latest results as they come in.
Unfortunately, as stated above, it’s not a simple CURL request to get these results. To demonstrate how this works I created a Python script that is thoroughly commented to help engineers understand how to create automations with the Kolide API. I will describe at a high level the process:
- Generate a Kolide JWT as demonstrated above
line: 127: def authenticateToKolide()
- Create a live query as demonstrated above
line 103: def createKolideLiveQuery()
- Create a Python WebSocket, which will be used for the remainder of the process
line 57: ws = create_connection(kolide_websocket_uri, sslopt={"cert_reqs": ssl.CERT_NONE})
- Send our Kolide JWT to the server to authenticate our WebSocket
line 61: ws.send(generateJSONAuthHeader(kolide_token))
- Send the Kolide query campaign ID, which identifies the query we want to retrieve results from
line 65: ws.send(generateKoldieQueryCampaignIDJSONpayload(koldie_query_campaign_id))
- Python script continually “polls” the server for new results until the server returns
'{"status": "finished"}'
line 92: if result.get("type") == "status" and result.get("data").get("status") == "finished":
- Close WebSocket
line 95: ws.close()
- Print results to console for user
line 98: print (result_list)
Next, I will demonstrate how to use the Python script provided to obtain results of a query
git clone https://github.com/CptOfEvilMinions/BlogProjects
cd BlogProjects/kolide-api-ansible
virtualenv -p python3 venv
source venv/bin/activate
pip3 install -r requirements.txt
python3 kolide_websocket_client.py --campaign_id <X>
- Enter Kolide username
- Enter Kolide password
- Enter Kolide URL
Submit a live query and get results with Python script
python3 kolide_websocket_client.py
- Enter Kolide username
- Enter Kolide password
- Enter Kolide URL
- Enter a list of hosts that are comma-separated
- Enter a list of labels that are comma-separated
Fleetctl
Download and init Fleetctl
wget https://github.com/kolide/fleet/releases/download/3.2.0/fleet.zip
unzip fleet.zip
mv <OS - macOS:darwin, linux:linux>/fleetctl /usr/local/bin/fleetctl
fleetctl config set --address https://<Kolide FQDN>:<port> --tls-skip-verify
- Set the Fleet API address
- Only specify
--tls-skip-verify
if you have a self-signed certificate
fleet login
- Enter Kolide user e-mail
- Enter Kolide user password
Create a live query
fleetctl query --query "<Osquery query>" --hosts <Kolide friendly name>
Creating a pack
wget https://raw.githubusercontent.com/osquery/osquery/master/packs/ossec-rootkit.conf
fleetctl convert -f ossec-rootkit.conf > ossec-rootkit--pack-fleet.yaml
- Convert JSON to YAML
fleetctl apply -f ossec-rootkit--pack-fleet.yaml
GOquery
This is probably one of the coolest features of Fleetctl. GOquery allows you to connect the endpoint in real-time and run queries as if you were at the console using osqueryi
. To demonstrate this feature I will connect to a Windows host, move around the file system, and request information about the Osquery instance running.
fleetctl goquery
.connect <Osquery UUID>
cd C:\Windows\debug
ls
.query SELECT * FROM SELECT * FROM osquery_info
Setup Fleetctl with BURP
To determine some of the API calls above, I ran the Fleetctl tool and captured the HTTP payloads with BURP.
- Download and install BURP
- Start BURP and create an intercepting proxy on
localhost:8080
HTTP_PROXY=http://127.0.0.1:8080 fleetctl query --query "SELECT * FROM osquery_info" --hosts ubuntuvm
- Go to Burp > Proxy > WebSockets history
Automate enrolling Osquery endpoints
Code break down
This section will demonstrate how to use Ansible to automate enrolling Osquery endpoints to Kolide. First, Ansible starts by installing Osquery on a set of machines. Second, Ansible will copy the Osquery.flags
file. Third, Ansible reads the controller’s (machine running the Ansible playbook) environment variables for a variable named KOLIDE_TOKEN
. Ansible will use the JWT assigned to this environment variable to authenticate the request to request the Osquery enroll secret from Kolide. Lastly, Ansible will start the OsqueryD service, which will call back to Kolide to register itself.
Configure Ansible playbook
git clone https://github.com/CptOfEvilMinions/BlogProjects
cd BlogProjects/kolide-api-ansible
mv group_vars/all.yml.example group_vars/all.yml
vim group_vars/all.yml
and set:kolide_hostname
– Set to the FQDN or IP address of Kolidekolide_port
– Set to the port Kolide is listening on
vim host.ini
add a list of IP addresses to install Osquery on under[osquery_linux]
Configure Ansible playbook for Windows
mv group_vars/windows.yml.example group_vars/windows.yml
vim group_vars/windows.yml
and set:ansible_user
– Set to a Windows admin useransible_password
– Set password for user
vim host.ini
add a list of IP addresses to install Osquery on under[osquery_windows]
Run Ansible playbook
KOLIDE_TOKEN=$(curl -k -X POST https://<Kolide FQDN>:<Port>/api/v1/kolide/login -d '{"Username": "<admin username>", "Password": "<admin password>"}' | jq '.token')
ansible-playbook -i hosts.ini deploy_osquery_linux.yml -u superadmin -K
- Deploy Osquery on Linux
ansible-playbook -i hosts.ini deploy_osquery_windows.yml
- Deploy Osquery on Windows
Discussions
NO RBAC support
Kolide Fleet does not support role-based access control (RBAC). This means all users are essentially admin users. This means you need to be careful with the operation and handling of Kolide credentials and JWTs.
Lessons learned
I am currently reading a book called “Cracking the Coding Interview” and it is a great book. One interesting part of the book is their matrix to describe projects you worked on and the matrix contains the following sections which are: challenges, mistakes/failures, enjoyed, leadership, conflicts, and what would you do differently. I am going to try and use this model at the end of my blog posts to summarize and reflect on the things I learn. I don’t blog to post things that I know, I blog to learn new things and to share the knowledge of my security research.
New skills/knowledge
- Learned how to utilize the Kolide API
- Learned more about WebSockets
- How to implement a Python client that uses WebSockets
Challenges
- Learning how to interact with WebSockets. I had to use BURP to intercept the traffic from the Fleetctl CLI tool to Kolide to understand the payloads.
What You’d Do Differently
- Created CURL statements or Python code for creating/updating Osquery query packs. Osquery packs are written in JSON and Kolide Fleet only accepts YAML files. The Fleetctl CLI tool can convert the pack from JSON to YAML but at that point, you might as well use the Fleetctl tool to upload the file as well.