Audit log
The audit log keeps a record of important actions in Semaphore: who signed in, who changed a user or a role, who created an API token. Each event shows who did it, when, from which address, and whether it worked. Use it to find out what happened on your installation, or send the events to your SIEM to keep them next to the rest of your logs.
The audit log is available in every edition. Filtering, exporting it as a file and sending events to a SIEM require Semaphore Pro.
What is recorded
Semaphore currently records sign-in, account, project and task activity:
- sign-ins, failed sign-in attempts, sign-outs and two-factor checks;
- rejected API tokens, denied requests and blocked cross-site requests;
- changes to users, passwords, two-factor authentication, external identities and API tokens;
- changes to project members, roles and template permissions;
- changes to projects, inventories, repositories, templates, schedules, integrations, host configs, environments, credentials and secret storages, and project backup exports and restores;
- changes to system settings and Pro license activation;
- task starts with their trigger (API, schedule, integration, autorun, workflow), approvals, stops, completions and deleted task history;
- runner changes, registrations (including refused registration tokens), unregistrations and runner reports with an invalid status;
- exports of the audit log as a file;
- every server start.
For the full list, see Audit events.
Passwords, tokens, secret values and task output never appear in audit events. API tokens are shown by a fingerprint instead of their value. A failed sign-in keeps the login name that was entered, so it can contain an email address.
In a task completion event, metadata.result shows the
status Semaphore gave the task, and metadata.end_reason says why Semaphore ended it: timeout when it ran
too long, runner_lost when its runner stopped responding. Task arguments, variables, runner tags and tokens
are not recorded.
During a rolling upgrade of an HA cluster, a task started on an upgraded node and finished on a node that is not upgraded yet has no completion event.
Repository URLs, host config URLs and integration aliases are not recorded either.
Sometimes Semaphore saves or deletes an object, but a later part of the same request fails. The UI or API
then shows an error, although the object was created or deleted. Such an event is recorded as a success
with metadata.partial=true, and reason says what did not complete:
secret_failed: an environment was saved or deleted, but some of its secrets were not saved or not removed;inventory_failed: a template was created, but its Terraform workspace inventory was not;restore_failed: a project was restored from a backup, but not all of its objects were;setup_failed: a project was created, but not fully set up, for example its creator was not added as owner.
Enable the audit log
The audit log is off by default. To turn it on, set audit.enabled and give your installation a name in
audit.instance_id:
{
"audit": {
"enabled": true,
"instance_id": "prod-eu"
}
}
Or using environment variables:
SEMAPHORE_AUDIT_ENABLED=true
SEMAPHORE_AUDIT_INSTANCE_ID=prod-eu
The instance ID is 1 to 255 characters without spaces. It is added to every event, so you can tell your installations apart when they send events to the same place.
Restart Semaphore. Recording starts after the restart; earlier actions are not added. For all options, see Configuration options.
Record the client address behind a proxy
If Semaphore runs behind a reverse proxy, events show the address of the proxy instead of the user. To
record the real client address, list your proxy networks in audit.trusted_proxy_cidrs:
{
"audit": {
"enabled": true,
"instance_id": "prod-eu",
"trusted_proxy_cidrs": ["10.0.0.0/8"]
}
}
Or using environment variables:
SEMAPHORE_AUDIT_TRUSTED_PROXY_CIDRS='["10.0.0.0/8"]'
Semaphore then takes the client address from X-Forwarded-For or X-Real-IP, but only for requests that
come from these networks. If requests pass through several proxies, list all of them. Do not list the
networks your users connect from: anyone there could set these headers to any address.
Storage
Events are stored in the Semaphore database, so your regular database backups include them. By default Semaphore keeps every event. To delete old events, set a retention period in days:
{
"audit": {
"retention_days": 365
}
}
Or using an environment variable: SEMAPHORE_AUDIT_RETENTION_DAYS=365.
Semaphore deletes older events once an hour and records an audit.retention/delete event with the number of deleted
events. If you export events to a SIEM, choose a period longer than the longest SIEM outage you want to survive:
events that are older than the period are deleted even if they were not sent.
Retention also runs when Semaphore starts. In an HA installation, use the same retention_days on every node.
The audit log never gets in the way of your users. If an event cannot be saved, Semaphore writes an error to the server log and the action continues as usual.
Export to a SIEM Pro
Semaphore Pro can send audit events to a Syslog receiver over TLS, such as rsyslog or Vector, and to any receiver of the Splunk HTTP Event Collector (HEC) protocol, such as Splunk, Vector, Fluent Bit, the OpenTelemetry Collector or Cribl. You can set up one Syslog and one HEC destination, or both at once.
You will need:
- the receiver hostname and port;
- a name for this destination, such as
security-syslog; - the CA certificate of the receiver, if the Semaphore host does not trust it already.
Add audit.syslog to config.json:
{
"audit": {
"enabled": true,
"instance_id": "prod-eu",
"syslog": {
"id": "security-syslog",
"address": "siem.example.com:6514",
"ca_file": "/etc/semaphore/siem-ca.pem",
"server_name": "siem.example.com",
"timeout": "10s"
}
}
}
Or using environment variables:
SEMAPHORE_AUDIT_SYSLOG_ID=security-syslog
SEMAPHORE_AUDIT_SYSLOG_ADDRESS=siem.example.com:6514
SEMAPHORE_AUDIT_SYSLOG_CA_FILE=/etc/semaphore/siem-ca.pem
SEMAPHORE_AUDIT_SYSLOG_SERVER_NAME=siem.example.com
SEMAPHORE_AUDIT_SYSLOG_TIMEOUT=10s
id and address are required. Semaphore remembers which events it has already sent to each destination,
so keep the same id when you change the address or the certificate. A new id starts with new events.
Semaphore always checks the receiver certificate and uses TLS 1.2 or later. ca_file adds your CA to the
trusted certificates, and server_name sets the name to check in the certificate when it differs from the
address.
Restart Semaphore. If the settings are invalid or the CA file cannot be read, Semaphore does not start.
Check that events arrive
Semaphore records an event every time it starts. After the restart, look for it at the receiver:
event_code is audit.lifecycle, action is start, and metadata.destinations includes your
destination ID.
Send events over HEC
You will need the HEC endpoint URL, a HEC token, a name for this destination such as security-hec, and the
CA certificate of the receiver if the Semaphore host does not trust it already.
{
"audit": {
"enabled": true,
"instance_id": "prod-eu",
"splunk_hec": {
"id": "security-hec",
"url": "https://splunk.example.com:8088/services/collector/event",
"token": "<HEC token>",
"index": "security",
"ca_file": "/etc/semaphore/siem-ca.pem"
}
}
}
Or using environment variables:
SEMAPHORE_AUDIT_SPLUNK_HEC_ID=security-hec
SEMAPHORE_AUDIT_SPLUNK_HEC_URL=https://splunk.example.com:8088/services/collector/event
SEMAPHORE_AUDIT_SPLUNK_HEC_TOKEN='<HEC token>'
SEMAPHORE_AUDIT_SPLUNK_HEC_INDEX=security
SEMAPHORE_AUDIT_SPLUNK_HEC_CA_FILE=/etc/semaphore/siem-ca.pem
id, url and token are required, and the URL must start with https://. Use a different id than for
Syslog. source and sourcetype default to semaphore and semaphore:audit. Certificates are checked the
same way as for Syslog, and the standard HTTPS_PROXY and NO_PROXY variables apply.
Semaphore sends up to 100 events per request. The event field of each HEC event holds the audit event JSON,
time is the event time, and host is the HA node ID, or the instance ID on a single node.
Restart Semaphore, then check that events arrive as described above.
How events are delivered
- If the receiver is down, events wait in the database and are sent when it is back. Users don't notice anything.
- After network errors, restarts or an HA failover, some events can arrive twice. Use
event_idto drop duplicates andseqto put events in order. - Over Syslog, if a connection breaks without an error, the event sent at that moment can be lost.
- In an HA installation, one node at a time sends events to each destination. If Redis is unavailable, sending pauses, and events are still recorded.
- Over HEC, an event counts as sent only after the receiver answers with a 2xx status. Any other answer, including 4xx, is retried. If the receiver crashes after answering, events it had not stored yet can be lost.
Over Syslog, each event is sent as an RFC 5424 Syslog message with the event JSON as its body. HOSTNAME is the HA node
ID, or the instance ID on a single node, and MSGID is the event code.
The examples below are minimal and only show how to receive the events. They accept a connection from any client that can reach the port. In production, protect the receiver so that only your Semaphore servers can send events to it.
rsyslog example
This rsyslog configuration accepts the TLS connection and writes one event per line:
global(
DefaultNetstreamDriver="gtls"
DefaultNetstreamDriverCAFile="/etc/rsyslog.d/ca.pem"
DefaultNetstreamDriverCertFile="/etc/rsyslog.d/cert.pem"
DefaultNetstreamDriverKeyFile="/etc/rsyslog.d/key.pem"
)
module(load="imtcp" StreamDriver.Name="gtls" StreamDriver.Mode="1" StreamDriver.AuthMode="anon")
input(type="imtcp" port="6514" ruleset="semaphore-audit")
template(name="semaphore-audit-json" type="string" string="%msg%\n")
ruleset(name="semaphore-audit") {
action(type="omfile" file="/var/log/semaphore-audit.json" template="semaphore-audit-json")
}
Vector example
This Vector configuration accepts the TLS connection, reads the event JSON and writes it to a file:
[sources.semaphore_audit]
type = "syslog"
mode = "tcp"
address = "0.0.0.0:6514"
tls.enabled = true
tls.crt_file = "/etc/vector/cert.pem"
tls.key_file = "/etc/vector/key.pem"
[transforms.semaphore_audit_event]
type = "remap"
inputs = ["semaphore_audit"]
source = ". = parse_json!(.message)"
[sinks.semaphore_audit_file]
type = "file"
inputs = ["semaphore_audit_event"]
path = "/var/log/semaphore-audit.json"
encoding.codec = "json"