Setting up external modalities or peers in my orthanc server

Hello

I have set up my Orthanc server and now I need to allow in runtime some external modalities or peers.

So far I am trying to set up locally a sample modality bu it doesn’t work

$ curl -v -X PUT https://localhost:8042/modalities/biomediqa -d '{"AET" : "MICRODICOM", "Host": "my.host.ip", "Port":
 50000}'
* Host localhost:8042 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8042...
* connect to ::1 port 8042 from ::1 port 60328 failed: Connexion refusée
*   Trying 127.0.0.1:8042...
* connect to 127.0.0.1 port 8042 from 127.0.0.1 port 52532 failed: Connexion refusée
* Failed to connect to localhost port 8042 after 0 ms: Could not connect to server
* closing connection #0
curl: (7) Failed to connect to localhost port 8042 after 0 ms: Could not connect to server

I have open the relevant ports

$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
8088/tcp                   ALLOW       Anywhere
8080/tcp                   ALLOW       Anywhere
4242/tcp                   ALLOW       Anywhere
8042/tcp                   ALLOW       Anywhere
8042                       ALLOW       Anywhere
4242                       ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)
8088/tcp (v6)              ALLOW       Anywhere (v6)
8080/tcp (v6)              ALLOW       Anywhere (v6)
4242/tcp (v6)              ALLOW       Anywhere (v6)
8042/tcp (v6)              ALLOW       Anywhere (v6)
8042 (v6)                  ALLOW       Anywhere (v6)
4242 (v6)                  ALLOW       Anywhere (v6)

My test client (MicroDicom) can’t connect

The problem seems to be the ports not properly opened

$ sudo netstat -tulpn | grep LISTEN
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      1637927/docker-prox
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      650/sshd: /usr/sbin
tcp6       0      0 :::443                  :::*                    LISTEN      1637933/docker-prox
tcp6       0      0 :::22                   :::*                    LISTEN      650/sshd: /usr/sbin

However, they should be opened, I don’t know what is happening

Well, looks like the first thing to do is to check the logs in verbose mode to make sure Orthanc is running …

Thanks, I see it is running

And keycloak is asking for authentication, so this stage is clear for now.

In Slicer, a typical CGET call responds like this

    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://pacs.biomediqa.com/orthanc/dicom-web/studies/1.3.6.1.4.1.33868.20210827082511.58195/series?includefield=SeriesNumber&offset=0

The dicom-web is ansering the echo

But it doens’t allow to get any study

Updated conf

