Skip to content

Commit

Permalink
v1.2.4: LDAP search logging; bound DN logging
Browse files Browse the repository at this point in the history
  • Loading branch information
JonathanWilbur committed Sep 5, 2022
1 parent e3fff4d commit 85f9121
Show file tree
Hide file tree
Showing 21 changed files with 142 additions and 23 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ MEERKAT_TEST_TELEMETRY=1 # Undocumented.
MEERKAT_CHAINING_TLS_OPTIONAL=1
MEERKAT_OB_AUTO_ACCEPT=0
NODE_OPTIONS=--stack-trace-limit=100
MEERKAT_LOG_BOUND_DN=1
14 changes: 14 additions & 0 deletions apps/meerkat-docs/docs/changelog-meerkat.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog for Meerkat DSA

## Version 1.2.4

- Log much more information on LDAP searches
- Almost the entire search request is logged.
- The number of search results returned is logged.
- This was implemented specifically to make it easier to diagnose issues with
integration with tools that use LDAP authentication. Often, LDAP searches
are used to enumerate users, so it is important to be able to debug these.
- Log bound distinguished names if the `MEERKAT_LOG_BOUND_DN` environment
variable is set to `1`.

No administrative action is needed to upgrade to this version. Just download it
and use it.

## Version 1.2.3

- Fixed a bug where `uid` and `dc` did not have LDAP names.
Expand Down
28 changes: 28 additions & 0 deletions apps/meerkat-docs/docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,34 @@ for using TLS version 1.3 to secure their traffic. These points are added
on top of the points granted via the
`MEERKAT_LOCAL_QUALIFIER_POINTS_FOR_USING_TLS` environment variable.

## MEERKAT_LOG_BOUND_DN

If set to `1`, Meerkat DSA will log the distinguished names of bound clients, if
they have authenticated using a mechanism that relates to an entry in the DIT,
such as simple authentication (as opposed to anonymous access).

:::tip

It may be desirable to enable this for debugging integrations or for identifying
brute force attacks.

:::

:::caution

It might **NOT** be desirable to enable this because doing so could have legal
implications. Meerkat DSA logs IP addresses. If Meerkat DSA also logs
distinguished names, and if distinguished names can readily be related to a real
person (e.g. `C=US,ST=FL,CN=John Doe`), it could be argued that the logs are
storing Personally-Identifiable Information (PII), because the real person's IP
address is stored in the logs along with their identity.

Consult with an attorney on the legality of using this. It may be safer to only
enable this in corporate or home environments where no such right to privacy
may exist.

:::

## MEERKAT_LOG_FILE

The filepath of the Meerkat DSA's log file. If set, Meerkat DSA will log to
Expand Down
2 changes: 1 addition & 1 deletion apps/meerkat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"url": "https://github.com/JonathanWilbur"
}
],
"version": "1.2.3",
"version": "1.2.4",
"license": "MIT",
"bin": {
"meerkat": "./main.js"
Expand Down
1 change: 1 addition & 0 deletions apps/meerkat/src/app/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ const config: Configuration = {
attributeCertificationPath,
},
log: {
boundDN: (process.env.MEERKAT_LOG_BOUND_DN === "1"),
level: logLevel as LogLevel,
console: !logNoConsole,
color: !logNoColor,
Expand Down
3 changes: 2 additions & 1 deletion apps/meerkat/src/app/dap/DAPConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,12 @@ class DAPAssociation extends ClientAssociation {
}), extraLogData);
} else {
ctx.log.info(ctx.i18n.t("log:connection_bound_auth", {
context: ctx.config.log.boundDN ? "with_dn" : undefined,
source: remoteHostIdentifier,
protocol: "DAP",
aid: this.id,
dn: this.boundNameAndUID?.dn
? stringifyDN(ctx, this.boundNameAndUID.dn)
? stringifyDN(ctx, this.boundNameAndUID.dn).slice(0, 512)
: "",
}), extraLogData);
}
Expand Down
27 changes: 22 additions & 5 deletions apps/meerkat/src/app/distributed/dapReplyToLDAPResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
} from "@wildboar/ldap/src/lib/extensions";
import decodeLDAPOID from "@wildboar/ldap/src/lib/decodeLDAPOID";
import { strict as assert } from "assert";
import LDAPAssociation from "../ldap/LDAPConnection";

