Users and sign-in¶
Everyone who signs in to Signalith has one of three roles. People can sign in in three ways:
| Account | Where it lives | Managed on |
|---|---|---|
| The built-in admin | auth.admin_user and auth.admin_password_hash in signalith.yaml |
The server, with signalith passwd |
| Local users | users.json in the lookups folder |
The Users page |
| Directory users | Your Active Directory or LDAP server; Signalith keeps a record after their first sign-in | The directory; on the Users page you can only disable or delete them |
Roles¶
Each role can do everything the roles above it can.
| Role | For | Can |
|---|---|---|
| Viewer | Managers, auditors, other teams | Run searches and see dashboards, findings, investigations, detections, risk and intel. Cannot change anything. |
| Analyst | SOC analysts | Also triage findings, work investigations, build dashboards, use Ask and add or remove intel indicators by hand. |
| Admin | Whoever runs Signalith | Also create and change detections, add and change threat intel feeds and upload intel, edit asset and identity lists, set up agents and create and revoke agent tokens, manage users and set up directory sign-in, and change server settings. |
By area:
| Area | Viewer | Analyst | Admin |
|---|---|---|---|
| Search, query builder, explain, field lists | Yes | Yes | Yes |
| Ask (plain English; can call a paid AI model) | No | Yes | Yes |
| Dashboards: view and run | Yes | Yes | Yes |
| Dashboards: create, edit, delete, add panels | No | Yes | Yes |
| Findings: view | Yes | Yes | Yes |
| Findings: status, owner, comments, bulk changes, create | No | Yes | Yes |
| Investigations: view | Yes | Yes | Yes |
| Investigations: create, edit, add findings, notes, entities | No | Yes | Yes |
| Investigations: remove someone else's note, pinned event or search | No | No | Yes |
| Detections and ATT&CK coverage: view | Yes | Yes | Yes |
| Detections: create, edit, delete, test, run, Sigma import | No | No | Yes |
| Risk, asset and identity context: view | Yes | Yes | Yes |
| Asset and identity lists: edit | No | No | Yes |
| Threat intel: view and look up | Yes | Yes | Yes |
| Threat intel: add or remove indicators by hand | No | Yes | Yes |
| Threat intel: add, change, test and refresh feeds, upload files | No | No | Yes |
| Playbooks: see runs, approve or reject steps that wait | No | Yes | Yes |
| Playbooks: create, edit, delete, turn on and off | No | No | Yes |
| Add data (agent setup, agent tokens) | No | No | Yes |
| Agent tokens: list names and use (API only) | No | Yes | Yes |
| Users and directory sign-in | No | No | Yes |
| Server settings (Settings page, restart) | No | No | Yes |
| Change your own password (local users) | Yes | Yes | Yes |
Dashboards have no owner: any analyst can change or delete any custom dashboard. Investigation notes, pinned events and saved searches are evidence: an analyst can remove only the ones they added, and an admin any of them.
When someone tries something their role does not allow, Signalith answers
403 and says what is needed, for example "This needs the Analyst role, and your
role is Viewer. Ask an admin if you need access." The API section lists
the role of every route.
A role change, disabling or deleting a user takes effect on that user's next request: Signalith looks up the role every time instead of remembering it in the session. If they have Signalith open and try something their new role does not allow, the page reloads to match the new role and says why.
The built-in admin¶
The account in auth.admin_user (admin unless you changed it) always works, even
when the directory server is down or every other admin has been removed, so you can
always get back in. It shows on the Users page as Built-in and cannot be edited,
disabled or deleted there. Change its password on the server:
With the Docker quick start, run
docker compose -f deploy/docker/docker-compose.yml exec signalith signalith passwd and
then docker compose -f deploy/docker/docker-compose.yml restart signalith. There the
first password comes from a file the setup step made (signalith initial-password shows
it), and the new one is kept in admin-password-hash in the signalith-data volume,
where it replaces the first one.
Keep its password somewhere safe and use your own account day to day. No local user or directory user can have the built-in admin's name.
Managing local users¶
Admins manage users on the Users page.
Add a user. Enter a username, optionally a name and email address, and pick a
role (Viewer if you don't). Usernames are 1 to 64 lower-case letters, digits, dots,
dashes, underscores or @, starting with a letter or digit; they are not case
sensitive. Signalith makes up a temporary password and shows it once (or you can
type one, at least 8 characters). Give it to the person: at their first sign-in they
must choose their own password before they can do anything else.
Change a user. Edit their name and email, change their role, disable or enable them, reset their password (they get a new temporary password and must change it at the next sign-in), or delete them. Disabling, deleting or resetting a password signs that user out everywhere at once.
To stop anyone locking everyone out:
- You cannot disable, delete or change the role of your own account. Ask another admin.
- Signalith always keeps at least one enabled admin, and the built-in admin counts as one.
Passwords are stored as bcrypt hashes and never shown again.
Changing your password¶
Local users change their password from the user menu (Change password): enter the current password and the new one (at least 8 characters, at most 72 bytes). Every other browser signed in as you is signed out, on every server that shares the users file too. Scripts using your old password with HTTP Basic auth stop working at once.
Directory users change their password in the directory (for example with
Ctrl+Alt+Del on Windows). The built-in admin's password is changed with
signalith passwd.
Signing in¶
Sign-in checks the name in this order:
- The built-in admin. If the name is
auth.admin_user, only its password works. - Local users. If a local user has the name, only the local password works.
The directory is never asked about a local username, even with a domain in front
(
CORP\jane). - The directory, if directory sign-in is on.
Failed sign-ins all get the same answer, "Username and password don't match.", so they do not reveal which usernames exist. Other answers are:
code |
When |
|---|---|
wrong_credentials (401) |
Wrong username or password, or an empty one |
disabled (403) |
The account is disabled in Signalith |
not_in_group (403) |
A directory user in no group mapped to a role: "Your directory account is not in a group that can use Signalith." |
password_expired (403) |
Active Directory says the password expired or must be changed |
directory_unavailable (503) |
The directory server could not be reached, or too many directory sign-ins are being checked at once |
locked_out (429) |
Too many wrong passwords; see below |
Failed sign-in limits. Wrong passwords are counted so that guessing stays slow, but someone guessing your password cannot keep you out:
| Counted | Limit | Then |
|---|---|---|
| One address, one account | 10 in 15 minutes | That account is refused from that address for 15 minutes, even with the right password |
| One address, any accounts | 30 in 15 minutes | That address is refused for 15 minutes |
| One account, from browsers and addresses that never signed in to it | 30 in an hour | Their next tries at that account wait: 1 second, then twice as long after each wrong password, up to 15 minutes. The account itself is never locked |
A successful sign-in clears only the count for its own address and account, so signing in to one account never helps someone guess another's password. Sign-ins still being checked count too, so a burst of parallel guesses cannot get past the limit. Wrong directory passwords count; an unreachable directory does not. Basic auth, Test a user with a password, and Change password share these limits. An IPv6 address is counted with the rest of its /64 network, since one computer can usually use any address in it.
Browsers you signed in with keep working. When you sign in, Signalith gives your
browser a cookie that says it has signed in to that account before. The cookie is
sent only over HTTPS, cannot be read by scripts on the page, and lasts a year from
your last sign-in. While someone elsewhere guesses your password, that browser is
not slowed down, and it keeps working even when its address is paused for wrong
passwords to other accounts. An address you signed in from in the last 30 days is not
slowed down either, which keeps scripts using HTTP Basic auth working (they keep no
cookies). Your own wrong passwords count against that browser or address only: 10 in
15 minutes pause it for 15 minutes. Browsers that signed in before share 30 wrong
passwords an hour per account, and addresses that did share another 30; past that
they wait like everyone else, so the guesses an account can take stay limited.
Someone else at an address you once used, such as an office or hotel network, can use
up the addresses' share, but not your browser's. Changing or resetting the password of
a local user or the built-in admin makes every browser and address sign in once more
before it is recognized again (the browser you change your own password in stays
recognized). A built-in admin password from SIGNALITH_ADMIN_PASSWORD or
SIGNALITH_ADMIN_PASSWORD_FILE keeps its browsers recognized across restarts while it
stays the same.
Change password counts against your signed-in browser, so it works while someone
else is guessing your password. The cookie needs HTTPS: over plain HTTP no browser
is remembered. Each server keeps its own key for these cookies, signin.key in its
state folder; deleting it only means every browser signs in once more before it is
recognized.
Many people behind one address. When people reach Signalith through one address, such as a reverse proxy, a load balancer, a NAT gateway or Docker Desktop's port forwarding, Signalith sees that address for all of them, and 30 wrong passwords from anyone behind it would pause sign-ins for everyone. To avoid that, list the address on the Settings page:
- A reverse proxy (nginx, Caddy, IIS, most load balancers): add it under
Trusted reverse proxies (
server.trusted_proxies). If the proxy setsX-Forwarded-Foritself, also choose X-Forwarded-For under Client address header (server.client_header); for nginx that takesproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;(see Behind a reverse proxy). Signalith then counts and logs each person by the address the proxy got the request from: the right-most address in the header that is not a trusted proxy, since the addresses further left come from the sender. It reads the header only from the addresses listed there, since anyone else could send a made-up one. Without Client address header, or for a request that comes from the proxy without the header, the proxy counts like an address under Proxies and gateways. - Anything else (a NAT gateway, a proxy that cannot send the header): add it
under Proxies and gateways (
server.shared_addresses). Wrong passwords from that address are counted for each account only, so it is never refused as a whole, and one person's wrong passwords never affect another account.
When an address is refused as a whole, the server log says so and names these settings.
The web UI keeps you signed in with a session cookie. Sessions end after
auth.session_timeout without activity (12 hours by default), after 7 days with
Keep me signed in, when you sign out, when your password is changed, or when an
admin disables, deletes or resets your account. Servers that share the users file end
them too, within a second or so.
Scripts and API clients can use HTTP Basic auth with the same username and password. Signalith remembers a correct username and password briefly so it does not check the hash or ask the directory on every request: for 10 minutes for local users and the built-in admin, and 2 minutes for directory users. Changing or resetting a local password, or changing the directory settings, ends that at once. A password changed in the directory keeps working for Basic auth for up to 2 minutes. Group changes and accounts disabled in the directory are picked up as described in Directory users who change or leave.
Temporary passwords. Signing in with a temporary password works, but the reply
says "must_change_password": true and the sign-in page asks for a new password.
Until it is changed, every request other than GET /api/me and
POST /api/me/password answers 403 with code password_change_required.
Sign in with Active Directory or LDAP¶
Directory sign-in lets people use their Active Directory or LDAP account, with their role decided by the groups they are in.
How it works¶
When someone signs in with a name that is not the built-in admin or a local user:
- Signalith connects to the directory server (the failover server if the first cannot be reached), over TLS when set up to.
- It signs in as the service account (or searches anonymously if there is none).
- It searches under the user base DN with the user filter, the typed name in place
of
{username}. Exactly one entry must match; none or several refuse the sign-in.CORP\janeis looked up asjane, and with the Active Directory filterjane@corp.example.comworks too. - It checks the password by signing in to the directory as that entry's DN. Empty passwords are refused before anything is sent: many LDAP servers treat a sign-in with an empty password as anonymous and let it through.
- It finds the user's groups and maps them to a role. A user in several mapped groups gets the highest role. A user in none is refused, unless you choose to let such users in as viewers.
- The user appears on the Users page as a directory user, with their name, email and DN from the directory. Their role is updated at every sign-in, and checked again every few minutes while they use Signalith (see below).
Directory users who have never signed in are not listed. You can disable a directory user in Signalith (they stay out whatever the directory says), or delete their record (it comes back at their next sign-in). Turning directory sign-in off signs out every directory user at their next request.
Directory users who change or leave¶
While a directory user is signed in, Signalith looks them up again with the service account (without their password) every 5 minutes, and right away when the directory settings are saved:
- Groups changed. Their role follows their groups from their next request.
- No longer in a mapped group, disabled or deleted in the directory. Their sessions end, and the Users page shows them with no role and the reason. With the Active Directory preset, disabled accounts are not found, so they count as deleted. A refused sign-in does the same at once.
- The directory cannot be reached. They keep the access they had and are checked again 5 minutes later. If the settings were changed since their role was last checked, their sessions end instead.
So someone who leaves keeps access for at most about 5 minutes. To stop someone at once, disable them on the Users page.
Set it up¶
On the Users page, open Directory sign-in and click Set up directory sign-in:
- Pick Active Directory or OpenLDAP. This fills in the filter and attribute defaults below; change any of them if your directory differs.
- Enter the server address and, if you have one, a failover server.
- Enter the service account's DN and password. The saved password is only used with the saved servers, bind DN and TLS settings: change any of those and you type the password again, so it can never be sent to a server someone else chose.
- Enter the user base DN. A group base DN is optional: when it is empty,
Signalith searches for groups in the whole domain (the
DC=part of the user's DN). - Map groups to roles by their full DN.
- Click Test connection, then Test a user with your own account, then turn directory sign-in on and save.
Settings can be saved switched off while you work on them; they are only required to be complete when directory sign-in is on.
Use an encrypted connection. Passwords are sent to the directory to check them.
Use ldaps://host (port 636), or ldap://host (port 389) with StartTLS. Signalith
accepts TLS 1.2 or newer and checks the server's certificate against the system's
trusted CAs plus any CA certificate you paste. Plain ldap:// without StartTLS works
but shows a warning, and so does turning certificate checks off.
The service account needs only read access to users and groups. In Active Directory any domain user account can do this; a dedicated account whose password does not expire is best, so sign-in does not stop when it expires.
Active Directory¶
| Setting | Example |
|---|---|
| Server | ldaps://dc1.corp.example.com |
| Failover server | ldaps://dc2.corp.example.com |
| Service account DN | CN=svc-signalith,OU=Service Accounts,DC=corp,DC=example,DC=com |
| User base DN | DC=corp,DC=example,DC=com |
| Admin groups | CN=Signalith Admins,OU=Groups,DC=corp,DC=example,DC=com |
| Analyst groups | CN=SOC Analysts,OU=Groups,DC=corp,DC=example,DC=com |
| Viewer groups | CN=SOC Viewers,OU=Groups,DC=corp,DC=example,DC=com |
The Active Directory preset:
- finds users by logon name or
user@domain, and skips disabled accounts:(&(objectCategory=person)(objectClass=user)(|(sAMAccountName={username})(userPrincipalName={username}))(!(userAccountControl:1.2.840.113556.1.4.803:=2))) - reads
sAMAccountName(username),displayName(name) andmail(email) - reads groups from
memberOf, and with Nested groups on (the default) also counts groups inside groups: ifIT Leadsis a member ofSignalith Admins, members ofIT Leadsare admins. The domain controller works this out with theLDAP_MATCHING_RULE_IN_CHAINrule (1.2.840.113556.1.4.1941).
Your domain controllers need an LDAPS certificate for ldaps:// (most domains with
Active Directory Certificate Services have one). Paste your enterprise root CA's
certificate in CA certificate if the Signalith server does not trust it already.
OpenLDAP¶
| Setting | Example |
|---|---|
| Server | ldap://ldap.example.org with StartTLS on, or ldaps://ldap.example.org |
| Service account DN | cn=signalith,ou=services,dc=example,dc=org |
| User base DN | ou=people,dc=example,dc=org |
| Group base DN | ou=groups,dc=example,dc=org |
| Admin groups | cn=signalith-admins,ou=groups,dc=example,dc=org |
| Analyst groups | cn=soc,ou=groups,dc=example,dc=org |
The OpenLDAP preset finds users with (&(objectClass=inetOrgPerson)(uid={username})),
reads uid, cn and mail, and finds groups by searching the group base DN with a
filter that covers the three usual kinds of group:
(|(&(objectClass=groupOfNames)(member={dn}))(&(objectClass=groupOfUniqueNames)(uniqueMember={dn}))(&(objectClass=posixGroup)(memberUid={username})))
{dn} is the user's DN and {username} their username as stored in the directory.
If your server has the memberof overlay, you can set Groups to memberOf
attribute instead. Nested groups are only followed on Active Directory.
The same preset works for 389 Directory Server and FreeIPA (users under
cn=users,cn=accounts,dc=..., groups under cn=groups,cn=accounts,dc=...).
Testing¶
Test connection connects to each server, signs in as the service account and checks that the base DNs exist. It tests what is in the form, saved or not. Each step says what worked or, in plain words, what went wrong and what to do: a name that does not resolve, a port nothing listens on, a firewall, an untrusted certificate, a certificate for another name, a refused service account password, a base DN that does not exist.
Test a user looks a user up exactly as sign-in would and shows their DN, every group found, the groups that matched a role, and the role they would get, or why they would be refused. Enter their password too to check it; a wrong password there counts toward the failed sign-in limit like a real sign-in.
Setting it in signalith.yaml¶
Instead of the Users page you can put the settings in signalith.yaml under
auth.ldap, for example to deploy the same settings to several servers. The Users
page then shows them read-only, marked From signalith.yaml, and changes made in
the file apply when Signalith restarts.
auth:
ldap:
type: ad
url: ldaps://dc1.corp.example.com
failover_url: ldaps://dc2.corp.example.com
ca_cert_file: /etc/signalith/corp-root-ca.pem
bind_dn: CN=svc-signalith,OU=Service Accounts,DC=corp,DC=example,DC=com
# bind_password: set SIGNALITH_LDAP_BIND_PASSWORD instead
user_base_dn: DC=corp,DC=example,DC=com
admin_groups:
- CN=Signalith Admins,OU=Groups,DC=corp,DC=example,DC=com
analyst_groups:
- CN=SOC Analysts,OU=Groups,DC=corp,DC=example,DC=com
viewer_groups:
- CN=SOC Viewers,OU=Groups,DC=corp,DC=example,DC=com
Directory sign-in is on whenever auth.ldap is present, unless it says
enabled: false. type defaults to ad. Keep the service account's password out
of the file with the SIGNALITH_LDAP_BIND_PASSWORD environment variable (for example
in the systemd unit's EnvironmentFile, or the container's environment). That
variable only applies to auth.ldap in signalith.yaml: settings made on the Users
page keep their own password in directory.json, and Signalith logs a warning at
start-up when the variable is set without auth.ldap.
signalith validate checks the settings, so run it with the same environment as the
service. signalith passwd works without the variable, so the built-in admin's
password can always be reset from a shell.
Settings reference¶
The keys are the same in signalith.yaml and the API.
| Key | Default | Meaning |
|---|---|---|
enabled |
off on the Users page; on in signalith.yaml |
Directory sign-in on or off |
type |
ad |
ad or openldap; fills in the defaults marked preset |
url |
required | ldaps://host[:port] or ldap://host[:port] |
failover_url |
none | Tried when url cannot be reached (not when it refuses a password) |
start_tls |
false |
Encrypt an ldap:// connection before anything else is sent |
ca_cert |
none | PEM certificate(s) of the CA that issued the server's certificate, added to the system's trusted CAs |
ca_cert_file |
none | signalith.yaml only: read ca_cert from this file |
tls_skip_verify |
false |
Accept any certificate. Only for testing: anyone who can intercept the connection can read passwords |
connect_timeout_seconds |
5 |
How long to wait for a server to answer a connection (1-120) |
timeout_seconds |
10 |
How long to wait for each request (1-300) |
bind_dn |
none (anonymous) | The service account that searches for users |
bind_password |
none | Its password. The API never returns it, only bind_password_set. Leave it empty to keep the saved one, which only works while url, failover_url, bind_dn, start_tls, tls_skip_verify and ca_cert are unchanged. In signalith.yaml, SIGNALITH_LDAP_BIND_PASSWORD can provide it instead |
user_base_dn |
required | Where to search for users |
user_filter |
preset | LDAP filter with {username} for the typed name (escaped, so * and ( match only themselves) |
username_attribute |
preset | Attribute holding the username: sAMAccountName, uid |
name_attribute, email_attribute |
preset | displayName or cn; mail |
group_search |
preset | memberof (read the user's memberOf) or search (search with group_filter) |
nested_groups |
true for ad |
Count groups inside groups (Active Directory only) |
group_base_dn |
the whole domain | Where to search for groups (nested groups and search). Empty uses the DC= part of the user's DN, such as DC=corp,DC=example,DC=com |
group_filter |
preset | Filter for search, with {dn} and/or {username} |
admin_groups, analyst_groups, viewer_groups |
none | Group DNs whose members get each role; the highest wins. Compared the way the directory compares DNs (case and spacing do not matter) |
default_role |
none |
Role for users in no mapped group: none refuses them, viewer lets them in as viewers |
Troubleshooting¶
Start with Test connection and Test a user; their messages name the problem. Every sign-in failure is also in the server log with the reason (see Audit log).
"The server's TLS certificate is not trusted." Paste the certificate of the CA that issued the directory server's certificate (for Active Directory, usually the enterprise root CA, exported as Base-64) under CA certificate.
"The server's TLS certificate is for dc1.corp.example.com, not 10.0.0.5." Use a name that is on the certificate in the server address, not an IP address.
"The server did not answer with TLS." The port does not speak LDAPS. Use
ldaps:// with port 636, or ldap:// with port 389 and StartTLS.
"refused the connection" or "did not answer within 5 seconds". Check the address and port, and that firewalls let the Signalith server reach the directory.
"The directory refused the service account's bind DN or password." Check the
full DN (not CORP\svc-signalith) and the password. Active Directory adds a reason,
such as password expired or account disabled.
"The server does not allow anonymous searches." Enter a service account. Active Directory always needs one.
"The server only accepts sign-ins over an encrypted connection." Use ldaps:// or
StartTLS. Active Directory with LDAP signing required refuses plain binds.
"No user matches." Check the user base DN (the user must be under it) and the user filter. With the Active Directory preset, disabled accounts are never found.
"More than one entry matches." The filter is too loose, for example two accounts
with the same uid in different branches. Narrow the base DN or the filter.
A user is refused with "Your account can’t use Signalith yet". Test a user
lists every group found. Map one of them to a role, or set default_role: viewer.
Check that the DN in the mapping is the group's full DN. Group changes in the
directory apply within about 5 minutes, or at the user's next sign-in.
Nested groups are not counted. Nested groups only work on Active Directory with Nested groups on. The groups must be under the group base DN; leave it empty to search the whole domain. For OpenLDAP, map each group directly.
"Enter the service account's password again." You changed the server address, the failover server, the bind DN or a TLS setting. The saved password is only sent to the saved servers, so type it again.
"Too many failed sign-ins." See failed sign-in limits. Wait the time shown, or sign in as another admin, from another address, or as the built-in admin if it is not the account being guessed. The log shows which address and account failed. If everyone behind one proxy or NAT gets this message, list that address as described under Many people behind one address in Signing in.
"Many wrong passwords were tried for this account, so sign-ins to it from new
browsers are slowed down." Someone is guessing that account's password. Sign in
from a browser you have signed in to the account with before, which is not slowed
down, or wait the time shown. The log's sign-in failed lines show where the guesses
come from.
"Your directory password has expired or must be changed." Active Directory accepted the password but requires a new one first. The user changes it (for example by signing in to Windows) and signs in again.
"Signalith could not reach the directory server." Sign-in with the built-in admin and local users still works. Fix the connection, or add a failover server.
A directory user who left still has access. Signed-in directory users are looked up again every 5 minutes (see Directory users who change or leave), and Basic auth credentials are checked with the directory again after at most 2 minutes. If the directory cannot be reached, they keep their access until it can. Disable them on the Users page to stop them at once.
signalith validate says to set SIGNALITH_LDAP_BIND_PASSWORD. Your shell does
not have the service's environment. Load the service's environment file first, for
example sudo sh -c 'set -a; . /etc/signalith/signalith.env; signalith validate -c /etc/signalith/signalith.yaml'.
Where user data is stored¶
| File | Holds | Default place |
|---|---|---|
users.json |
Local users (with bcrypt password hashes), directory users seen so far, last sign-in times | The lookups folder (lookups.dir), or auth.users_file / SIGNALITH_USERS_FILE |
directory.json |
Directory sign-in settings from the Users page, including the service account's password | Next to users.json |
The lookups folder is:
| Install | Folder |
|---|---|
| Docker | /var/lib/signalith/lookups in the signalith-data volume |
| Linux (systemd) | /var/lib/signalith/lookups |
| Windows | C:\ProgramData\Signalith\lookups |
| Anywhere else | lookups next to signalith.yaml |
Both files are written with permissions 0600 (readable only by the account
Signalith runs as), by writing a new file and renaming it so a crash never leaves
half a file. Back them up with the rest of the lookups folder. Edit them only while
Signalith is stopped.
Signalith reads directory.json at start-up. It also looks at users.json while it
runs, because servers that share a lookups folder share that file (see
servers.md): before every change it reads the file as it is now, and
otherwise it checks for changes about once a second. While a server changes
users.json it keeps a small users.json.lock file next to it; leave that file
alone, it goes away by itself.
If users.json is damaged, Signalith refuses to start and says so. If it becomes
damaged while Signalith runs, everyone signed in keeps working, but every change on
the Users page fails with a message that says so. Either way, fix the file, or
stop Signalith, move the file away and start Signalith again with only the
built-in admin.
Audit log¶
Signalith logs every sign-in, sign-out and change to users or directory settings to
its log (journald, the Windows event log, or the container's output), with
audit=true, who did it and from which address. Passwords never appear. Changes on
the Settings page and restarts are logged the same way; see
settings.md.
| Message | Fields |
|---|---|
signed in |
user, role, source, client, method (session or basic) |
sign-in failed |
user, client, method, reason (for example wrong password, account disabled, directory: in no group mapped to a role), locked_out (further tries are refused or slowed down for a while) |
sign-ins from this address are paused … |
client, minutes |
signed out |
user, client |
user created |
by, user, role, must_change_password |
user changed |
by, user, fields |
user role changed |
by, user, from, to |
user disabled, user enabled |
by, user, sessions_ended |
user deleted |
by, user, source, role, sessions_ended |
password reset |
by, user, sessions_ended |
password changed |
by, user, other_sessions_ended |
password change refused |
user, client, reason, locked_out |
directory settings changed |
by, enabled, servers, fields (names only) |
directory connection tested |
by, servers, bind_dn, ok |
directory user looked up |
by, user, servers, ok, code, password_checked |
directory user lost access |
by=directory, user, reason, sessions_ended |
directory user's role changed |
by=directory, user, from, to |
client is the address the request came from. Behind a reverse proxy listed under
Trusted reverse proxies, with Client address header chosen, it is the address
the proxy forwarded the request for, and peer names the proxy. When that header held
no usable address, such as unknown, forwarded shows what it held, and the request
counts under the proxy's own address.
For example:
level=INFO msg="user role changed" audit=true by=ann client=10.0.0.12 user=sam from=analyst to=admin
level=WARN msg="sign-in failed" audit=true user=dave client=10.0.0.40 method=session reason="directory: in no group mapped to a role" locked_out=false
level=WARN msg="sign-in failed" audit=true user=admin client=203.0.113.9 peer=10.0.0.5 method=session reason="wrong password" locked_out=false
API¶
All routes need a signed-in user (session cookie or HTTP Basic auth) except
POST /api/login, POST /api/logout, GET /api/tls/ca.pem, POST /api/ingest and
POST /api/otlp/v1/logs (which take an agent token, never a session or
password), and osquery's routes under /api/osquery/ (POST /api/osquery/enroll takes an agent
token; the others take the node key a host got when it enrolled; see
endpoints.md). Requests from a browser that change something must come from the Signalith
page itself.
Roles of every route¶
Every role (also allowed while a password change is pending): GET /api/me,
POST /api/me/password.
Viewer: GET /api/health, POST /api/search, POST /api/explain,
POST /api/build, POST /api/parse, GET /api/datamodels, GET /api/fields,
GET /api/fields/values, GET /api/language, GET /api/ask,
GET /api/detections, GET /api/detections/{id}, GET /api/detections/{id}/yaml,
GET /api/coverage, GET /api/detections/packs, GET /api/detections/content, GET /api/findings, GET /api/findings/summary,
GET /api/findings/{id}, GET /api/cases, GET /api/cases/summary,
GET /api/cases/{id}, GET /api/lookups/context, GET /api/lookups/{kind},
GET /api/risk, GET /api/risk/object, GET /api/intel, GET /api/intel/feeds,
GET /api/intel/catalog, GET /api/intel/indicators, GET /api/intel/lookup, GET /api/posture,
GET /api/dashboards, GET /api/dashboards/{id}, GET /api/dashboards/{id}/yaml,
POST /api/dashboards/{id}/run, POST /api/panels/run, and the web UI.
Analyst: POST /api/ask, POST /api/findings, POST /api/findings/bulk,
PATCH /api/findings/{id}, POST /api/cases, PATCH /api/cases/{id},
POST /api/cases/{id}/findings, DELETE /api/cases/{id}/findings/{fid},
POST /api/cases/{id}/entries, DELETE /api/cases/{id}/entries/{entry},
POST /api/cases/{id}/entities, POST /api/dashboards, PUT /api/dashboards/{id},
DELETE /api/dashboards/{id}, POST /api/dashboards/{id}/panels,
POST /api/intel/indicators, DELETE /api/intel/indicators/{id}, GET /api/endpoints,
GET /api/endpoints/{id}, GET /api/endpoints/settings, GET /api/endpoints/checks,
GET /api/respond/actions, GET /api/respond/runs, POST /api/respond/run (running
response actions), GET /api/playbooks/runs,
POST /api/playbooks/runs/{id}/approve, POST /api/playbooks/runs/{id}/reject
(the Approvals page; see playbooks.md), GET /api/agent-tokens (the names,
status and use of the agent tokens, never a secret).
Admin: GET /api/agents/config, POST /api/agent-tokens,
PATCH /api/agent-tokens/{id}, POST /api/agent-tokens/{id}/revoke,
POST /api/agent-tokens/legacy/{id}/refuse, POST /api/agent-tokens/legacy/{id}/accept
(see agent-tokens.md), POST /api/tls/names, GET /api/security,
POST /api/security/ca, POST /api/detections,
POST /api/detections/test, POST /api/detections/sigma, POST /api/detections/enable, POST /api/detections/{id}/exceptions, DELETE /api/detections/{id}/exceptions/{exception}, POST /api/findings/{id}/suppress,
PUT /api/detections/{id}, DELETE /api/detections/{id},
POST /api/detections/{id}/run, POST /api/detections/content/check, POST /api/detections/content/upload, DELETE /api/detections/content, PUT /api/lookups/{kind},
POST /api/intel/feeds, PUT /api/intel/feeds/{name}, DELETE /api/intel/feeds/{name},
POST /api/intel/feeds/test, POST /api/intel/feeds/{name}/refresh, POST /api/intel/catalog,
POST /api/intel/taxii/collections, POST /api/intel/upload,
GET /api/respond/config, PUT /api/respond/config, POST /api/respond/test
(setting up response actions),
GET /api/playbooks, PUT /api/playbooks (the Playbooks page; see playbooks.md),
GET /api/sample, POST /api/sample/load, POST /api/sample/remove
(the one-click sample data), PUT /api/endpoints/settings,
DELETE /api/endpoints/{id}, GET /api/endpoints/blocked, DELETE /api/endpoints/blocked/{host},
POST /api/endpoints/queries, GET /api/endpoints/queries/{id} (the Endpoints page),
GET /api/cluster (the Servers page; see servers.md), every
/api/notify route (the Notifications page; see notifications.md), every
/api/retention route (see retention.md), every
/api/users and /api/auth/directory route below, and every /api/settings route
(GET and PUT /api/settings, POST /api/settings/test/opensearch,
POST /api/settings/test/assistant, POST /api/settings/restart,
GET /api/settings/export; see settings.md).
A route added without a role is admin-only, and a test fails until it gets one. That
includes routes registered outside the role table, straight on the server's outer
router: any /api/ route there other than the public ones (sign-in, sign-out, agent
ingest and the CA certificate, GET /api/tls/ca.pem) is admin-only.
A refused request:
{"error": "This needs the Analyst role, and your role is Viewer. Ask an admin if you need access.",
"code": "forbidden", "role": "viewer", "required": "analyst"}
The signed-in user¶
GET /api/me, and the reply to POST /api/login:
{"username": "jlee", "user": "jlee", "name": "Jordan Lee", "email": "",
"role": "analyst", "role_label": "Analyst", "source": "local",
"must_change_password": true, "can_change_password": true, "auth": "session"}
source is builtin, local or directory; auth is session or basic.
POST /api/me/password with {"current_password": "...", "new_password": "..."}
returns the same object. Errors are 400 with a code: wrong_password (field
current_password), same_password, invalid_password (field new_password),
builtin or directory (those passwords are changed elsewhere).
Users (admin)¶
GET /api/users:
{
"users": [
{"username": "admin", "name": "", "email": "", "role": "admin", "source": "builtin",
"disabled": false, "must_change_password": false, "created": null, "updated": null,
"last_sign_in": "2026-10-12T15:35:22Z", "you": true, "actions": []},
{"username": "alice", "name": "Alice Admin", "email": "alice@corp.example.com",
"role": "admin", "source": "directory", "disabled": false, "must_change_password": false,
"dn": "CN=Alice Admin,OU=Staff,DC=corp,DC=example,DC=com",
"created": "2026-10-12T15:35:11Z", "updated": "2026-10-12T15:35:11Z",
"last_sign_in": "2026-10-12T15:35:11Z", "you": false, "actions": ["disable", "delete"]}
],
"roles": [{"id": "viewer", "label": "Viewer", "description": "..."}, ...],
"directory": {"enabled": true, "from_config": false}
}
actions lists what the signed-in admin may do to that user: edit (name and
email), role, reset_password, disable or enable, and delete. A directory
user the directory no longer lets in has "role": "" and "refused" saying why, for
example "in no group mapped to a role".
| Route | Body | Reply |
|---|---|---|
POST /api/users |
{"username", "name", "email", "role", "password", "must_change_password"}; only username is required. role defaults to viewer, an empty password makes one up, must_change_password defaults to true |
201 {"user": {...}, "temporary_password": "kqpp-bsqy-b6hq-mwfm"} (temporary_password only when made up) |
PUT /api/users/{username} |
Any of {"name", "email", "role", "disabled"} |
{"user": {...}} |
POST /api/users/{username}/password |
Optional {"password"} |
{"user": {...}, "temporary_password": "..."} |
DELETE /api/users/{username} |
{"deleted": "priya"} |
Errors are {"error": "..."}, with "field" naming the form field when one value is
the problem (username, name, email, role, password): 400 for a bad value,
404 for an unknown user, 409 for a taken name, the built-in admin, a change to
your own account, the last admin, or a directory user's name, email, role or
password.
Directory sign-in (admin)¶
GET /api/auth/directory (and the reply to a successful PUT):
{
"settings": {"enabled": true, "type": "ad", "url": "ldaps://dc1.corp.example.com",
"failover_url": "", "start_tls": false, "ca_cert": "", "tls_skip_verify": false,
"connect_timeout_seconds": 5, "timeout_seconds": 10,
"bind_dn": "CN=svc-signalith,OU=Service Accounts,DC=corp,DC=example,DC=com",
"bind_password_set": true,
"user_base_dn": "DC=corp,DC=example,DC=com", "user_filter": "(&(objectCategory=person)...)",
"username_attribute": "sAMAccountName", "name_attribute": "displayName",
"email_attribute": "mail", "group_search": "memberof", "nested_groups": true,
"group_base_dn": "", "group_filter": "",
"admin_groups": ["CN=Signalith Admins,OU=Groups,DC=corp,DC=example,DC=com"],
"analyst_groups": [], "viewer_groups": [], "default_role": "none"},
"from_config": false,
"warnings": [],
"presets": {"ad": {...}, "openldap": {...}},
"directory_users": 3
}
from_config is true when the settings come from signalith.yaml; config_file
then names the file, and PUT answers 409.
PUT /api/auth/directory takes the settings object (with bind_password instead
of bind_password_set; leave it empty to keep the saved one). Settings that need
fixing answer 400:
{"error": "Some settings need fixing: ...",
"problems": ["\"http://x\" must be ldaps://host or ldap://host, with an optional :port and nothing after it"],
"fields": {"url": "\"http://x\" must be ldaps://host or ldap://host, with an optional :port and nothing after it"}}
POST /api/auth/directory/test with an optional {"settings": {...}} (the unsaved
form; the saved settings when left out):
{"ok": true, "message": "Connected and signed in as the service account.",
"servers": [{"url": "ldaps://dc1.corp.example.com", "ok": true,
"message": "Connected to dc1.corp.example.com over TLS 1.3.",
"steps": [{"step": "connect", "ok": true, "message": "Connected to dc1.corp.example.com over TLS 1.3."},
{"step": "bind", "ok": true, "message": "Signed in as CN=svc-signalith,..."},
{"step": "user_base", "ok": true, "message": "Found DC=corp,DC=example,DC=com."}]}]}
A failed server has "ok": false, a code (connect, tls_untrusted,
tls_hostname, tls_expired, tls_handshake, starttls_refused, ca_cert,
encryption_required, bind_refused, bind_failed, bind_required,
base_not_found, search_refused, search_failed), a plain message and the
technical detail.
POST /api/auth/directory/test-user with {"username": "alice", "password": "...",
"settings": {...}} (password and settings optional):
{"ok": true, "message": "alice would sign in as Admin. The password was not checked.",
"user": {"username": "alice", "dn": "CN=Alice Admin,OU=Staff,DC=corp,DC=example,DC=com",
"name": "Alice Admin", "email": "alice@corp.example.com",
"groups": ["CN=All Staff,OU=Groups,DC=corp,DC=example,DC=com",
"CN=IT Leads,OU=Groups,DC=corp,DC=example,DC=com",
"CN=Signalith Admins,OU=Groups,DC=corp,DC=example,DC=com"],
"matched_groups": ["CN=Signalith Admins,OU=Groups,DC=corp,DC=example,DC=com"],
"role": "admin"},
"password_checked": false}
When the user would be refused, ok is false and code is user_not_found,
multiple_matches, wrong_password, password_expired, no_role, refused, or
one of the connection codes above. warning is added when the name belongs to the
built-in admin or a local user, since sign-in would then never ask the directory.