Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
==== 8.0.0~dev ===
* Security: the <maxconnected> limit (maximum number of simultaneous
connections) is now enforced again. It throttles the accept loop instead
of being silently ignored.
* Security: error responses no longer include the raw OCaml exception in
their body. Clients now receive the generic HTTP status reason phrase;
the exception details remain in the logs.
* Security: the command pipe is now created with mode 0o600 instead of
0o660, so that local users in the server's group can no longer send it
control commands (shutdown, reload, ...).
* Security (TLS): the HTTPS server now requires TLS 1.2 at minimum (TLS 1.0
and 1.1 are deprecated) and prefers the server's cipher order. The
<ciphers>, <dhfile> and <curve> entries of <ssl>, previously parsed but
ignored, are now applied to the TLS context.
* One-command serve mode: "ocsigenserver ./public" (or
"ocsigenserver --serve ./public --port 8000 --directory-listing") now
serves a directory of static files without any configuration file or
Expand Down
90 changes: 64 additions & 26 deletions doc/config.mld
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,6 @@ Here is a simple example:
<datadir>/var/lib/ocsigenserver</datadir>
<commandpipe>/var/run/ocsigenserver/command</commandpipe>

<user>www-data</user>
<group>www-data</group>

<charset>utf-8</charset>

<extension findlib-package="ocsigenserver.ext.staticmod"/>
Expand Down Expand Up @@ -147,9 +144,6 @@ exist. To create it run [psql] and execute [CREATE TABLE ocsipersist;].
<datadir>/home/toto/var/lib/ocsigenserver</datadir>
<commandpipe>/home/toto/var/run/ocsigenserver_command</commandpipe>

<user>toto</user>
<group>toto</group>

<charset>utf-8</charset>

<extension findlib-package="ocsigenserver.ext.staticmod"/>
Expand Down Expand Up @@ -233,10 +227,11 @@ If you want to use HTTPS, you need to specify a certificate to use, and a privat
</ssl>
v}

Use the tools provided by openssl to create a 1024-bit private key to use when creating your CA.:
Use the tools provided by openssl to create a private key. Use at least a
2048-bit RSA key (4096 bits is common today), or an elliptic-curve key:

{v
openssl genrsa -des3 -out privkey.pem 1024
openssl genrsa -des3 -out privkey.pem 4096
v}

To create a master certificate based on this key, to use when signing other certificates:
Expand All @@ -251,6 +246,43 @@ If you don't want to be asked for a password at start-up, you can uncrypt the pr
openssl rsa -in privkey.pem -out privkey-unsec.pem
v}

The TLS context requires {b TLS 1.2 at minimum} (TLS 1.0 and 1.1 are
deprecated, RFC 8996) and prefers the server's cipher order. You can further
tune the context inside [<ssl>]:

{v
<ssl>
<certificate>path_to/cert.pem</certificate>
<privatekey>path_to/privkey.pem</privatekey>
<ciphers>ECDHE+AESGCM:ECDHE+CHACHA20</ciphers>
<dhfile>path_to/dhparams.pem</dhfile>
<curve>prime256v1</curve>
</ssl>
v}

[<ciphers>] is an OpenSSL cipher string, [<dhfile>] a file of
Diffie-Hellman parameters, and [<curve>] a named elliptic curve.

If you serve both HTTP and HTTPS and want to force HTTPS, redirect plain HTTP
requests to HTTPS using the {{!page-redirectmod}Redirectmod} extension guarded
by the [<ssl/>] condition of {{!page-accesscontrol}Accesscontrol} (both
extensions must be loaded):

{v
<host hostfilter="www.example.org" defaulthttpport="80" defaulthttpsport="443">
<if>
<not><ssl/></not>
<then>
<redirect suburl="(.*)" dest="https://www.example.org/\1"/>
</then>
</if>
</host>
v}

Note: HTTP Strict Transport Security (HSTS) is not enabled automatically.
Once you serve only over HTTPS, add a [Strict-Transport-Security] response
header (for instance with the {{!page-outputfilter}Outputfilter} extension).


{3 [<logdir>] : log files directory}

Expand Down Expand Up @@ -295,16 +327,20 @@ v}
{3 [<user>] and [<group>] : user who runs the server}


