opf/openproject · error · Import::JiraClient::SsrfError

Connection blocked: the Jira host resolves to a private IP a

Error message

Connection blocked: the Jira host resolves to a private IP address. If your Jira instance runs on an internal network, allow its IP via the OPENPROJECT_SSRF__PROTECTION__IP__ALLOWLIST environment variable.

What it means

Import::JiraClient#download_attachment streams Jira attachments through OpenProject::SsrfProtection.get (a wrapper over the SsrfFilter gem). The wrapper resolves the hostname first and raises SsrfFilter::PrivateIPAddress when every resolved IP falls in a private/reserved range (loopback, link-local, RFC1918); the client re-raises it as Import::JiraClient::SsrfError with this message. The check exists to prevent server-side request forgery, so it fires even for legitimate internal Jira hosts unless they are explicitly allowlisted.

Source

Thrown at app/services/import/jira_client.rb:269

    # @raise [ApiError] If the server returns a non-success response
    def download_attachment(content_url, filename) # rubocop:disable Metrics/AbcSize
      tempfile = nil
      OpenProject::SsrfProtection.get(content_url, headers: @headers, http_options: HTTP_OPTIONS, max_redirects: 1) do |response|
        case response
        when Net::HTTPSuccess
          tempfile = Tempfile.create(filename, binmode: true)
          response.read_body do |chunk|
            tempfile.write chunk
          end
          yield tempfile
        else
          status = response.code.to_i
          raise ApiError.new(I18n.t("admin.jira.client.api_error", status:), status:, response_body: response.body)
        end
      end
      nil
    rescue SsrfFilter::PrivateIPAddress
      raise SsrfError, I18n.t("admin.jira.client.ssrf_blocked")
    rescue SsrfFilter::Error => e
      raise ConnectionError, I18n.t("admin.jira.client.connection_error", message: e.message)
    rescue OpenSSL::SSL::SSLError => e
      raise ConnectionError, I18n.t("admin.jira.client.ssl_error", message: e.message)
    rescue Timeout::Error => e
      raise ConnectionError, I18n.t("admin.jira.client.connection_timeout", message: e.message)
    ensure
      File.unlink(tempfile) if tempfile
    end

    private

    def get(path, params: {})
      response = get_response(path, params:)
      handle_response(response)
    end

    def get_response(path, params: {})

View on GitHub (pinned to d9742c43f3)

Solutions

  1. Set OPENPROJECT_SSRF__PROTECTION__IP__ALLOWLIST to the Jira host's internal IP (comma-separated list, CIDR ranges supported) in the OpenProject environment and restart the app/container
  2. Check what the host actually resolves to from inside the OpenProject container (e.g. getent hosts jira.corp.local) and allowlist exactly that IP
  3. If the Jira instance should be reachable publicly, fix DNS/split-horizon resolution or point the import configuration at the public hostname instead of allowlisting
  4. Keep the allowlist as narrow as possible (single IP or small CIDR); never widen it to all private ranges

Example fix

# before — Jira attachment host resolves to 10.0.0.15, download raises SsrfError
# docker-compose.yml environment:
#   (no SSRF allowlist set)

# after — allowlist the internal Jira IP and restart
environment:
  OPENPROJECT_SSRF__PROTECTION__IP__ALLOWLIST: "10.0.0.15"
Defensive patterns

Strategy: validation

Validate before calling

# before importing or downloading attachments, pre-flight the host
host = URI.parse(content_url).host
unless OpenProject::SsrfProtection.safe_ip?(host)
  raise ArgumentError,
        "#{host} resolves to a blocked private IP — allowlist it via " \
        "OPENPROJECT_SSRF__PROTECTION__IP__ALLOWLIST before importing"
end
client.download_attachment(content_url, filename) { |tf| attach(tf) }

Prevention

When it happens

Trigger: Calling client.download_attachment(content_url, filename) where the attachment URL returned by the Jira REST API points at a host that resolves to a private IP: http://jira.internal:8080/..., http://10.x.x.x/..., http://localhost/..., or a public-looking name whose DNS (or the one allowed redirect, max_redirects: 1) lands on an internal address.

Common situations: Self-hosted Jira Server/Data Center on the same LAN as OpenProject; Docker/Kubernetes deployments where the Jira service name resolves to a cluster-internal IP; split-horizon DNS that returns an internal IP inside the OpenProject container; an SSO/reverse proxy redirecting attachment downloads to an internal host.

Related errors


AI-assisted analysis of opf/openproject@d9742c43f3 (2026-08-21). Data as JSON: /api/errors/c514347a250bcc2d. Report an issue: GitHub.