FIM is a distributed File Integrity Monitoring project written in Go. It consists of a lightweight endpoint agent that watches the filesystem, computes hashes for relevant files, and streams structured events to a central gRPC server secured with mutual TLS. The server emits JSON logs that are collected by Promtail, stored in Loki, and visualized in Grafana.
The current implementation already covers the full end-to-end event pipeline:
- local file monitoring
- baseline file scan on startup
- SHA-256 hashing with retry logic
- buffered event delivery over gRPC
- mutual TLS authentication between agent and server
- structured log shipping into Loki
- a pre-provisioned Grafana security dashboard
The goal is to answer questions such as:
- What file changed?
- When did it change?
- Was it created, modified, deleted, or chmod-ed?
- What is the file hash after the change?
- Which host generated the event?
.
|-- agent/ Agent-side watcher, scanner, hashing, transport
| |-- network/ TLS and gRPC sender logic
|-- cmd/
| |-- agent/ Agent entrypoint
| |-- server/ Central server entrypoint
|-- config/ Agent configuration
|-- certs/ CA, server, and client certificates
|-- proto/ Protobuf and generated gRPC types
|-- deploy/ Docker Compose + Loki/Promtail/Grafana assets
|-- Dockerfile.server Root server container build
flowchart LR
A[Filesystem Activity] --> B[Agent Watcher]
B --> C[Hash + Metadata Builder]
C --> D[Buffered Sender Queue]
D --> E[gRPC Stream over mTLS]
E --> F[Central Server]
F --> G[JSON Structured Logs]
G --> H[Promtail]
H --> I[Loki]
I --> J[Grafana Dashboard]
The agent is responsible for:
- loading YAML configuration
- resolving agent identity from the hostname when
agent.id: auto - recursively or non-recursively watching configured directories
- skipping excluded files and directories
- running a baseline scan at startup
- hashing created/modified/scanned files
- queueing events locally
- reconnecting to the server indefinitely after disconnects
The server is responsible for:
- listening on TCP
:9000 - enforcing client certificate authentication
- receiving a client-streaming gRPC event stream
- writing received events as JSON logs
The deployment stack adds:
- Promtail to parse server JSON logs
- Loki to store and query log events
- Grafana to visualize recent FIM activity
When cmd/agent/main.go starts, it:
- loads
config/gowatch.yaml - resolves the agent ID from the local hostname if configured as
auto - initializes the network sender with a buffered queue of
1000events - starts the sender loop in a goroutine
- creates the filesystem watcher
- starts a baseline scan in a goroutine
- begins consuming live filesystem events until interrupted
At startup, ScanExistingFiles walks every configured watch root and emits a FILE_SCAN event for each non-excluded file.
Baseline scan behavior:
- walks every configured path with
filepath.WalkDir - skips excluded directories with
filepath.SkipDir - hashes eligible files with SHA-256
- emits one event per file with timestamp, hostname, OS, file path, and new hash
This gives the backend an initial view of the monitored file set before live changes arrive.
The watcher uses fsnotify and maps native events into the shared protobuf enum:
Create->FILE_CREATEWrite->FILE_MODIFYRemove->FILE_DELETEChmod->CHANGE_PERMISSIONRenameis currently ignored and becomesUNKNOWN
Important watcher logic:
- recursive mode adds every discovered directory at startup
- newly created directories are added dynamically so recursive monitoring continues after boot
- events on the same path are debounced for
500ms - excluded names are filtered using
filepath.Matchagainst the basename only - hashes are computed only for
FILE_CREATEandFILE_MODIFY
Hash calculation uses SHA-256 and is intentionally defensive:
- files larger than the configured maximum size are skipped
- the agent retries file access up to 3 times
- each retry waits
500ms - this helps when files are briefly locked by another process
The current default maximum file size is 50 MB if it is omitted from config.
The sender maintains an in-memory buffered channel:
- queue capacity is
1000events - enqueue is non-blocking
- if the queue is full, the event is dropped and a warning is logged
- if sending fails, the current event is re-enqueued before reconnecting
- reconnect attempts happen every
5suntil the server is available again
This design prioritizes agent stability and continued operation over guaranteed delivery.
The event contract is defined in proto/event.proto.
UNKNOWNFILE_CREATEFILE_DELETEFILE_MODIFYCHANGE_PERMISSIONFILE_SCAN
hostname
os
file_path
event_type
new_hash
timestamp
- protocol: gRPC
- RPC shape: client-streaming
- listening address:
:9000 - message format: protobuf
The agent opens a single long-lived stream and continuously sends FileEvent messages. When the agent shuts down cleanly, it closes the stream and expects an Ack.
sequenceDiagram
participant FS as Filesystem
participant AG as Agent
participant Q as Sender Queue
participant SRV as gRPC Server
participant PT as Promtail
participant LK as Loki
participant GF as Grafana
FS->>AG: create/modify/delete/chmod
AG->>AG: map event + hash file if needed
AG->>Q: enqueue FileEvent
Q->>SRV: send protobuf event over mTLS
SRV->>SRV: write JSON log
PT->>SRV: tail log file
PT->>LK: push parsed labels + fields
GF->>LK: query events for dashboards
Security in the current codebase is centered on mutual TLS for the agent-to-server channel.
- encryption in transit between agent and server
- server authentication to the agent
- agent authentication to the server
- rejection of clients without a trusted certificate
The agent:
- loads a client certificate and key
- loads the CA certificate into a trust pool
- uses TLS transport credentials for the gRPC client
The server:
- loads a server certificate and key
- loads the CA certificate into
ClientCAs - sets
ClientAuth: tls.RequireAndVerifyClientCert - refuses connections from clients without a valid CA-signed certificate
Agent config references:
certs/ca.crtcerts/client.crtcerts/client.key
Server startup loads:
certs/ca.crtcerts/server.crtcerts/server.key
These certificates are intended to live locally in certs/ for development/testing and are already excluded by .gitignore. If you want to recreate them from scratch, the following OpenSSL flow matches the filenames expected by the code.
First create the certificate directory:
mkdir -p certsCreate the local CA:
openssl genrsa -out certs/ca.key 2048
openssl req -x509 -new -nodes -key certs/ca.key -sha256 -days 3650 -out certs/ca.crt -subj "/CN=FIM-Local-CA"Create the server certificate:
openssl genrsa -out certs/server.key 2048
openssl req -new -key certs/server.key -out certs/server.csr -subj "/CN=localhost"
openssl x509 -req -in certs/server.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial -out certs/server.crt -days 825 -sha256Create the client certificate:
openssl genrsa -out certs/client.key 2048
openssl req -new -key certs/client.key -out certs/client.csr -subj "/CN=fim-agent"
openssl x509 -req -in certs/client.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial -out certs/client.crt -days 825 -sha256After generation, the directory should contain:
certs/ca.crtcerts/ca.keycerts/ca.srlcerts/server.crtcerts/server.csrcerts/server.keycerts/client.crtcerts/client.csrcerts/client.key
Important notes:
- the server trusts only client certificates signed by
certs/ca.crt - the agent trusts only servers signed by
certs/ca.crt - for real deployments, replace the simple
CN=localhostexample with hostnames or SANs that match your actual server address - keep
*.keyfiles local and private; this repository already ignorescerts/in.gitignore - if you rotate the CA, both server and client certificates must be reissued
Current example config:
agent:
id: auto
server:
address: localhost:9000
tls: true
ca_cert: certs/ca.crt
client_cert: certs/client.crt
client_key: certs/client.key
watch:
paths:
- ./test-watch
recursive: true
max_file_size_mb: 50
exclude:
- ".git"
- ".*tmp"
- ".*log"
events:
include:
- FILE_CREATE
- FILE_MODIFY
- FILE_DELETE
- CHANGE_PERMISSION
scan:
on_reconnect: trueThe deploy/ directory provisions a basic security telemetry pipeline.
fim-serverlokipromtailgrafana
9000-> FIM gRPC server3100-> Loki HTTP API3000-> Grafana UI
- the Go server writes JSON logs to stdout and optionally to
/var/log/fim/fim.log - Promtail tails
/var/log/fim/*.log - Promtail parses JSON fields such as
hostname,action,file_path, andhash - Promtail promotes
actionandhostnameinto Loki labels - Grafana queries Loki and renders the dashboard
The bundled Grafana dashboard currently shows:
- total file events in the last hour
- action breakdown by event type
- per-minute event velocity
- real-time security event logs
- Go
1.24.xfor local builds - valid CA, server, and client certificates in
certs/ - reachable network path between agent and server
go run ./cmd/servergo run ./cmd/agentFrom deploy/:
docker compose up --buildThe agent uses several defensive choices to avoid destabilizing monitored endpoints:
- non-blocking queue enqueue
- retry-based file opening for transient locks
- max file size cap for expensive hashing
- debounce window to reduce duplicate bursts
- graceful shutdown through context cancellation
FIM already provides a working secure telemetry path for file integrity events:
filesystem -> agent -> buffered queue -> mTLS gRPC -> JSON logs -> Promtail -> Loki -> Grafana