If the server is launched by root, it will change itself the user and group for the process, for security reasons. Create a group and a user for Ocsigenserver or use an existing user of your system (e.g. [www-data]).
{b Deprecated and ignored.} Ocsigenserver no longer drops privileges: these
tags are accepted for backward compatibility but have {b no effect}, and the
server logs a warning if they are present.

Example :
{b Do not launch the server as root.} Run it directly as a dedicated
unprivileged user (e.g. [www-data]). To serve a privileged port (80 or 443)
as a non-root user, use one of:
- a reverse proxy (nginx, HAProxy, Caddy, ...) terminating ports 80/443 and
forwarding to Ocsigenserver on a high port (recommended, see the [<timeout>]
note above);
- the Linux capability [cap_net_bind_service] on the executable
([setcap 'cap_net_bind_service=+ep' ...]);
- a firewall/port redirection from 80/443 to a high port.



{v
<user>www-data</user>
<group>www-data</group>
v}
{3 [<charset>] : default charset for pages}


Expand Down Expand Up @@ -396,22 +432,24 @@ v}
{3 [<timeout>] : Timeout for connections}


Written in seconds. If the client does not say anything during that amount of time, the connection will be closed. Example:


Written in seconds. Example:

{v
<timeout>20</timeout>
v}
{3 [<keepalivetimeout>] : Timeout for Keep-Alive}


Amount of time (in seconds) the server will wait for subsequent requests on a persistent connection. Example:
{b Limitation.} This option (and its aliases [<clienttimeout>] and
[<servertimeout>]) is currently parsed but {b not enforced}: the cohttp-based
connection handling does not yet apply a per-connection idle/read timeout. As
a consequence Ocsigenserver does {b not} protect against slow-client
("Slowloris") attacks on its own, where a client opens many connections and
sends the request very slowly to exhaust resources.


{v
<keepalivetimeout>10</keepalivetimeout>
v}
{b Recommendation.} For an Internet-facing deployment, place a reverse proxy
(nginx, HAProxy, Caddy, ...) in front of Ocsigenserver. The proxy enforces
read/idle timeouts, buffers slow clients, terminates TLS and absorbs malformed
requests. The [<maxconnected>] limit above does cap the number of
simultaneous connections, which mitigates connection exhaustion.

{3 [<shutdowntimeout>] : Timeout for graceful shutdown {e From version 1.3}}

Expand Down
45 changes: 34 additions & 11 deletions src/server/Ocsigen/ocsigen_cohttp.ml
Original file line number Diff line number Diff line change
Expand Up @@ -77,32 +77,38 @@ let handler ~ssl ~address ~port ~connector (flow, conn) request body =
fmt
("Got exception while handling request." ^^ "@\n%s")
(Printexc.to_string exn));
let headers, ret_code =
let headers, ret_code, explicit_msg =
match exn with
| Ocsigen_http_error (cookies_to_set, code) ->
let headers =
Cookie.serialize cookies_to_set (Cohttp.Header.init ())
in
Some headers, code
Some headers, code, None
| Ocsigen_base.Ocsigen_stream.Interrupted
Ocsigen_base.Ocsigen_stream.Already_read ->
None, `Internal_server_error
| Unix.Unix_error (Unix.EACCES, _, _) -> None, `Forbidden
| Ext_http_error (code, _, headers) -> headers, code
| Ocsigen_base.Lib.Ocsigen_Bad_Request -> None, `Bad_request
None, `Internal_server_error, None
| Unix.Unix_error (Unix.EACCES, _, _) -> None, `Forbidden, None
| Ext_http_error (code, msg, headers) -> headers, code, msg
| Ocsigen_base.Lib.Ocsigen_Bad_Request -> None, `Bad_request, None
| Ocsigen_base.Lib.Ocsigen_Request_too_long ->
None, `Request_entity_too_large
None, `Request_entity_too_large, None
| exn ->
Logs.err ~src:section (fun fmt ->
fmt
("Error while handling request." ^^ "@\n%s")
(Printexc.to_string exn));
None, `Internal_server_error
None, `Internal_server_error, None
in
let body =
match ret_code with
| `Not_found -> "Not Found"
| _ -> Printexc.to_string exn
(* Never leak the OCaml exception (constructor names, file paths, ...)
to the client: the exception detail stays in the logs (above). The
client only gets the generic status reason phrase, or the explicit
message an extension chose to expose through [Ext_http_error]. *)
match explicit_msg with
| Some msg -> msg
| None ->
Cohttp.Code.reason_phrase_of_code
(Cohttp.Code.code_of_status (ret_code :> Cohttp.Code.status_code))
in
Response.respond_error ?headers
~status:(ret_code :> Cohttp.Code.status_code)
Expand Down Expand Up @@ -169,7 +175,24 @@ let shutdown timeout =
in
ignore (Lwt.pick [process (); stop] >>= fun () -> exit 0 : unit Lwt.t)

(* Harden the TLS server context used by Conduit/OpenSSL. Conduit already
disables SSLv2/SSLv3 on its default server context; we additionally require
TLS 1.2 at minimum (TLS 1.0 and 1.1 are deprecated, RFC 8996) and let the
server choose the cipher. The <ciphers>, <dhfile> and <curve> entries of the
<ssl> configuration, until now parsed but never applied, are honoured. *)
let harden_tls_context () =
let ctx = Conduit_lwt_unix_ssl.Server.default_ctx in
Ssl.set_min_protocol_version ctx Ssl.TLSv1_2;
Ssl.honor_cipher_order ctx;
match Config.get_ssl_info () with
| None -> ()
| Some {Config.ssl_ciphers; ssl_dhfile; ssl_curve; _} ->
Option.iter (Ssl.set_cipher_list ctx) ssl_ciphers;
Option.iter (Ssl.init_dh_from_file ctx) ssl_dhfile;
Option.iter (Ssl.init_ec_from_named_curve ctx) ssl_curve

let service ?ssl ~address ~port ~connector () =
(match ssl with Some _ -> harden_tls_context () | None -> ());
let tls_own_key =
match ssl with
| Some (crt, key, Some password) ->
Expand Down
20 changes: 16 additions & 4 deletions src/server/Ocsigen/server.ml
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,14 @@ let main config =
true
with Unix.Unix_error _ -> (
try
let umask = Unix.umask 0 in
Unix.mkfifo commandpipe 0o660;
ignore (Unix.umask umask : int);
(* The command pipe is a control channel (shutdown, reload,
clearcache, ...). Now that <user>/<group> no longer drop
privileges, the server runs as the launching user and owns the
pipe, so only that user should be able to send commands: hence
0o600. (Group administration via the pipe is no longer
supported.) A 0o600 mode has no group/other bits, so it is
unaffected by the umask and there is no need to force it to 0. *)
Unix.mkfifo commandpipe 0o600;
Logs.warn ~src:section (fun fmt -> fmt "Command pipe created");
true
with e ->
Expand Down Expand Up @@ -283,7 +288,7 @@ let main config =
(if with_commandpipe
then
let pipe =
Unix.(openfile commandpipe [O_RDWR; O_NONBLOCK; O_APPEND]) 0o660
Unix.(openfile commandpipe [O_RDWR; O_NONBLOCK; O_APPEND]) 0o600
|> Lwt_unix.of_unix_file_descr
|> Lwt_io.(of_fd ~mode:input)
in
Expand Down Expand Up @@ -315,6 +320,13 @@ let main config =
>>= f
in
ignore (f () : 'a Lwt.t));
(* Enforce the maximum number of simultaneous connections: conduit
throttles its accept loop (across all listening sockets, as file
descriptors are a global resource) while the number of active
connections exceeds this cap. Without this call the configured limit
would have no effect. *)
Conduit_lwt_server.set_max_active
(Config.get_max_number_of_connections ());
Lwt.join
(List.map
(fun (address, port) ->
Expand Down
3 changes: 3 additions & 0 deletions test/security-tls.t/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(executable
(name test)
(libraries ocsigenserver))
1 change: 1 addition & 0 deletions test/security-tls.t/dune-project
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(lang dune 3.18)
23 changes: 23 additions & 0 deletions test/security-tls.t/run.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
F5: the HTTPS server must require TLS 1.2 at minimum (TLS 1.0/1.1 deprecated).
Self-contained POSIX sh.

$ mkdir -p log data
$ openssl req -x509 -newkey rsa:2048 -nodes -keyout privkey.pem \
> -out cert.pem -days 2 -subj /CN=localhost >/dev/null 2>&1
$ dune build ./test.exe 2>&1
$ dune exec -- ./test.exe >server.log 2>&1 &
$ trap 'echo shutdown > local.cmd 2>/dev/null; wait' EXIT
$ i=0; while [ ! -e local.cmd ]; do
> i=$((i+1)); [ $i -gt 200 ] && break; sleep 0.05; done
$ i=0; while ! curl -sk --tls-max 1.2 https://127.0.0.1:8453/ >/dev/null 2>&1; do
> i=$((i+1)); [ $i -gt 200 ] && break; sleep 0.05; done

A TLS 1.2 client succeeds:

$ curl -sk --tlsv1.2 --tls-max 1.2 -o /dev/null -w '%{http_code}\n' https://127.0.0.1:8453/
200

A TLS 1.1 client is refused (curl returns a non-zero exit code):

$ curl -sk --tls-max 1.1 https://127.0.0.1:8453/ >/dev/null 2>&1 && echo accepted || echo rejected
rejected
25 changes: 25 additions & 0 deletions test/security-tls.t/test.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
(* F5: the HTTPS server must require TLS 1.2 at minimum. This starts an HTTPS
listener with a self-signed certificate (generated by the cram test). The
handler is irrelevant: the test only checks the TLS handshake. *)

let respond _vh _config_info _path _request_state =
Lwt.return
(Ocsigen.Extensions.Ext_found
(fun () ->
Lwt.return
(Ocsigen.Response.make
(Cohttp.Response.make ~status:`OK ()))))

let () =
Ocsigen.Server.start ~ports:[]
~ssl_ports:[ (`All, 8453) ]
~ssl_info:
(Some
{ Ocsigen.Config.ssl_certificate = Some "cert.pem"
; ssl_privatekey = Some "privkey.pem"
; ssl_ciphers = None
; ssl_dhfile = None
; ssl_curve = None })
~logdir:"log" ~datadir:"data" ~uploaddir:None ~usedefaulthostname:true
~command_pipe:"local.cmd" ~default_charset:(Some "utf-8")
[ Ocsigen.Server.host [ respond ] ]
3 changes: 3 additions & 0 deletions test/security.t/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(executable
(name test)
(libraries ocsigenserver))
1 change: 1 addition & 0 deletions test/security.t/dune-project
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(lang dune 3.18)
26 changes: 26 additions & 0 deletions test/security.t/run.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Security hardening checks: command pipe permissions (F8) and error-body
sanitisation (F4). Self-contained (POSIX sh, no bash helper) so it runs
regardless of the cram shell.

$ mkdir -p log data
$ dune build ./test.exe 2>&1
$ dune exec -- ./test.exe >server.log 2>&1 &
$ trap 'echo shutdown > local.cmd 2>/dev/null; wait' EXIT
$ i=0; while [ ! -e local.sock ] || [ ! -e local.cmd ]; do
> i=$((i+1)); [ $i -gt 200 ] && break; sleep 0.05; done

F8: the command pipe is created with mode 0o600, so that only the server's own
user can send it control commands.

$ stat -c '%a' local.cmd
600

F4: a handler error returns the generic HTTP status reason phrase, never the
OCaml exception (the word "secret" from the exception must not appear).

$ curl --unix-socket local.sock -s -o /dev/null -w '%{http_code}\n' http://x/anything
500
$ curl --unix-socket local.sock -s http://x/anything
Error: Internal Server Error
$ curl --unix-socket local.sock -s http://x/anything | grep -c secret || true
0
17 changes: 17 additions & 0 deletions test/security.t/test.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
(* End-to-end checks for the security hardening:
- F4: a handler error must not leak the OCaml exception in the response
body; the client gets the generic HTTP status reason phrase.
- F8: the command pipe must be created with mode 0o600.

The [boom] instruction intercepts every request and fails with an
exception carrying a "secret" message that must never reach the client. *)

let boom _vh _config_info _path _request_state =
Lwt.fail (Failure "secret internal detail that must not leak to the client")

let () =
Ocsigen.Server.start
~ports:[ (`Unix "./local.sock", 0) ]
~logdir:"log" ~datadir:"data" ~uploaddir:None ~usedefaulthostname:true
~command_pipe:"local.cmd" ~default_charset:(Some "utf-8")
[ Ocsigen.Server.host [ boom ] ]
Loading