services:

  nginx:
    image: orthancteam/orthanc-nginx:25.9.0
    depends_on: [orthanc, orthanc-auth-service]
    restart: unless-stopped
    ports: ["80:80", "443:443"]
    volumes:
      - /etc/letsencrypt/live/pacs.biomediqa.com/fullchain.pem:/etc/nginx/tls/crt.pem
      - /etc/letsencrypt/live/pacs.biomediqa.com/privkey.pem:/etc/nginx/tls/key.pem
    environment:
      ENABLE_ORTHANC: "true"
      ENABLE_KEYCLOAK: "true"
      ENABLE_HTTPS: "true"
      ENABLE_ORTHANC_TOKEN_SERVICE: "true"
      ENABLE_OHIF: "true"
      DOMAIN_NAME: "pacs.biomediqa.com"

  orthanc:
    image: orthancteam/orthanc:25.9.0
    depends_on: [orthanc-db]
    volumes:
      - orthanc-storage:/var/lib/orthanc/db:z
      - orthanc-tls:/var/lib/orthanc/tls:z
      - ./orthanc.jsonc:/etc/orthanc/orthanc.json
    restart: unless-stopped
    environment:
      ORTHANC__NAME: "Orthanc"
      ORTHANC__DATABASE_SERVER_IDENTIFIER: "orthanc2"
      VERBOSE_ENABLED: "true"
      VERBOSE_STARTUP: "true"
      VOLVIEW_PLUGIN_ENABLED: "true"
      ORTHANC__POSTGRESQL__HOST: "orthanc-db"
      ORTHANC__POSTGRESQL__INDEX_CONNECTIONS_COUNT: 20
      ORTHANC__POSTGRESQL__TRANSACTION_MODE: "ReadCommitted"

  orthanc-db:
    image: postgres:16
    restart: unless-stopped
    volumes:
       - orthanc-db:/var/lib/orthanc/data:z
    environment:
      POSTGRES_HOST_AUTH_METHOD: "trust"

  orthanc-auth-service:
    image: orthancteam/orthanc-auth-service:25.9.0
    # always disable this port mapping in production !!!
    # ports: ["8000:8000"]
    depends_on: [keycloak]
    restart: unless-stopped
    environment:
      ENABLE_KEYCLOAK: "true"
      KEYCLOAK_URI: "https://pacs.biomediqa.com/keycloak/realms/orthanc"
      KEYCLOAK_REALM: "orthanc"
      KEYCLOAK_CLIENT_ID: "orthanc"
      KEYCLOAK_ADMIN_URI: "https://pacs.biomediqa.com/keycloak/realms/master"
      PUBLIC_ORTHANC_ROOT: "https://pacs.biomediqa.com/orthanc/"
      PUBLIC_LANDING_ROOT: "https://pacs.biomediqa.com/orthanc/ui/app/token-landing.html"
      PERMISSIONS_FILE_PATH: "/orthanc_auth_service/permissions.json"
      ENABLE_KEYCLOAK_API_KEYS: "true"
      PUBLIC_OHIF_ROOT: "https://pacs.biomediqa.com/ohif/"
    env_file:
      - ./secrets/orthanc-token.secret.env
    secrets:
      - SECRET_KEY
      - KEYCLOAK_CLIENT_SECRET
    volumes:
      - ./permissions.json:/orthanc_auth_service/permissions.json
      - ./anonymous-profile.json:/orthanc_auth_service/anonymous-profile.json

  ohif:
    image: orthancteam/ohif-v3:25.9.0
    volumes:
      - ./ohif-app-config.js:/usr/share/nginx/html/app-config.js
    restart: unless-stopped

  keycloak:
    image: orthancteam/orthanc-keycloak:25.9.0
    depends_on: [keycloak-db]
    restart: unless-stopped
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: "admin"
      KC_DB: "postgres"
      KC_DB_URL: "jdbc:postgresql://keycloak-db:5432/keycloak"
      KC_DB_USERNAME: "keycloak"
      KC_DB_PASSWORD: "keycloak"
      KC_HOSTNAME: "https://pacs.biomediqa.com/keycloak"
    secrets:
      - KC_BOOTSTRAP_ADMIN_PASSWORD

  keycloak-db:
    image: postgres:16
    restart: unless-stopped
    volumes:
      - keycloak-db:/var/lib/keycloak/data:Z
    environment:
      POSTGRES_PASSWORD: "keycloak"
      POSTGRES_USER: "keycloak"
      POSTGRES_DB: "keycloak"

volumes:
  keycloak-db:
    driver_opts:
      type: none
      o: bind
      device: /home/bastion/orthanc/keycloak
  orthanc-storage:
    driver_opts:
      type: none
      o: bind
      device: /home/bastion/orthanc/data
  orthanc-db:
    driver_opts:
      type: none
      o: bind
      device: /home/bastion/orthanc/index
  orthanc-tls:
    driver_opts:
      type: none
      o: bind
      device: /home/bastion/orthanc-setup/DicomTLS

secrets:
  SECRET_KEY:
    file: ./secrets/SECRET_KEY
  KEYCLOAK_CLIENT_SECRET:
    file: ./secrets/KEYCLOAK_CLIENT_SECRET
  KEYCLOAK_ADMIN_PASSWORD:
    file: ./secrets/KC_BOOTSTRAP_ADMIN_PASSWORD

Where permissions.json is

{
  "roles" : {
    "admin-role": {
      "permissions": ["all", "admin-permissions"],
      "authorized_labels": ["*"]
    },
    "doctor-role": {
      "permissions":["view", "download", "share", "send"],
      "authorized_labels": ["*"]
    },
    "upload": {
      "permissions":["upload"],
      "authorized_labels": ["*"]
    },
    "external-role": {
      "authorized-labels": [
        "external"
      ],
      "permissions": [
        "view",
        "download",
      ]
    }
  },
  "available-labels": []
}

And anonymous-profile.json is

{
    "name": "Anonymous",
    "user-id": null,
    "authorized-labels": [
        "*"
    ],
    "permissions": [
        "upload", "view", "send", "download"
    ],
    "groups": [],
    "validity": 60
}

orthanc.json is

