Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include client IP address in audit log #147526

Merged
merged 3 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/user/security/audit-logging.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,19 @@ Example: `[marketing]`
| *Field*
| *Description*

| `client.ip`
| Client IP address.

| `http.request.method`
| HTTP request method.

Example: `get`, `post`, `put`, `delete`

| `http.request.headers.x-forwarded-for`
| `X-Forwarded-For` request header used to identify the originating client IP address when connecting through proxy server(s).
thomheymann marked this conversation as resolved.
Show resolved Hide resolved

Example: `161.66.20.177, 236.198.214.101`

| `url.domain`
| Domain of the URL.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,11 @@ describe('KibanaSocket', () => {
expect(socket.authorizationError).toBe(authorizationError);
});
});

describe('remoteAddress', () => {
it('mirrors the value of net.Socket instance', () => {
const socket = new KibanaSocket({ remoteAddress: '1.1.1.1' } as Socket);
expect(socket.remoteAddress).toBe('1.1.1.1');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export class KibanaSocket implements IKibanaSocket {
return this.socket instanceof TLSSocket ? this.socket.authorizationError : undefined;
}

public get remoteAddress() {
return this.socket.remoteAddress;
}

constructor(private readonly socket: Socket) {}

getPeerCertificate(detailed: true): DetailedPeerCertificate | null;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/http/core-http-server/src/router/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ export interface IKibanaSocket {
* only when `authorized` is `false`.
*/
readonly authorizationError?: Error;

/**
* The string representation of the remote IP address. For example,`'74.125.127.100'` or
* `'2001:4860:a005::68'`. Value may be `undefined` if the socket is destroyed (for example, if
* the client disconnected).
*/
readonly remoteAddress?: string;
}
109 changes: 67 additions & 42 deletions x-pack/plugins/security/server/audit/audit_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,71 @@ import type { EcsEventOutcome, EcsEventType, KibanaRequest, LogMeta } from '@kbn
import type { AuthenticationProvider } from '../../common/model';
import type { AuthenticationResult } from '../authentication/authentication_result';

/**
* Audit kibana schema using ECS format
*/
export interface AuditKibana {
/**
* The ID of the space associated with this event.
*/
space_id?: string;
/**
* The ID of the user session associated with this event. Each login attempt
* results in a unique session id.
*/
session_id?: string;
/**
* Saved object that was created, changed, deleted or accessed as part of this event.
*/
saved_object?: {
type: string;
id: string;
};
/**
* Name of authentication provider associated with a login event.
*/
authentication_provider?: string;
/**
* Type of authentication provider associated with a login event.
*/
authentication_type?: string;
/**
* Name of Elasticsearch realm that has authenticated the user.
*/
authentication_realm?: string;
/**
* Name of Elasticsearch realm where the user details were retrieved from.
*/
lookup_realm?: string;
/**
* Set of space IDs that a saved object was shared to.
*/
add_to_spaces?: readonly string[];
/**
* Set of space IDs that a saved object was removed from.
*/
delete_from_spaces?: readonly string[];
}

type EcsHttp = Required<LogMeta>['http'];
type EcsRequest = Required<EcsHttp>['request'];

/**
* Audit request schema using ECS format
*/
export interface AuditRequest extends EcsRequest {
headers?: {
'x-forwarded-for'?: string;
};
}

/**
* Audit http schema using ECS format
*/
export interface AuditHttp extends EcsHttp {
request?: AuditRequest;
}

/**
* Audit event schema using ECS format: https://www.elastic.co/guide/en/ecs/1.12/index.html
*
Expand All @@ -24,48 +89,8 @@ import type { AuthenticationResult } from '../authentication/authentication_resu
*/
export interface AuditEvent extends LogMeta {
message: string;
kibana?: {
/**
* The ID of the space associated with this event.
*/
space_id?: string;
/**
* The ID of the user session associated with this event. Each login attempt
* results in a unique session id.
*/
session_id?: string;
/**
* Saved object that was created, changed, deleted or accessed as part of this event.
*/
saved_object?: {
type: string;
id: string;
};
/**
* Name of authentication provider associated with a login event.
*/
authentication_provider?: string;
/**
* Type of authentication provider associated with a login event.
*/
authentication_type?: string;
/**
* Name of Elasticsearch realm that has authenticated the user.
*/
authentication_realm?: string;
/**
* Name of Elasticsearch realm where the user details were retrieved from.
*/
lookup_realm?: string;
/**
* Set of space IDs that a saved object was shared to.
*/
add_to_spaces?: readonly string[];
/**
* Set of space IDs that a saved object was removed from.
*/
delete_from_spaces?: readonly string[];
};
kibana?: AuditKibana;
http?: AuditHttp;
}

export interface HttpRequestParams {
Expand Down
44 changes: 42 additions & 2 deletions x-pack/plugins/security/server/audit/audit_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import type { Socket } from 'net';
import { lastValueFrom, Observable, of } from 'rxjs';

import {
Expand All @@ -22,6 +23,7 @@ import {
AuditService,
createLoggingConfig,
filterEvent,
getForwardedFor,
RECORD_USAGE_INTERVAL,
} from './audit_service';

Expand Down Expand Up @@ -186,14 +188,26 @@ describe('#asScoped', () => {
recordAuditLoggingUsage,
});
const request = httpServerMock.createKibanaRequest({
socket: { remoteAddress: '3.3.3.3' } as Socket,
headers: {
'x-forwarded-for': '1.1.1.1, 2.2.2.2',
},
kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' },
});

await auditSetup.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } });
expect(logger.info).toHaveBeenCalledWith('MESSAGE', {
await auditSetup.asScoped(request).log({
message: 'MESSAGE',
event: { action: 'ACTION' },
http: { request: { method: 'GET' } },
});
expect(logger.info).toHaveBeenLastCalledWith('MESSAGE', {
event: { action: 'ACTION' },
kibana: { space_id: 'default', session_id: 'SESSION_ID' },
trace: { id: 'REQUEST_ID' },
client: { ip: '3.3.3.3' },
http: {
request: { method: 'GET', headers: { 'x-forwarded-for': '1.1.1.1, 2.2.2.2' } },
},
user: { id: 'uid', name: 'jdoe', roles: ['admin'] },
});
audit.stop();
Expand Down Expand Up @@ -424,6 +438,32 @@ describe('#createLoggingConfig', () => {
});
});

describe('#getForwardedFor', () => {
it('extracts x-forwarded-for header from request', () => {
const request = httpServerMock.createKibanaRequest({
headers: {
'x-forwarded-for': '1.1.1.1',
},
});
expect(getForwardedFor(request)).toBe('1.1.1.1');
});

it('concatenates multiple headers into single string in correct order', () => {
const request = httpServerMock.createKibanaRequest({
headers: {
// @ts-expect-error Headers can be arrays but HAPI mocks are incorrectly typed
'x-forwarded-for': ['1.1.1.1, 2.2.2.2', '3.3.3.3'],
},
});
expect(getForwardedFor(request)).toBe('1.1.1.1, 2.2.2.2, 3.3.3.3');
});

it('returns undefined when header not present', () => {
const request = httpServerMock.createKibanaRequest();
expect(getForwardedFor(request)).toBeUndefined();
});
});

describe('#filterEvent', () => {
let event: AuditEvent;

Expand Down
27 changes: 27 additions & 0 deletions x-pack/plugins/security/server/audit/audit_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export class AuditService {
const spaceId = getSpaceId(request);
const user = getCurrentUser(request);
const sessionId = await getSID(request);
const forwardedFor = getForwardedFor(request);

log({
...event,
user:
Expand All @@ -177,6 +179,18 @@ export class AuditService {
...event.kibana,
},
trace: { id: request.id },
client: { ip: request.socket.remoteAddress },
http: forwardedFor
? {
...event.http,
request: {
...event.http?.request,
headers: {
'x-forwarded-for': forwardedFor,
},
},
}
: event.http,
});
},
enabled,
Expand Down Expand Up @@ -243,3 +257,16 @@ export function filterEvent(
}
return true;
}

/**
* Extracts `X-Forwarded-For` header(s) from `KibanaRequest`.
*/
export function getForwardedFor(request: KibanaRequest) {
const forwardedFor = request.headers['x-forwarded-for'];

if (Array.isArray(forwardedFor)) {
return forwardedFor.join(', ');
}

return forwardedFor;
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function ({ getService }: FtrProviderContext) {
await supertest
.post('/internal/security/login')
.set('kbn-xsrf', 'xxx')
.set('X-Forwarded-For', '1.1.1.1, 2.2.2.2')
.send({
providerType: 'basic',
providerName: 'basic',
Expand All @@ -71,12 +72,15 @@ export default function ({ getService }: FtrProviderContext) {
expect(loginEvent.event.outcome).to.be('success');
expect(loginEvent.trace.id).to.be.ok();
expect(loginEvent.user.name).to.be(username);
expect(loginEvent.client.ip).to.be.ok();
expect(loginEvent.http.request.headers['x-forwarded-for']).to.be('1.1.1.1, 2.2.2.2');
});

it('logs audit events when failing to log in', async () => {
await supertest
.post('/internal/security/login')
.set('kbn-xsrf', 'xxx')
.set('X-Forwarded-For', '1.1.1.1, 2.2.2.2')
.send({
providerType: 'basic',
providerName: 'basic',
Expand All @@ -93,6 +97,8 @@ export default function ({ getService }: FtrProviderContext) {
expect(loginEvent.event.outcome).to.be('failure');
expect(loginEvent.trace.id).to.be.ok();
expect(loginEvent.user).not.to.be.ok();
expect(loginEvent.client.ip).to.be.ok();
expect(loginEvent.http.request.headers['x-forwarded-for']).to.be('1.1.1.1, 2.2.2.2');
});
});
}