Skip to content

Enable API Security by default and make it lazy loading #9009

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

Merged
merged 2 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class AppSecSystem {
private static ReplaceableEventProducerService REPLACEABLE_EVENT_PRODUCER; // testing
private static Runnable STOP_SUBSCRIPTION_SERVICE;
private static Runnable RESET_SUBSCRIPTION_SERVICE;
private static final AtomicBoolean API_SECURITY_INITIALIZED = new AtomicBoolean(false);
private static volatile ApiSecuritySampler API_SECURITY_SAMPLER = new ApiSecuritySampler.NoOp();

public static void start(SubscriptionService gw, SharedCommunicationObjects sco) {
try {
Expand All @@ -69,18 +71,6 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s
EventDispatcher eventDispatcher = new EventDispatcher();
REPLACEABLE_EVENT_PRODUCER.replaceEventProducerService(eventDispatcher);

ApiSecuritySampler requestSampler;
if (Config.get().isApiSecurityEnabled()) {
requestSampler = new ApiSecuritySamplerImpl();
// When DD_API_SECURITY_ENABLED=true, ths post-processor is set even when AppSec is inactive.
// This should be low overhead since the post-processor exits early if there's no AppSec
// context.
SpanPostProcessor.Holder.INSTANCE =
new AppSecSpanPostProcessor(requestSampler, REPLACEABLE_EVENT_PRODUCER);
} else {
requestSampler = new ApiSecuritySampler.NoOp();
}

ConfigurationPoller configurationPoller = sco.configurationPoller(config);
// may throw and abort startup
APP_SEC_CONFIG_SERVICE =
Expand All @@ -94,7 +84,7 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s
new GatewayBridge(
gw,
REPLACEABLE_EVENT_PRODUCER,
requestSampler,
() -> API_SECURITY_SAMPLER,
APP_SEC_CONFIG_SERVICE.getTraceSegmentPostProcessors());

loadModules(eventDispatcher, sco.monitoring);
Expand Down Expand Up @@ -129,6 +119,9 @@ public static void setActive(boolean status) {
log.debug("AppSec is now {}", status ? "active" : "inactive");
ProductChangeCollector.get()
.update(new ProductChange().productType(ProductChange.ProductType.APPSEC).enabled(status));
if (status) {
maybeInitializeApiSecurity();
}
}

public static void stop() {
Expand Down Expand Up @@ -196,6 +189,25 @@ private static void reloadSubscriptions(
}
}

private static void maybeInitializeApiSecurity() {
if (!Config.get().isApiSecurityEnabled()) {
return;
}
if (!ActiveSubsystems.APPSEC_ACTIVE) {
return;
}
// We initialize API Security the first time AppSec becomes active.
// We never de-initialize it, as that could lead to a leak of open WAF contexts in-flight.
if (API_SECURITY_INITIALIZED.compareAndSet(false, true)) {
if (SpanPostProcessor.Holder.INSTANCE == SpanPostProcessor.Holder.NOOP) {
Copy link
Member

@manuel-alvarez-alvarez manuel-alvarez-alvarez Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctly if I'm wrong, but this should also have a positive effect on span processing overhead when appsec is disabled right? (I'm asking because of the regressions in the high_load benchmarks, that might be spurious)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this should ensure that the post-processor is always no-op unless enabled.

ApiSecuritySampler requestSampler = new ApiSecuritySamplerImpl();
SpanPostProcessor.Holder.INSTANCE =
new AppSecSpanPostProcessor(requestSampler, REPLACEABLE_EVENT_PRODUCER);
API_SECURITY_SAMPLER = requestSampler;
}
}
}

public static boolean isStarted() {
return STARTED.get();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -89,7 +91,7 @@ public class GatewayBridge {

private final SubscriptionService subscriptionService;
private final EventProducerService producerService;
private final ApiSecuritySampler requestSampler;
private final Supplier<ApiSecuritySampler> requestSamplerSupplier;
private final List<TraceSegmentPostProcessor> traceSegmentPostProcessors;

// subscriber cache
Expand All @@ -115,11 +117,11 @@ public class GatewayBridge {
public GatewayBridge(
SubscriptionService subscriptionService,
EventProducerService producerService,
ApiSecuritySampler requestSampler,
@Nonnull Supplier<ApiSecuritySampler> requestSamplerSupplier,
List<TraceSegmentPostProcessor> traceSegmentPostProcessors) {
this.subscriptionService = subscriptionService;
this.producerService = producerService;
this.requestSampler = requestSampler;
this.requestSamplerSupplier = requestSamplerSupplier;
this.traceSegmentPostProcessors = traceSegmentPostProcessors;
}

Expand Down Expand Up @@ -778,6 +780,7 @@ private boolean maybeSampleForApiSecurity(
if (route != null) {
ctx.setRoute(route.toString());
}
ApiSecuritySampler requestSampler = requestSamplerSupplier.get();
return requestSampler.preSampleRequest(ctx);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class GatewayBridgeSpecification extends DDSpecification {

TraceSegmentPostProcessor pp = Mock()
ApiSecuritySamplerImpl requestSampler = Mock(ApiSecuritySamplerImpl)
GatewayBridge bridge = new GatewayBridge(ig, eventDispatcher, requestSampler, [pp])
GatewayBridge bridge = new GatewayBridge(ig, eventDispatcher, () -> requestSampler, [pp])

Supplier<Flow<AppSecRequestContext>> requestStartedCB
BiFunction<RequestContext, AgentSpan, Flow<Void>> requestEndedCB
Expand Down Expand Up @@ -258,8 +258,9 @@ class GatewayBridgeSpecification extends DDSpecification {
ctx.data.rawURI = '/'
ctx.data.peerAddress = '0.0.0.0'
eventDispatcher.getDataSubscribers(_) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
{ bundle = it[2]; NoopFlow.INSTANCE }
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From here onwards, this is just spotless being funny.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let it have its fun then 😓

bundle = it[2]; NoopFlow.INSTANCE
}

and:
reqHeadersDoneCB.apply(ctx)
Expand All @@ -277,8 +278,9 @@ class GatewayBridgeSpecification extends DDSpecification {

when:
eventDispatcher.getDataSubscribers(_) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
{ bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE }
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE
}

and:
reqHeadersDoneCB.apply(ctx)
Expand All @@ -298,8 +300,9 @@ class GatewayBridgeSpecification extends DDSpecification {

when:
eventDispatcher.getDataSubscribers(_) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
{ bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE }
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE
}

and:
reqHeadersDoneCB.apply(ctx)
Expand All @@ -319,8 +322,9 @@ class GatewayBridgeSpecification extends DDSpecification {

when:
eventDispatcher.getDataSubscribers(_) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
{ bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE }
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE
}

and:
reqHeadersDoneCB.apply(ctx)
Expand All @@ -339,9 +343,12 @@ class GatewayBridgeSpecification extends DDSpecification {
def adapter = TestURIDataAdapter.create(uri, supportsRaw)

when:
eventDispatcher.getDataSubscribers({ KnownAddresses.REQUEST_URI_RAW in it }) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
{ bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE }
eventDispatcher.getDataSubscribers({
KnownAddresses.REQUEST_URI_RAW in it
}) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE
}

and:
requestMethodURICB.apply(ctx, 'GET', adapter)
Expand Down Expand Up @@ -373,9 +380,12 @@ class GatewayBridgeSpecification extends DDSpecification {
def adapter = TestURIDataAdapter.create(uri)

when:
eventDispatcher.getDataSubscribers({ KnownAddresses.REQUEST_URI_RAW in it }) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
{ bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE }
eventDispatcher.getDataSubscribers({
KnownAddresses.REQUEST_URI_RAW in it
}) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE
}

and:
requestMethodURICB.apply(ctx, 'GET', adapter)
Expand Down Expand Up @@ -406,9 +416,12 @@ class GatewayBridgeSpecification extends DDSpecification {
GatewayContext gatewayContext

when:
eventDispatcher.getDataSubscribers({ KnownAddresses.REQUEST_PATH_PARAMS in it }) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
{ bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE }
eventDispatcher.getDataSubscribers({
KnownAddresses.REQUEST_PATH_PARAMS in it
}) >> nonEmptyDsInfo
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> {
bundle = it[2]; gatewayContext = it[3]; NoopFlow.INSTANCE
}

and:
pathParamsCB.apply(ctx, [a: 'b'])
Expand Down Expand Up @@ -663,9 +676,9 @@ class GatewayBridgeSpecification extends DDSpecification {

when:
Flow<?> flow = requestBodyProcessedCB.apply(ctx, new Object() {
@SuppressWarnings('UnusedPrivateField')
private String foo = 'bar'
})
@SuppressWarnings('UnusedPrivateField')
private String foo = 'bar'
})

then:
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
Expand Down Expand Up @@ -762,9 +775,9 @@ class GatewayBridgeSpecification extends DDSpecification {

when:
Flow<?> flow = grpcServerRequestMessageCB.apply(ctx, new Object() {
@SuppressWarnings('UnusedPrivateField')
private String foo = 'bar'
})
@SuppressWarnings('UnusedPrivateField')
private String foo = 'bar'
})

then:
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >>
Expand Down Expand Up @@ -919,28 +932,28 @@ class GatewayBridgeSpecification extends DDSpecification {

void 'no appsec events if was not created request context in request_start event'() {
RequestContext emptyCtx = new RequestContext() {
final Object data = null
BlockResponseFunction blockResponseFunction

@Override
Object getData(RequestContextSlot slot) {
data
}

@Override
final TraceSegment getTraceSegment() {
GatewayBridgeSpecification.this.traceSegment
}

@Override
def <T> T getOrCreateMetaStructTop(String key, Function<String, T> defaultValue) {
return null
}

@Override
void close() throws IOException {}
final Object data = null
BlockResponseFunction blockResponseFunction

@Override
Object getData(RequestContextSlot slot) {
data
}

@Override
final TraceSegment getTraceSegment() {
GatewayBridgeSpecification.this.traceSegment
}

@Override
def <T> T getOrCreateMetaStructTop(String key, Function<String, T> defaultValue) {
return null
}

@Override
void close() throws IOException {}
}

StoredBodySupplier supplier = Stub()
IGSpanInfo spanInfo = Stub(AgentSpan)
Object obj = 'obj'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public final class ConfigDefaults {
static final int DEFAULT_APPSEC_TRACE_RATE_LIMIT = 100;
static final boolean DEFAULT_APPSEC_WAF_METRICS = true;
static final int DEFAULT_APPSEC_WAF_TIMEOUT = 100000; // 0.1 s
static final boolean DEFAULT_API_SECURITY_ENABLED = false;
static final boolean DEFAULT_API_SECURITY_ENABLED = true;
static final float DEFAULT_API_SECURITY_SAMPLE_DELAY = 30.0f;
// TODO: change to true once the RFC is approved
static final boolean DEFAULT_API_SECURITY_ENDPOINT_COLLECTION_ENABLED = false;
Expand Down
Loading