Home SIEM Part 1: AdGuardHome

08/04/2025

Introduction

I've been building a SIEM at home. For no reason then just playing. My main goal was to use the hardware I had, nothing new. My home router is a GL.iNet AX1800 which comes with a preconfigured AdGuardHome option for DNS. Which is great, AdGuard is awesome I used to host it on a separate server but I wanted to have all my network infrastructure on a single device so I can easily run it during power outages.

AdGuard has a 2 obvious options for getting logs: local file or syslog. When hosting it on a raspberry pi, it was easy to ship the logs using filebeat from file. What's great about the file is the json format ships beautifully to elastic.

Logs from Syslog

AdGuard on GL.iNet ships it's logs to syslog by default. I was excited about it, until I saw the logs:

2025/07/30 17:23:16.167173 12904#5146 [debug] github.com/AdguardTeam/dnsproxy/proxy.(*Proxy).logDNSMessage(): IN: ;; opcode: QUERY, status: NOERROR, id: 35122 ;; flags: rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;example.com IN A

I've ingested space delimited grok and regex logs before, so I wasn't too concerned. Until I realized the log changed depending on the request method, response count, response type, etc. My regex got so huge and hard to understand I decided there had to be another way. I also relized after writing regex for a few hours that the logs didn't have the requesting client IP. Syslog wasn't going to work.

Logs from File

I started looking for other options. I decided saving logs to a file made the most sense, first AdGuard supported it, second I've done it before so it would be simple. It wasn't and I had to get around a few hurdles.

First, GL.iNet's AdGuard configuration forces syslog regardless of the configuration file. Simple enough fix, just modify the init file. It worked! I just removed -l syslog from this line:

procd_set_param command /usr/bin/AdGuardHome --glinet --no-check-update -c /etc/AdGuardHome/config.yaml -w /etc/AdGuardHome -l syslog

Removing that argument got rid of the forced syslog and allowed AdGuard to follow the configuration.

Next hurdle, disk space. I had forgotten I'm dealing with a router that has very limited resources, I had 30 mb of disk space to play around with. This is doable, just pull the data every few minutes and delete the logs. I was worried about eating that disk space with logs and the init file getting removed upon update. So I decided to try another route.

Logs from API

I finally landed on the solution I'm currently using, pulling logs via API. Adguard uses openapi for it's API and it's very well documented. Reading the openapi.yaml file from AdGuardHome Github I decided to use /querylog. It was perfect: - Same json results as file logs - Has a result limit - Has an offset - Has a time filter (a bit limited can only do "older than")

I ended up writing a really simple python script that runs from cron every 5 minutes. The logs are appended to a file and log rotate runs every day to prevent eating all my disk space. I'm using filebeat to ship the logs to my elastic cluster.

import requests
import json
import time
import datetime


url = "http://IP:3000/control/querylog?limit=500"
username = "user"
password = "pass"


def pull():
    limit = datetime.datetime.now() - datetime.timedelta(minutes=1)
    r = requests.get(url, auth=(username, password))

    if r.status_code == 200:
        data = json.loads(r.text)
    else:
        print("Returned unexpected status code of {}".format(r.status_code))
        if r.text:
            print(r.text)
        print("Exiting")

    results = []
    for i in data['data']:
        event_time = time.mktime(time.strptime(i['time'].split('.')[0], "%Y-%m-%dT%H:%M:%S"))
        if limit.timestamp() < event_time:
            results.append({
                "message": i
            })

    if results:
        with open("logs/resolve-{}.log".format(limit.strftime('%Y-%m-%d:%H:%M')), "w") as file:
            for i in results:
                file.write(json.dumps(i))
                file.write("\n")


if __name__ == '__main__':
    pull()

Filebeat configuration can be annoying when dealing with json. Here's what I landed on.

filebeat.inputs:
- type: filestream
  id: adguardhome
  enabled: true
  paths:
    - /home/user/apps/adguard_log_pull/logs/*.log
  processors:
    - decode_json_fields:
        fields: ["message"]
        process_array: false
        target: "dns"
        overwrite_keys: false
        add_error_key: true
    - timestamp:
        field: "dns.time"
        layouts:
          - "2006-01-02T15:04:05.999-07:00"
        test:
          - "2024-11-21T12:20:12.458792613-07:00"
    - add_fields:
        fields:
          source: "dns"

Last problem. The GL.iNet AdGuardHome configuration does not come with any user profiles. You're authenticated through the GL.iNet menu. In order for the API to work I had to add a user to the AdGuardHome config. The AdGuardHome configuration is located at /etc/AdGuardHome/config.yaml. Replace the line:

users: []

with

users:
  - name: username
    password: $2a$10$WDBCv0GwxCYv76Z8lzrtg.6iBU/hNp.OiomTBRcUm/gbir4r4CtP6

The password is a BCrypt hash use AdGuard Home documentation for help generating the hash.

I hope this helps anyone who wants to ship DNS logs and are using similar hardware as mine.

Thanks,

Back to top