/**
* @summary Break apart an X.500 SearchResult into LDAP search results
Expand All @@ -81,12 +82,13 @@ import { strict as assert } from "assert";
* @function
* @async
*/
async function getSearchResultEntries (
function getSearchResultEntries (
ctx: Context,
searchResult: SearchResult,
onEntry: (entry: SearchResultEntry) => void,
): Promise<void> {
): number {
const data = getOptionallyProtectedValue(searchResult);
let count: number = 0;
if ("searchInfo" in data) {
for (const einfo of data.searchInfo.entries) {
try {
Expand All @@ -101,11 +103,13 @@ async function getSearchResultEntries (
ctx.log.error(ctx.i18n.t("err:error_converting_entry_to_ldap", { e }));
}
}
count += data.searchInfo.entries.length;
} else if ("uncorrelatedSearchInfo" in data) {
for (const resultSet of data.uncorrelatedSearchInfo) {
getSearchResultEntries(ctx, resultSet, onEntry);
count += getSearchResultEntries(ctx, resultSet, onEntry);
}
}
return count;
}

/**
Expand All @@ -116,8 +120,9 @@ async function getSearchResultEntries (
* ITU X.518 (2016), Section 20.7.
*
* @param ctx The context object
* @param assn The LDAP association.
* @param res The X.500 directory result that is to be translated to an LDAP result
* @param messageId The message ID of the original LDAP request
* @param req The message of the original LDAP request
* @param onEntry A callback that takes a search result
* @param foundDSE The DSE returned by the Find DSE procedure.
* @returns An LDAP message response
Expand All @@ -128,6 +133,7 @@ async function getSearchResultEntries (
export
function dapReplyToLDAPResult (
ctx: Context,
assn: LDAPAssociation,
res: Result,
req: LDAPMessage,
onEntry: (entry: SearchResultEntry) => void,
Expand Down Expand Up @@ -296,7 +302,18 @@ function dapReplyToLDAPResult (
if (compareCode(res.opCode, search["&operationCode"]!)) {
const result = search.decoderFor["&ResultType"]!(res.result!);
const data = getOptionallyProtectedValue(result);
getSearchResultEntries(ctx, result, onEntry);
const count = getSearchResultEntries(ctx, result, onEntry);
const logInfo = {
remoteFamily: assn.socket.remoteFamily,
remoteAddress: assn.socket.remoteAddress,
remotePort: assn.socket.remotePort,
association_id: assn.id,
};
ctx.log.debug(ctx.i18n.t("log:ldap_search_result", {
cid: assn.id,
mid: req.messageID,
count,
}), logInfo);
const responseControls: Control[] = [];
if (sortRequestControl) {
responseControls.push(new Control(
Expand Down
3 changes: 2 additions & 1 deletion apps/meerkat/src/app/dop/DOPConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,11 +633,12 @@ class DOPAssociation extends ClientAssociation {
}), extraLogData);
} else {
ctx.log.info(ctx.i18n.t("log:connection_bound_auth", {
context: ctx.config.log.boundDN ? "with_dn" : undefined,
source: remoteHostIdentifier,
protocol: "DOP",
aid: this.id,
dn: this.boundNameAndUID?.dn
? stringifyDN(ctx, this.boundNameAndUID.dn)
? stringifyDN(ctx, this.boundNameAndUID.dn).slice(0, 512)
: "",
}), extraLogData);
}
Expand Down
3 changes: 2 additions & 1 deletion apps/meerkat/src/app/dsp/DSPConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,11 +583,12 @@ class DSPAssociation extends ClientAssociation {
}), extraLogData);
} else {
ctx.log.info(ctx.i18n.t("log:connection_bound_auth", {
context: ctx.config.log.boundDN ? "with_dn" : undefined,
source: remoteHostIdentifier,
protocol: "DSP",
aid: this.id,
dn: this.boundNameAndUID?.dn
? stringifyDN(ctx, this.boundNameAndUID.dn)
? stringifyDN(ctx, this.boundNameAndUID.dn).slice(0, 512)
: "",
}), extraLogData);
}
Expand Down
46 changes: 41 additions & 5 deletions apps/meerkat/src/app/ldap/LDAPConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptiona
import codeToString from "@wildboar/x500/src/lib/stringifiers/codeToString";
import getServerStatistics from "../telemetry/getServerStatistics";
import getConnectionStatistics from "../telemetry/getConnectionStatistics";
import {
SearchRequest_scope_baseObject,
} from "@wildboar/ldap/src/lib/modules/Lightweight-Directory-Access-Protocol-V3/SearchRequest-scope.ta";
import {
modifyPassword,
whoAmI,
Expand All @@ -73,6 +70,16 @@ import getCommonResultsStatistics from "../telemetry/getCommonResultsStatistics"
import isDebugging from "is-debugging";
import { strict as assert } from "assert";
import { stringifyDN } from "../x500/stringifyDN";
import {
SearchRequest_scope,
SearchRequest_scope_baseObject,
SearchRequest_scope_singleLevel,
SearchRequest_scope_wholeSubtree,
} from "@wildboar/ldap/src/lib/modules/Lightweight-Directory-Access-Protocol-V3/SearchRequest-scope.ta";
import decodeLDAPDN from "./decodeLDAPDN";
import {
stringifyFilter,
} from "@wildboar/ldap/src/lib/stringifiers/Filter";

const UNIVERSAL_SEQUENCE_TAG: number = 0x30;

Expand All @@ -92,6 +99,14 @@ function isRootSubschemaDN (dn: Uint8Array): boolean {
].includes(dnstr);
}

function scopeToString (scope: SearchRequest_scope): string {
return {
[SearchRequest_scope_baseObject]: "base",
[SearchRequest_scope_singleLevel]: "oneLevel",
[SearchRequest_scope_wholeSubtree]: "subtree",
}[scope] ?? "UNKNOWN";
}

async function handleRequest (
ctx: MeerkatContext,
assn: LDAPAssociation,
Expand All @@ -108,6 +123,26 @@ async function handleRequest (
remotePort: assn.socket.remotePort,
association_id: assn.id,
});
if ("searchRequest" in message.protocolOp) {
const sr = message.protocolOp.searchRequest;
ctx.log.debug(ctx.i18n.t("log:ldap_search", {
mid: message.messageID,
cid: assn.id,
scope: scopeToString(sr.scope),
base: stringifyDN(ctx, decodeLDAPDN(ctx, sr.baseObject)).slice(0, 128),
filter: stringifyFilter(sr.filter).slice(0, 128),
sel: sr.attributes.map((attr) => Buffer.from(attr.buffer).toString("utf-8")).join(", "),
typesOnly: sr.typesOnly ? "TRUE": "FALSE",
deref: sr.derefAliases,
size: sr.sizeLimit,
time: sr.timeLimit,
}), {
remoteFamily: assn.socket.remoteFamily,
remoteAddress: assn.socket.remoteAddress,
remotePort: assn.socket.remotePort,
association_id: assn.id,
});
}
// let toWriteBuffer: Buffer = Buffer.alloc(0);
// let resultsBuffered: number = 0;
const onEntry = (searchResEntry: SearchResultEntry): void => {
Expand Down Expand Up @@ -194,7 +229,7 @@ async function handleRequest (
stats.request = result.request ?? stats.request;
stats.outcome = result.outcome ?? stats.outcome;
const unprotectedResult = getOptionallyProtectedValue(result.result);
const ldapResult = dapReplyToLDAPResult(ctx, {
const ldapResult = dapReplyToLDAPResult(ctx, assn, {
invokeId: dapRequest.invokeId,
opCode: dapRequest.opCode,
result: unprotectedResult.result,
Expand Down Expand Up @@ -712,11 +747,12 @@ class LDAPAssociation extends ClientAssociation {
}), extraLogData);
} else {
ctx.log.info(ctx.i18n.t("log:connection_bound_auth", {
context: ctx.config.log.boundDN ? "with_dn" : undefined,
source: remoteHostIdentifier,
protocol: "LDAP",
aid: this.id,
dn: this.boundNameAndUID?.dn
? stringifyDN(ctx, this.boundNameAndUID.dn)
? stringifyDN(ctx, this.boundNameAndUID.dn).slice(0, 512)
: "",
}), extraLogData);
}
Expand Down
4 changes: 3 additions & 1 deletion apps/meerkat/src/assets/locales/en/language/log.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,7 @@
"lcr_invalid_invoke_id_response": "Invalid invokeID format in response from remote DSA {{ae}} in chained list continuation for invocation {{iid}}. This is either a bug or malicious behavior.",
"lcr_mismatch_invoke_id": "InvokeID in response from remote DSA {{ae}} in chained list continuation for invocation {{iid}} did not match the request. This is a bug. Please report it to https://github.com/Wildboar-Software/directory/issues.",
"lcr_missing_opcode": "Missing opCode in response from remote DSA {{ae}} in chained list continuation for invocation {{iid}}. This is either a bug or malicious behavior.",
"lcr_mismatch_opcode": "OpCode in response from remote DSA {{ae}} in chained list continuation for invocation {{iid}} was not the opCode for list. This is either a bug or malicious behavior."
"lcr_mismatch_opcode": "OpCode in response from remote DSA {{ae}} in chained list continuation for invocation {{iid}} was not the opCode for list. This is either a bug or malicious behavior.",
"ldap_search": "{{cid}}#{{mid}}: LDAP search: SCOPE={{scope}}; BASE='{{base}}'; FILTER='{{filter}}'; SELECTION='{{sel}}'; TYPES_ONLY={{typesOnly}}; DEREF_ALIASES={{deref}}; SIZE_LIMIT={{size}}; TIME_LIMIT={{time}}.",
"ldap_search_result": "{{cid}}#{{mid}}: LDAP search returned {{count}} results."
}
4 changes: 2 additions & 2 deletions apps/meerkat/src/assets/static/conformance.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Conformance

In the statements below, the term "Meerkat DSA" refers to version 1.2.3 of
Meerkat DSA, hence these statements are only claimed for version 1.2.3 of
In the statements below, the term "Meerkat DSA" refers to version 1.2.4 of
Meerkat DSA, hence these statements are only claimed for version 1.2.4 of
Meerkat DSA.

## X.519 Conformance Statement
Expand Down
4 changes: 2 additions & 2 deletions k8s/charts/meerkat-dsa/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: meerkat-dsa
description: X.500 Directory Server (DSA) and LDAP Server by Wildboar Software.
type: application
version: 2.3.2
appVersion: 1.2.3
version: 2.3.3
appVersion: 1.2.4
home: https://wildboarsoftware.com
keywords:
- directory
Expand Down
1 change: 1 addition & 0 deletions k8s/charts/meerkat-dsa/templates/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ data:
ldap_buffer_size: {{ .Values.ldap_buffer_size | default "1000000" | quote }}
ldap_port: {{ .Values.ldap_port | default 389 | quote }}
ldaps_port: {{ .Values.ldaps_port | default 636 | quote }}
log_bound_dn: {{ .Values.log_bound_dn | default false | ternary 1 0 | quote }}
local_qualifier_points_for_using_ssl3: {{ .Values.local_qualifier_points_for_using_starttls | default 8 | quote }}
local_qualifier_points_for_using_starttls: {{ .Values.local_qualifier_points_for_using_starttls | default 16 | quote }}
local_qualifier_points_for_using_tls: {{ .Values.local_qualifier_points_for_using_starttls | default 32 | quote }}
Expand Down
6 changes: 6 additions & 0 deletions k8s/charts/meerkat-dsa/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ spec:
name: {{ include "meerkat-dsa.fullname" . }}-config
key: local_qualifier_points_for_using_tls_1_3
optional: true
- name: MEERKAT_LOG_BOUND_DN
valueFrom:
configMapKeyRef:
name: {{ include "meerkat-dsa.fullname" . }}-config
key: log_bound_dn
optional: true
- name: MEERKAT_LOG_FILE
valueFrom:
configMapKeyRef:
Expand Down
1 change: 1 addition & 0 deletions k8s/charts/meerkat-dsa/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ local_qualifier_points_for_using_tls_1_0: 32
local_qualifier_points_for_using_tls_1_1: 64
local_qualifier_points_for_using_tls_1_2: 128
local_qualifier_points_for_using_tls_1_3: 256
log_bound_dn: false
# log_file: /var/log/meerkat.log
# log_file_max_files: 100
# log_file_max_size: "1000000"
Expand Down
9 changes: 9 additions & 0 deletions libs/meerkat-types/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,15 @@ interface Configuration {
authn: AuthenticationConfiguration;

log: {
/**
* If true, Meerkat DSA will log distinguished names of bound clients,
* if they are not bound anonymously. It may be useful to log this for
* debugging purposes, but it may also be desirable to leave this
* disabled, because Meerkat DSA logs IP addresses, and tying an IP
* address to a distinguished name could have data privacy law
* implications.
*/
boundDN: boolean;
level: LogLevel;
color: boolean;
timestamp: boolean;
Expand Down
2 changes: 1 addition & 1 deletion pkg/control
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Package: meerkat-dsa
Version: 1.2.3
Version: 1.2.4
Section: database
Priority: optional
Architecture: i386
Expand Down
2 changes: 1 addition & 1 deletion pkg/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ services:
labels:
author: Wildboar Software
app: meerkat
version: "1.2.3"
version: "1.2.4"
ports:
- '1389:389/tcp' # LDAP TCP Port
- '4632:4632/tcp' # IDM Socket
Expand Down
2 changes: 1 addition & 1 deletion pkg/meerkat-dsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class MeerkatDSA < Formula
desc "X.500 Directory Server (DSA) and LDAP Server by Wildboar Software"
homepage "https://github.com/Wildboar-Software/directory"
url "https://github.com/Wildboar-Software/directory/archive/v1.1.0.tar.gz"
version = "1.2.3"
version = "1.2.4"
# sha256 "e86694b2e15d8d4da2477c44e584fb5e860666787d010801199a0a77bcf28a2d"

def install
Expand Down
2 changes: 1 addition & 1 deletion snap/snapcraft.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: meerkat-dsa
base: core20
version: '1.2.3'
version: '1.2.4'
summary: X.500 Directory (DSA) and LDAP Server
description: |
Fully-featured X.500 directory server / directory system agent (DSA)
Expand Down

0 comments on commit 85f9121

Please sign in to comment.