{"record":{"id":"c514347a250bcc2d","repo":"opf/openproject","slug":"connection-blocked-the-jira-host-resolves-to-a-pr","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"Import::JiraClient::SsrfError","httpStatus":null,"severity":"error","filePath":"app/services/import/jira_client.rb","lineNumber":269,"sourceCode":"    # @raise [ApiError] If the server returns a non-success response\n    def download_attachment(content_url, filename) # rubocop:disable Metrics/AbcSize\n      tempfile = nil\n      OpenProject::SsrfProtection.get(content_url, headers: @headers, http_options: HTTP_OPTIONS, max_redirects: 1) do |response|\n        case response\n        when Net::HTTPSuccess\n          tempfile = Tempfile.create(filename, binmode: true)\n          response.read_body do |chunk|\n            tempfile.write chunk\n          end\n          yield tempfile\n        else\n          status = response.code.to_i\n          raise ApiError.new(I18n.t(\"admin.jira.client.api_error\", status:), status:, response_body: response.body)\n        end\n      end\n      nil\n    rescue SsrfFilter::PrivateIPAddress\n      raise SsrfError, I18n.t(\"admin.jira.client.ssrf_blocked\")\n    rescue SsrfFilter::Error => e\n      raise ConnectionError, I18n.t(\"admin.jira.client.connection_error\", message: e.message)\n    rescue OpenSSL::SSL::SSLError => e\n      raise ConnectionError, I18n.t(\"admin.jira.client.ssl_error\", message: e.message)\n    rescue Timeout::Error => e\n      raise ConnectionError, I18n.t(\"admin.jira.client.connection_timeout\", message: e.message)\n    ensure\n      File.unlink(tempfile) if tempfile\n    end\n\n    private\n\n    def get(path, params: {})\n      response = get_response(path, params:)\n      handle_response(response)\n    end\n\n    def get_response(path, params: {})","sourceCodeStart":251,"sourceCodeEnd":287,"githubUrl":"https://github.com/opf/openproject/blob/d9742c43f3424c34b63550f8c03f201fe5c3040c/app/services/import/jira_client.rb#L251-L287","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","Check what the host actually resolves to from inside the OpenProject container (e.g. getent hosts jira.corp.local) and allowlist exactly that IP","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","Keep the allowlist as narrow as possible (single IP or small CIDR); never widen it to all private ranges"],"exampleFix":"# before — Jira attachment host resolves to 10.0.0.15, download raises SsrfError\n# docker-compose.yml environment:\n#   (no SSRF allowlist set)\n\n# after — allowlist the internal Jira IP and restart\nenvironment:\n  OPENPROJECT_SSRF__PROTECTION__IP__ALLOWLIST: \"10.0.0.15\"","handlingStrategy":"validation","validationCode":"# before importing or downloading attachments, pre-flight the host\nhost = URI.parse(content_url).host\nunless OpenProject::SsrfProtection.safe_ip?(host)\n  raise ArgumentError,\n        \"#{host} resolves to a blocked private IP — allowlist it via \" \\\n        \"OPENPROJECT_SSRF__PROTECTION__IP__ALLOWLIST before importing\"\nend\nclient.download_attachment(content_url, filename) { |tf| attach(tf) }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Decide before the import whether the Jira host is internal, and set OPENPROJECT_SSRF__PROTECTION__IP__ALLOWLIST in the deployment rather than reacting to the error","Document which IP was allowlisted and why, so the security exception is reviewable","Use safe_ip? in a pre-flight check script to fail fast with a clear message instead of a mid-import failure"],"tags":["ssrf","jira","network","security","import","private-ip"],"backgroundTag":"ssrf-private-ip-blocked","analyzedSha":"d9742c43f3424c34b63550f8c03f201fe5c3040c","analyzedAt":"2026-08-21T14:40:06.829Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}