{
    "Name": "Orthanc",
    "OrthancExplorer2": {
        "Keycloak": {
            "ClientId": "orthanc",
            "Enable": true,
            "Realm": "orthanc",
            "Url": "https://pacs.biomediqa.com/keycloak"
        },
        "Tokens": {
            "InstantLinksValidity": 3600,
            "LandingOptions": [
                {
                    "Type": "open-viewer-button"
                },
                {
                    "Type": "download-study"
                },
                {
                    "Icon": "bi bi-filetype-jpg",
                    "Id": "get-jpeg-archive",
                    "Title": "Download study as jpeg Archive",
                    "Type": "custom",
                    "Url": "../../studies/{UUID}/download-as-jpeg-archive?preview-level=instance&filename={StudyInstanceUID}.zip"
                }
            ],
            "ShareType": "stone-viewer-publication"
        },
        "UiOptions": {
            "DefaultShareDuration": 90,
            "EnableApiViewMenu": true,
            "EnableDicomModalities": true,
            "EnableOpenInOhifViewer3": true,
            "EnableShares": true,
            "OhifViewer3PublicRoot": "https://pacs.biomediqa.com/ohif/",
            "ShareDurations": [
                0,
                7,
                15,
                30,
                90,
                365
            ],
            "ShowSamePatientStudiesFilter": [
                "PatientBirthDate",
                "PatientID"
            ],
            "StudyListContentIfNoSearch": "empty",
            "StudyListSearchMode": "search-button"
        }
    },
	
    "AuthenticationEnabled": false,
    "Authorization": {
        "CheckedLevel": "studies",
        "Permissions": [
            [
                "post",
                "^/auth/tokens/decode$",
                ""
            ],
            [
                "post",
                "^/tools/find$",
                "all|view"
            ],
            [
                "get",
                "^/(patients|studies|series|instances)/([a-f0-9-]+)$",
                "all|view"
            ],
            [
                "get",
                "^/(patients|studies|series|instances)/([a-f0-9-]+)/(studies|study|series|instances)$",
                "all|view"
            ],
            [
                "get",
                "^/instances/([a-f0-9-]+)/(tags|header)$",
                "all|view"
            ],
            [
                "get",
                "^/statistics$",
                "all|view"
            ],
            [
                "put",
                "^/auth/tokens/(viewer-instant-link|meddream-instant-link)$",
                "all|view"
            ],
            [
                "put",
                "^/auth/tokens/(download-instant-link)$",
                "all|download"
            ],
            [
                "put",
                "^/auth/tokens/(stone-viewer-publication|meddream-viewer-publication|osimis-viewer-publication)$",
                "all|share"
            ],
            [
                "post",
                "^/instances$",
                "all|upload"
            ],
            [
                "get",
                "^/jobs/([a-f0-9-]+)$",
                "all|send|modify|anonymize|q-r-remote-modalities"
            ],
            [
                "post",
                "^/(peers|modalities)/(.*)/store$",
                "all|send"
            ],
            [
                "get",
                "^/(peers|modalities)$",
                "all|send|q-r-remote-modalities"
            ],
            [
                "post",
                "^/modalities/(.*)/echo$",
                "all|send|q-r-remote-modalities"
            ],
            [
                "post",
                "^/modalities/(.*)/query$",
                "all|q-r-remote-modalities"
            ],
            [
                "get",
                "^/queries/([a-f0-9-]+)/answers$",
                "all|q-r-remote-modalities"
            ],
            [
                "post",
                "^/modalities/(.*)/move$",
                "all|q-r-remote-modalities"
            ],
            [
                "get",
                "^/DICOM_WEB_ROOT/servers$",
                "all|send|q-r-remote-modalities"
            ],
            [
                "get",
                "^/DICOM_WEB_ROOT/(servers)/(.*)/stow$",
                "all|send"
            ],
            [
                "post",
                "^/(patients|studies|series|instances)/([a-f0-9-]+)/modify(.*)$",
                "all|modify"
            ],
            [
                "post",
                "^/(patients|studies|series|instances)/([a-f0-9-]+)/anonymize(.*)$",
                "all|anonymize"
            ],
            [
                "delete",
                "^/(patients|studies|series|instances)/([a-f0-9-]+)$",
                "all|delete"
            ]
        ],
        "StandardConfigurations": [
            "stone-webviewer",
            "orthanc-explorer-2",
            "ohif",
            "volview"
        ],
        "TokenHttpHeaders": [
            "Authorization", "api-key"
        ],
        "UncheckedResources": [
            "/app/images/unsupported.png",
			"/system",
            "/ui/api/pre-login-configuration"
        ],
		"UncheckedFolders": [
            "/ui/app/"
        ],
		// auth checks are performed at study level
        "UncheckedLevels": [
            "patients",
            "series",
            "instances"
        ],
        "WebServicePassword": "hidden",
        "WebServiceRootUrl": "http://orthanc-auth-service:8000/",
        "WebServiceUsername": "biomediqa"
    },
    "DicomModalities": {
        "MICRODICOM": [
            "MICRODICOM",
            "x.x.x.x",
            50000
        ],
        "Slicer": [
            "CTKSTORE",
            "x.x.x.x",
            11112
        ]
    },
    "DicomTlsCertificate": "/var/lib/orthanc/tls/orthanc.crt",
    "DicomTlsEnabled": true,
	"DicomAlwaysAllowEcho" : true,
    "DicomTlsPrivateKey": "/var/lib/orthanc/tls/orthanc.key",
    "DicomTlsRemoteCertificateRequired": false,
    "DicomWeb": {
        "Enable": true,
        "PublicRoot": "/orthanc/dicom-web/"
    },
    "OverwriteInstances": true,
    "StoneWebViewer": {
        "ShowInfoPanelAtStartup": "Never"
    }
}

From remote console I get

>curl.exe -v https://pacs.biomediqa.com/modalities?expand -u biomediqa
Enter host password for user 'biomediqa':
* Host pacs.biomediqa.com:443 was resolved.
* IPv6: (none)
* IPv4: 37.59.167.172
*   Trying 37.59.167.172:443...
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* Connected to pacs.biomediqa.com (37.59.167.172) port 443
* using HTTP/1.x
* Server auth using Basic with user 'biomediqa'
> GET /modalities?expand HTTP/1.1
> Host: pacs.biomediqa.com
> Authorization: Basic YmlvbWVkaXFhOk9ydGhAbmMwMQ==
> User-Agent: curl/8.14.1
> Accept: */*
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* Request completely sent off
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.27.4
< Date: Thu, 23 Oct 2025 08:04:03 GMT
< Content-Type: text/html
< Content-Length: 145
< Connection: keep-alive
< Location: /orthanc/ui/app/
<
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.27.4</center>
</body>
</html>
* Connection #0 to host pacs.biomediqa.com left intact

And for the dicomWeb

> curl.exe -v https://pacs.biomediqa.com/orthanc/dicom-web/modalities?expand -u biomediqa
Enter host password for user 'biomediqa':
* Host pacs.biomediqa.com:443 was resolved.
* IPv6: (none)
* IPv4: 37.59.167.172
*   Trying 37.59.167.172:443...
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* Connected to pacs.biomediqa.com (37.59.167.172) port 443
* using HTTP/1.x
* Server auth using Basic with user 'biomediqa'
> GET /orthanc/dicom-web/modalities?expand HTTP/1.1
> Host: pacs.biomediqa.com
> Authorization: Basic YmlvbWVkaXFhOk9ydGhAbmMwMQ==
> User-Agent: curl/8.14.1
> Accept: */*
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* Request completely sent off
< HTTP/1.1 403 Forbidden
< Server: nginx/1.27.4
< Date: Thu, 23 Oct 2025 08:07:14 GMT
< Content-Length: 0
< Connection: keep-alive
< X-Content-Type-Options: nosniff
<
* Connection #0 to host pacs.biomediqa.com left intact

my firewall is now

$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)

The only way Slicer works so far is to remove the authentication

"UncheckedFolders": [
            "/ui/app/",
            "/dicom-web/studies" // This line allow Slicer to connect
        ],

However, I feel this configuration is totally unsecure.

Indeed, you should remove that line !
Since you are using Keycloak and the auth plugin, the dicom-web route is protected and Slicer must provide HTTP authentication headers to get access to Orthanc.

Usage of Api Keys is explained here.

Now, let’s hope that Slicer provides you with the ability to add a custom HTTP header …

I did all that and

> curl.exe -v -H "api-key: api-key-for-administration" https://pacs.biomediqa.com/orthanc/dicom-web/modalities?expand
* Host pacs.biomediqa.com:443 was resolved.
* IPv6: (none)
* IPv4: 37.59.167.172
*   Trying 37.59.167.172:443...
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* Connected to pacs.biomediqa.com (37.59.167.172) port 443
* using HTTP/1.x
> GET /orthanc/dicom-web/modalities?expand HTTP/1.1
> Host: pacs.biomediqa.com
> User-Agent: curl/8.14.1
> Accept: */*
> api-key: api-key-for-administration
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* Request completely sent off
< HTTP/1.1 403 Forbidden
< Server: nginx/1.27.4
< Date: Thu, 23 Oct 2025 09:11:03 GMT
< Content-Length: 0
< Connection: keep-alive
< X-Content-Type-Options: nosniff
<
* Connection #0 to host pacs.biomediqa.com left intact

Support for http header access token · Issue #25 · lassoan/SlicerDICOMwebBrowser

OK, it works

> curl.exe -v -H "api-key: api-key-for-applications" https://pacs.biomediqa.com/orthanc/dicom-web/studies
* Host pacs.biomediqa.com:443 was resolved.
* IPv6: (none)
* IPv4: 37.59.167.172
*   Trying 37.59.167.172:443...
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* Connected to pacs.biomediqa.com (37.59.167.172) port 443
* using HTTP/1.x
> GET /orthanc/dicom-web/studies HTTP/1.1
> Host: pacs.biomediqa.com
> User-Agent: curl/8.14.1
> Accept: */*
> api-key: api-key-for-applications
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.27.4
< Date: Thu, 23 Oct 2025 09:34:13 GMT
< Content-Type: application/dicom+json
< Content-Length: 35089
< Connection: keep-alive
< X-Content-Type-Options: nosniff
<
[{
        "00080005" :
        {
                "Value" :
                [
                        "ISO_IR 192"
                ],
                "vr" : "CS"
        },
        "00080020" :
        {
                "Value" :
                [
                        "20241105"

EDIT: False alarm, it only get the study list for the echo, nothing else

schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* Connected to pacs.biomediqa.com (37.59.167.172) port 443
* using HTTP/1.x
> GET /orthanc/dicom-web/studies/1.3.6.1.4.1.33868.20210827082511.58195/series/1.2.246.352.61.2.4662168571522978563.16767127903955670429/instances?offset=0 HTTP/1.1
> Host: pacs.biomediqa.com
> User-Agent: curl/8.14.1
> Accept: */*
> api-key: api-key-for-applications
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
< HTTP/1.1 403 Forbidden
< Server: nginx/1.27.4
< Date: Thu, 23 Oct 2025 09:50:16 GMT
< Content-Length: 0
< Connection: keep-alive
< X-Content-Type-Options: nosniff
<
* Connection #0 to host pacs.biomediqa.com left intact

Right now I am facing this

orthanc-auth-service-1  | 2025-10-27T10:58:03.782941000Z INFO:root:validating token: {"dicom_uid":"1.2.826.0.1.3680043.2.908.14.0.0.2000915.10","orthanc_id":"1af1ba41-b9fe8a55-90af0b2b-161ec9c5-f8e4f97f","token_key":"api-key","token_value":"external-api-key-for-applications","server_id":null,"level":"study","method":"get","uri":null}
orthanc-auth-service-1  | 2025-10-27T10:58:03.783169000Z WARNING:root:Token Validation: failed to decode token
orthanc-auth-service-1  | 2025-10-27T10:58:03.783910000Z INFO:root:validate token: {"granted":false,"validity":60}

I have double checked that the api key is indeed what I have saved in keycloak

Current status

What I can do right now

  • Keycloak: working
  • orthanc through web: working, I can see the studies and upload/download them using the web interface of orthanc
  • orthanc though scripts: working, I can use python scripts to upload/download studies
  • orthanc in fiji: working

what it’s not working:

  • peers: access refused 403
  • modalities (slicer, microdicom): access refused 403

I use the exact same code in slicer as in my python script, however, slicer uses dicomweb library (from dicomweb_client.api import DICOMwebClient) instead of direct http calls (h = httplib2.Http()). This is the only meaninful difference I can see.

I can’t connect other peers as I get error 403 access denied

I have tried access through

  • api-key mechanism: failed
  • basic auth without keycloak: failed
  • auth through keycloak: failed