mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Include client IP address in audit log (#147526)
Resolves #127481 ## Release notes Include IP address in audit log ## Testing 1. Update `kibana.dev.yaml`: ```yaml xpack.security.audit.enabled: true xpack.security.audit.appender: type: console layout: type: json ``` 2. Observe audit logs in console when interacting with Kibana: ```json { "@timestamp": "2022-12-13T15:50:42.236+00:00", "message": "User is requesting [/dev/internal/security/me] endpoint", "client": { "ip": "127.0.0.1" }, "http": { "request": { "headers": { "x-forwarded-for": "1.1.1.1, 127.0.0.1" } } } } ``` Note: You will see the `x-forwarded-for` field populated when running Kibana in development mode (`yarn start`) since Kibana runs behind a development proxy. Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
This commit is contained in:
parent
a2036f0ecf
commit
a02c7dce50
8 changed files with 168 additions and 44 deletions
|
@ -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 servers.
|
||||
|
||||
Example: `161.66.20.177, 236.198.214.101`
|
||||
|
||||
| `url.domain`
|
||||
| Domain of the URL.
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
@ -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 {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Socket } from 'net';
|
||||
import { lastValueFrom, Observable, of } from 'rxjs';
|
||||
|
||||
import {
|
||||
|
@ -22,6 +23,7 @@ import {
|
|||
AuditService,
|
||||
createLoggingConfig,
|
||||
filterEvent,
|
||||
getForwardedFor,
|
||||
RECORD_USAGE_INTERVAL,
|
||||
} from './audit_service';
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue