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

nil access when enabling ASM without enabling tracing #3564

Closed
frsantos opened this issue Apr 1, 2024 · 10 comments · Fixed by #3645
Closed

nil access when enabling ASM without enabling tracing #3564

frsantos opened this issue Apr 1, 2024 · 10 comments · Fixed by #3645
Labels
bug Involves a bug community Was opened by a community member

Comments

@frsantos
Copy link

frsantos commented Apr 1, 2024

Background: we enable tracing dynamically per host, so it's disabled by default and later enabled at runtime by looking at some AWS tags.

We are trying ASM, so we just pasted the recommended config:

c.appsec.enabled = true
c.appsec.instrument :rails

However, it seems that ASM has a strong dependency over tracing, and one can't be enabled without enabling the other.

Current behaviour
We get an undefined method 'set_tag' for nil:NilClass exception on lib/datadog/appsec/contrib/devise/tracking.rb:34 when logging in.

undefined method `set_tag' for nil:NilClass

ddtrace (1.21.1) lib/datadog/appsec/contrib/devise/tracking.rb:34:in `track'
ddtrace (1.21.1) lib/datadog/appsec/contrib/devise/tracking.rb:16:in `track_login_success'
ddtrace (1.21.1) lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb:40:in `validate'
devise (4.8.1) lib/devise/strategies/database_authenticatable.rb:13:in `authenticate!'
warden (1.2.9) lib/warden/strategies/base.rb:55:in `_run!'
warden (1.2.9) lib/warden/proxy.rb:372:in `block in _run_strategies_for'
warden (1.2.9) lib/warden/proxy.rb:365:in `each'
warden (1.2.9) lib/warden/proxy.rb:365:in `_run_strategies_for'
warden (1.2.9) lib/warden/proxy.rb:335:in `_perform_authentication'
warden (1.2.9) lib/warden/proxy.rb:133:in `authenticate!'
devise (4.8.1) app/controllers/devise/sessions_controller.rb:19:in `create'
actionpack (6.0.5.1) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
actionpack (6.0.5.1) lib/abstract_controller/base.rb:195:in `process_action'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rails/patcher.rb:83:in `block in process_action'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:37:in `block in push'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rails/gateway/watcher.rb:53:in `block in watch_request_action'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:19:in `call'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:43:in `block (2 levels) in push'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:47:in `push'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rails/patcher.rb:82:in `process_action'
ddtrace (1.21.1) lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb:105:in `process_action'
actionpack (6.0.5.1) lib/action_controller/metal/rendering.rb:30:in `process_action'
...
actionpack (6.0.5.1) lib/action_dispatch/middleware/host_authorization.rb:103:in `call'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rack/request_middleware.rb:61:in `block (2 levels) in call'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:37:in `block in push'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rack/gateway/watcher.rb:56:in `block in watch_request'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:19:in `call'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:43:in `block (2 levels) in push'
ddtrace (1.21.1) lib/datadog/appsec/instrumentation/gateway.rb:47:in `push'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rack/request_middleware.rb:60:in `block in call'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rack/request_middleware.rb:59:in `catch'
ddtrace (1.21.1) lib/datadog/appsec/contrib/rack/request_middleware.rb:59:in `call'
ddtrace (1.21.1) lib/datadog/tracing/contrib/rack/middlewares.rb:109:in `call'
webpacker (5.4.3) lib/webpacker/dev_server_proxy.rb:25:in `perform_request'

Expected behaviour
No errors, to disable ASM if tracing is disabled, or some exception at configuration time. Not sure what should be the behaviour.

To bypass the error, we just disabled appsec on static config and enabled it dynamically along with tracing.

Steps to reproduce

  • Disable tracing and enable ASM
Datadog.configure do |c|
  c.tracing.enabled = false
  c.tracing.instrument :rails

  c.appsec.enabled = true
  c.appsec.instrument :rails
end
  • Login using devise on a rails application.

How does ddtrace help you?

Environment

  • ddtrace version: 1.21.1
  • Configuration block (Datadog.configure ...):
  • Ruby version: 3.0.6
  • Operating system: Ubuntu 23.10
  • Relevant library versions:
@frsantos frsantos added bug Involves a bug community Was opened by a community member labels Apr 1, 2024
@lloeki
Copy link
Contributor

lloeki commented Apr 2, 2024

Thanks for the report and sorry for the issue.

However, it seems that ASM has a strong dependency over tracing, and one can't be enabled without enabling the other.

It's not supposed to be so! AppSec should not crash when Tracing is disabled, it should merely not leverage spans and traces. We even have specs for that.

Unfortunately it seems that the Devise integration does not account for that. We'll track this internally and be looking to fix this oversight.

Your workaround is a good one.

we enable tracing dynamically per host, so it's disabled by default and later enabled at runtime by looking at some AWS tags

A very interesting use case! Would you be able to elaborate on what exactly you mean by "dynamically per host" as well as "later enabled at runtime by looking at some AWS tags"?

Especially, when in the app lifecycle (e.g at the Rails initializer stage?) is Tracing being enabled?

@frsantos
Copy link
Author

frsantos commented Apr 2, 2024

Unfortunately it seems that the Devise integration does not account for that. We'll track this internally and be looking to fix this oversight.

Nice

Your workaround is a good one.

we enable tracing dynamically per host, so it's disabled by default and later enabled at runtime by looking at some AWS tags

A very interesting use case! Would you be able to elaborate on what exactly you mean by "dynamically per host" as well as "later enabled at runtime by looking at some AWS tags"?

Especially, when in the app lifecycle (e.g at the Rails initializer stage?) is Tracing being enabled?

We have a fleet of AWS servers, and only some of them are tagged on AWS to report to Datadog. As we add/remove servers, the servers are retagged to keep a specific number of reporting servers of each class (web/background jobs). In rails, there is a middleware that periodically checks if the current server is tagged or not, and enables/disables Datadog tracing (and now AppSec).

I suppose Datadog maybe now has a configuration for this, but this was added long time ago and it's working for us fine.

@lloeki
Copy link
Contributor

lloeki commented Apr 2, 2024

In rails, there is a middleware that periodically checks if the current server is tagged or not, and enables/disables Datadog tracing (and now AppSec).

Is this a Rack middleware? If so this does the configuration via Datadog.configure while the app is live and serving requests?

@frsantos
Copy link
Author

frsantos commented Apr 2, 2024

In rails, there is a middleware that periodically checks if the current server is tagged or not, and enables/disables Datadog tracing (and now AppSec).

Is this a Rack middleware? If so this does the configuration via Datadog.configure while the app is live and serving requests?

Yes. We use unicorn, so it's enabled/disabled just at the beginning of a request, and no other requests are running.

@lloeki
Copy link
Contributor

lloeki commented Apr 2, 2024

OK you got me worried a moment 😅. If there were any concurrency there would have been problems as Datadog.configure is not designed to be operated live, but in the unicorn case it should be fine as long as your middleware is at the very top of the stack, more precisely before Datadog's Tracing and AppSec ones: if it's after then the request that triggers enablement will not have a trace started, and if it's disablement it may have a trace then have tracing disabled during the request, which itself could cause some occasional issues.

Thanks for the details, we'll keep that kind of use case in mind for the future.

@frsantos
Copy link
Author

frsantos commented Apr 2, 2024

OK you got me worried a moment 😅. If there were any concurrency there would have been problems as Datadog.configure is not designed to be operated live, but in the unicorn case it should be fine as long as your middleware is at the very top of the stack, more precisely before Datadog's Tracing and AppSec ones

Exactly, at the very top

: if it's after then the request that triggers enablement will not have a trace started, and if it's disablement it may have a trace then have tracing disabled during the request, which itself could cause some occasional issues.

Thanks for the details, we'll keep that kind of use case in mind for the future.

Thanks to you!

@glaucocustodio
Copy link

glaucocustodio commented May 13, 2024

We are using ddtrace 1.17 and devise 4.8.1 with the following initializer file in our rails app and getting the same error NoMethodError: undefined method set_tag for nil:

service_name = "companyx-admin"
datadog_tracing_enabled = ENV.fetch("DATADOG_TRACING_ENABLED", "false") == "true"
datadog_agent_host = if datadog_tracing_enabled
  ENV.fetch("DATADOG_AGENT_HOST") do
    Timeout.timeout(3) { Net::HTTP.get(URI.parse(ENV["AWS_METADATA_IP_URL"])) }
  end
end

Datadog.configure do |c|
  c.tracing.enabled = datadog_tracing_enabled
  c.diagnostics.startup_logs.enabled = datadog_tracing_enabled
  c.telemetry.enabled = false
  c.agent.host = datadog_agent_host
  c.env = ENV["RAILS_ENV"]
  c.tags = {env: ENV["RAILS_ENV"]}

  if datadog_tracing_enabled
    # when Rails instrumentation is enabled,
    # logs will be injected with trace_id and span_id,
    # making them more verbose and harder to read
    c.tracing.instrument :rails, service_name: service_name, log_injection: true
  end
  c.tracing.instrument :active_record, service_name: "#{service_name}-postgres", orm_service_name: "#{service_name}-activerecord"
  c.tracing.instrument :sidekiq, service_name: "#{service_name}-sidekiq", client_service_name: "#{service_name}-sidekiq-client"
  c.tracing.instrument :redis, service_name: "#{service_name}-redis"
  c.tracing.instrument :faraday, service_name: "#{service_name}-faraday"
end

Datadog::Statsd.new(datadog_agent_host, 8125, {namespace: "foo"})

Disabling appsec by setting DD_APPSEC_ENABLED=false made the error stop.

@lloeki
Copy link
Contributor

lloeki commented May 14, 2024

@glaucocustodio in your situation, instead of DD_APPSEC_ENABLED=false (or true for appsec auto-instrument) the following might do the trick:

c.appsec.enabled = datadog_tracing_enabled
c.appsec.instrument :rails
c.appsec.instrument :devise

But I understand that you may want to separately toggle appsec and tracing.

Tangentially, I'm wondering why this block is not conditioned by datadog_tracing_enabled:

  c.tracing.instrument :active_record, service_name: "#{service_name}-postgres", orm_service_name: "#{service_name}-activerecord"
  c.tracing.instrument :sidekiq, service_name: "#{service_name}-sidekiq", client_service_name: "#{service_name}-sidekiq-client"
  c.tracing.instrument :redis, service_name: "#{service_name}-redis"
  c.tracing.instrument :faraday, service_name: "#{service_name}-faraday"

IIUC when tracing is disabled this may still instrument but do nothing?

Anyway, I'll try to come up with a fix shortly.

@glaucocustodio
Copy link

yeah indeed the block could be nested in the condition..

here is the backtrack of the error, it might help you

track(/app/vendor/bundle/ruby/3.3.0/gems/ddtrace-1.17.0/lib/datadog/appsec/contrib/devise/tracking.rb:34)
track_login_success(/app/vendor/bundle/ruby/3.3.0/gems/ddtrace-1.17.0/lib/datadog/appsec/contrib/devise/tracking.rb:16)
validate(/app/vendor/bundle/ruby/3.3.0/gems/ddtrace-1.17.0/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb:40)
authenticate!(/app/vendor/bundle/ruby/3.3.0/gems/devise-4.8.1/lib/devise/strategies/database_authenticatable.rb:13)

@lloeki
Copy link
Contributor

lloeki commented May 15, 2024

We've merged a fix to master, it'll land in 2.0.

The fix should probably be backported to 1.x-stable as well.

@lloeki lloeki mentioned this issue May 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Involves a bug community Was opened by a community member
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants