diff --git a/docling_serve/__main__.py b/docling_serve/__main__.py index a1d140c..003c8db 100644 --- a/docling_serve/__main__.py +++ b/docling_serve/__main__.py @@ -86,6 +86,11 @@ def _run( uvicorn_settings.workers is not None and uvicorn_settings.workers > 1 ) or uvicorn_settings.reload + run_ssl = ( + uvicorn_settings.ssl_certfile is not None + and uvicorn_settings.ssl_keyfile is not None + ) + if run_subprocess and docling_serve_settings.artifacts_path != artifacts_path: err_console.print( "\n[yellow]:warning: The server will run with reload or multiple workers. \n" @@ -105,7 +110,8 @@ def _run( docling_serve_settings.enable_ui = enable_ui # Print documentation - url = f"http://{uvicorn_settings.host}:{uvicorn_settings.port}" + protocol = "https" if run_ssl else "http" + url = f"{protocol}://{uvicorn_settings.host}:{uvicorn_settings.port}" url_docs = f"{url}/docs" url_ui = f"{url}/ui" @@ -136,6 +142,9 @@ def _run( root_path=uvicorn_settings.root_path, proxy_headers=uvicorn_settings.proxy_headers, timeout_keep_alive=uvicorn_settings.timeout_keep_alive, + ssl_certfile=uvicorn_settings.ssl_certfile, + ssl_keyfile=uvicorn_settings.ssl_keyfile, + ssl_keyfile_password=uvicorn_settings.ssl_keyfile_password, ) @@ -190,6 +199,15 @@ def dev( timeout_keep_alive: Annotated[ int, typer.Option(help="Timeout for the server response.") ] = uvicorn_settings.timeout_keep_alive, + ssl_certfile: Annotated[ + Optional[Path], typer.Option(help="SSL certificate file") + ] = uvicorn_settings.ssl_certfile, + ssl_keyfile: Annotated[ + Optional[Path], typer.Option(help="SSL key file") + ] = uvicorn_settings.ssl_keyfile, + ssl_keyfile_password: Annotated[ + Optional[str], typer.Option(help="SSL keyfile password") + ] = uvicorn_settings.ssl_keyfile_password, # docling options artifacts_path: Annotated[ Optional[Path], @@ -218,6 +236,9 @@ def dev( uvicorn_settings.root_path = root_path uvicorn_settings.proxy_headers = proxy_headers uvicorn_settings.timeout_keep_alive = timeout_keep_alive + uvicorn_settings.ssl_certfile = ssl_certfile + uvicorn_settings.ssl_keyfile = ssl_keyfile + uvicorn_settings.ssl_keyfile_password = ssl_keyfile_password _run( command="dev", @@ -285,6 +306,15 @@ def run( timeout_keep_alive: Annotated[ int, typer.Option(help="Timeout for the server response.") ] = uvicorn_settings.timeout_keep_alive, + ssl_certfile: Annotated[ + Optional[Path], typer.Option(help="SSL certificate file") + ] = uvicorn_settings.ssl_certfile, + ssl_keyfile: Annotated[ + Optional[Path], typer.Option(help="SSL key file") + ] = uvicorn_settings.ssl_keyfile, + ssl_keyfile_password: Annotated[ + Optional[str], typer.Option(help="SSL keyfile password") + ] = uvicorn_settings.ssl_keyfile_password, # docling options artifacts_path: Annotated[ Optional[Path], @@ -316,6 +346,9 @@ def run( uvicorn_settings.root_path = root_path uvicorn_settings.proxy_headers = proxy_headers uvicorn_settings.timeout_keep_alive = timeout_keep_alive + uvicorn_settings.ssl_certfile = ssl_certfile + uvicorn_settings.ssl_keyfile = ssl_keyfile + uvicorn_settings.ssl_keyfile_password = ssl_keyfile_password _run( command="run", diff --git a/docling_serve/settings.py b/docling_serve/settings.py index 9a48a04..36cf51b 100644 --- a/docling_serve/settings.py +++ b/docling_serve/settings.py @@ -17,6 +17,9 @@ class UvicornSettings(BaseSettings): root_path: str = "" proxy_headers: bool = True timeout_keep_alive: int = 60 + ssl_certfile: Optional[Path] = None + ssl_keyfile: Optional[Path] = None + ssl_keyfile_password: Optional[str] = None workers: Union[int, None] = None diff --git a/docs/configuration.md b/docs/configuration.md index 769fd3d..06184a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -25,6 +25,9 @@ The following table shows the options which are propagated directly to the | `--root-path` | `UVICORN_ROOT_PATH` | `""` | The root path is used to tell your app that it is being served to the outside world with some | | `--proxy-headers` | `UVICORN_PROXY_HEADERS` | `true` | Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info. | | `--timeout-keep-alive` | `UVICORN_TIMEOUT_KEEP_ALIVE` | `60` | Timeout for the server response. | +| `--ssl-certfile` | `UVICORN_SSL_CERTFILE` | | SSL certificate file. | +| `--ssl-keyfile` | `UVICORN_SSL_KEYFILE` | | SSL key file. | +| `--ssl-keyfile-password` | `UVICORN_SSL_KEYFILE_PASSWORD` | | SSL keyfile password. | ## Docling Serve configuration diff --git a/docs/deploy-examples/docling-serve-oauth.yaml b/docs/deploy-examples/docling-serve-oauth.yaml new file mode 100644 index 0000000..b11fb0a --- /dev/null +++ b/docs/deploy-examples/docling-serve-oauth.yaml @@ -0,0 +1,209 @@ +# This example deployment configures Docling Serve with a OAuth-Proxy sidecar and TLS termination +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: docling-serve + labels: + app: docling-serve + annotations: + serviceaccounts.openshift.io/oauth-redirectreference.primary: '{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"docling-serve"}}' +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: docling-serve-oauth + labels: + app: docling-serve + component: docling-serve-api +rules: + - verbs: + - create + apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + - verbs: + - create + apiGroups: + - authentication.k8s.io + resources: + - tokenreviews +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: docling-serve-oauth + labels: + app: docling-serve + component: docling-serve-api +subjects: + - kind: ServiceAccount + name: docling-serve +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: docling-serve-oauth +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: docling-serve + labels: + app: docling-serve + component: docling-serve-api +spec: + to: + kind: Service + name: docling-serve + port: + targetPort: oauth + tls: + termination: Reencrypt +--- +apiVersion: v1 +kind: Service +metadata: + name: docling-serve + labels: + app: docling-serve + component: docling-serve-api + annotations: + service.alpha.openshift.io/serving-cert-secret-name: docling-serve-tls +spec: + ports: + - name: oauth + port: 8443 + targetPort: oauth + - name: http + port: 5001 + targetPort: http + selector: + app: docling-serve + component: docling-serve-api +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: docling-serve + labels: + app: docling-serve + component: docling-serve-api +spec: + replicas: 1 + selector: + matchLabels: + app: docling-serve + component: docling-serve-api + template: + metadata: + labels: + app: docling-serve + component: docling-serve-api + spec: + restartPolicy: Always + serviceAccountName: docling-serve + containers: + - name: api + resources: + limits: + cpu: 500m + memory: 2Gi + requests: + cpu: 250m + memory: 1Gi + readinessProbe: + httpGet: + path: /health + port: http + scheme: HTTPS + initialDelaySeconds: 10 + timeoutSeconds: 2 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /health + port: http + scheme: HTTPS + initialDelaySeconds: 3 + timeoutSeconds: 2 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + env: + - name: DOCLING_SERVE_ENABLE_UI + value: 'true' + - name: UVICORN_SSL_CERTFILE + value: '/etc/tls/private/tls.crt' + - name: UVICORN_SSL_KEYFILE + value: '/etc/tls/private/tls.key' + ports: + - name: http + containerPort: 5001 + protocol: TCP + volumeMounts: + - name: proxy-tls + mountPath: /etc/tls/private + imagePullPolicy: Always + image: 'ghcr.io/docling-project/docling-serve:dev-ssl' + - name: oauth-proxy + resources: + limits: + cpu: 100m + memory: 256Mi + requests: + cpu: 100m + memory: 256Mi + readinessProbe: + httpGet: + path: /oauth/healthz + port: oauth + scheme: HTTPS + initialDelaySeconds: 5 + timeoutSeconds: 1 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /oauth/healthz + port: oauth + scheme: HTTPS + initialDelaySeconds: 30 + timeoutSeconds: 1 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + ports: + - name: oauth + containerPort: 8443 + protocol: TCP + imagePullPolicy: IfNotPresent + volumeMounts: + - name: proxy-tls + mountPath: /etc/tls/private + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: 'registry.redhat.io/openshift4/ose-oauth-proxy:v4.13' + args: + - '--https-address=:8443' + - '--provider=openshift' + - '--openshift-service-account=docling-serve' + - '--upstream=https://docling-serve.$(NAMESPACE).svc.cluster.local:5001' + - '--upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt' + - '--tls-cert=/etc/tls/private/tls.crt' + - '--tls-key=/etc/tls/private/tls.key' + - '--cookie-secret=SECRET' + - '--openshift-delegate-urls={"/": {"group":"route.openshift.io","resource":"routes","verb":"get","name":"docling-serve","namespace":"$(NAMESPACE)"}}' + - '--openshift-sar={"namespace":"$(NAMESPACE)","resource":"routes","resourceName":"docling-serve","verb":"get","resourceAPIGroup":"route.openshift.io"}' + - '--skip-auth-regex=''(^/health|^/docs)''' + volumes: + - name: proxy-tls + secret: + secretName: docling-serve-tls + defaultMode: 420 diff --git a/docs/deployment.md b/docs/deployment.md index 98a8c4e..168b5e9 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,12 +1,40 @@ # Deployment -## Kubernetes and OpenShift +## OpenShift -### Knative +### Secure deployment with `oauth-proxy` -The following manifest will launch Docling Serve using Knative to expose the application -with an external ingress endpoint. +Manifest example: [docling-serve-oauth.yaml](./deploy-examples/docling-serve-oauth.yaml) -```yaml -# TODO +This deployment has the following features: + +- TLS encryption between all components (using the cluster-internal CA authority). +- Authentication via a secure `oauth-proxy` sidecar. +- Expose the service using a secure OpenShift `Route` + +Install the app with: + +```sh +kubectl apply -f docs/deploy-examples/docling-serve-oauth.yaml +``` + +For using the API: + +```sh +# Retrieve the endpoint +DOCLING_NAME=docling-serve +DOCLING_ROUTE="https://$(oc get routes ${DOCLING_NAME} --template={{.spec.host}})" + +# Retrieve the authentication token +OCP_AUTH_TOKEN=$(oc whoami --show-token) + +# Make a test query +curl -X 'POST' \ + "${DOCLING_ROUTE}/v1alpha/convert/source/async" \ + -H "Authorization: Bearer ${OCP_AUTH_TOKEN}" \ + -H "accept: application/json" \ + -H "Content-Type: application/json" \ + -d '{ + "http_sources": [{"url": "https://arxiv.org/pdf/2501.17887"}] + }' ```