Custom SSL/HSTS

I have upgraded to version 6 today and everything seems to be running smoothly. I previously had some custom SSL configuration running through Lighttpd, including but not limited to specific TLS ciphers as well as HSTS and a few other headers.

I think I can see how to set the headers (field seems to be present in the file, although not yet tested), but I cannot figure out how to set ciphers and enable HSTS. My setup is as below (not running in Docker):

  • Ubuntu Server 22.04 LTS
  • Raspberry Pi 2B

I found a page for CivetWeb detailing how to do it, but when I make these changes in pihole.toml, they are ignored and wiped service restart. Page is at civetweb/examples/https/civetweb.conf at master · civetweb/civetweb

Is this possible to do? Or perhaps supported by drop-in configuration file?

What webserver options did you change in pihole.toml?
Please share the respective section, with the changes that you want applied.

I tried to apply the below in pihole.toml, as well as via the web interface (API) - scans suggest it is not taking effect:

strict_transport_security_max_age 15552000

I also tried applying the below directly into the TOML file, without any success:

ssl_protocol_version 4
ssl_cipher_list ECDH+AESGCM+AES256:!aNULL:!MD5:!DSS

This is my first time working with TOML, so it is entirely possible/probable that I am making a mistake with my syntax or applying it in the wrong place. I believe the HSTS component is the right place, as I understand that to be a header. The ciphers are a different matter.

To answer the question directly, I did not change any options, but added some additional configuration. Any idea what I am missing?

Much appreciated!

These are not invalid pihole.toml options.

Where did you find them?

From the CivetWeb repository: civetweb/examples/https/civetweb.conf at 5864b55a94f4b5238155cbf2baec707f0fa2ba6d · civetweb/civetweb

Did I screw up somewhere and make a bad assumption? I was under the impression that the web server was based on CivetWeb in v6.

Am I trying to do something which is not possible? If that is the case, how do I go about logging a feature request? There are also other headers which I would like to set, but these are the important ones for me.

The web server is based on civetweb, but Pi-hole uses a completely different config file and not every civetweb settings is exposed. Some options would require FTL code changes to work.

That .conf file format can't be read or understood by Pi-hole.

To be honest, I don't know if there is a way to set the options you want without changing code (maybe @DL6ER can answer that), but I saw other users configuring HSTS using a reverse proxy.


You can open a topic here: Feature Requests - Pi-hole Userspace

Did you share your pihole.toml webserver section yet?

webserver.headers would seem a likely candidate for adding your custom headers, but we'd need to see your genuine pihole.toml content.

This makes sense, thanks - so I am trying to do something which is not possible at the moment. I would prefer not to use a reverse proxy in this case if possible, but am prepared to forego the header if necessary. If I was running Pi-hole in a container, then it would be much simpler for sure.

Thank you for the link, will consider my wording and what exactly I would like to request if this is the best way forward.

1 Like
[webserver]
  # On which domain is the web interface served?
  #
  # Possible values are:
  #     <valid domain>
  domain = "pihole.domain" ### CHANGED, default = "pi.hole"

  # Webserver access control list (ACL) allowing for restrictions to be put on the list
  # of IP addresses which have access to the web server. The ACL is a comma separated
  # list of IP subnets, where each subnet is prepended by either a - or a + sign. A plus
  # sign means allow, where a minus sign means deny. If a subnet mask is omitted, such
  # as -1.2.3.4, this means to deny only that single IP address. If this value is not
  # set (empty string), all accesses are allowed. Otherwise, the default setting is to
  # deny all accesses. On each request the full list is traversed, and the last (!)
  # match wins. IPv6 addresses may be specified in CIDR-form [a:b::c]/64.
  #
  # Example 1: acl = "+127.0.0.1,+[::1]"
  # ---> deny all access, except from 127.0.0.1 and ::1,
  # Example 2: acl = "+192.168.0.0/16"
  # ---> deny all accesses, except from the 192.168.0.0/16 subnet,
  # Example 3: acl = "+[::]/0" ---> allow only IPv6 access.
  #
  # Possible values are:
  #     <valid ACL>
  acl = ""

  # Ports to be used by the webserver.
  # Comma-separated list of ports to listen on. It is possible to specify an IP address
  # to bind to. In this case, an IP address and a colon must be prepended to the port
  # number. For example, to bind to the loopback interface on port 80 (IPv4) and to all
  # interfaces port 8080 (IPv4), use "127.0.0.1:80,8080". "[::]:80" can be used to
  # listen to IPv6 connections to port 80. IPv6 addresses of network interfaces can be
  # specified as well, e.g. "[::1]:80" for the IPv6 loopback interface. [::]:80 will
  # bind to port 80 IPv6 only.
  # In order to use port 80 for all interfaces, both IPv4 and IPv6, use either the
  # configuration "80,[::]:80" (create one socket for IPv4 and one for IPv6 only), or
  # "+80" (create one socket for both, IPv4 and IPv6). The '+' notation to use IPv4 and
  # IPv6 will only work if no network interface is specified. Depending on your
  # operating system version and IPv6 network environment, some configurations might not
  # work as expected, so you have to test to find the configuration most suitable for
  # your needs. In case "+80" does not work for your environment, you need to use
  # "80,[::]:80".
  # If the port is TLS/SSL, a letter 's' (secure) must be appended, for example,
  # "80,443s" will open port 80 and port 443, and connections on port 443 will be
  # encrypted. For non-encrypted ports, it is allowed to append letter 'r' (as in
  # redirect). Redirected ports will redirect all their traffic to the first configured
  # SSL port. For example, if webserver.port is "80r,443s", then all HTTP traffic coming
  # at port 80 will be redirected to HTTPS port 443.
  # When specifying 'o' (optional) behind a port, inability to use this port is not
  # considered an error. For instance, specifying "80o,8080o" will allow the webserver
  # to listen on either 80, 8080, both or even none of the two ports. This flag may be
  # combined with 'r' and 's' like "80or,443os,8080,4443s" (80 redirecting to SSL if
  # available, 443 encrypted if available, 8080 mandatory and unencrypted, 4443
  # mandatory and encrypted).
  # If this value is not set (empty string), the web server will not be started and,
  # hence, the API will not be available.
  #
  # Possible values are:
  #     comma-separated list of <[ip_address:]port>
  port = "80o,443os,[::]:80o,[::]:443os"

  # Maximum number of worker threads allowed.
  # The Pi-hole web server handles each incoming connection in a separate thread.
  # Therefore, the value of this option is effectively the number of concurrent HTTP
  # connections that can be handled. Any other connections are queued until they can be
  # processed by a unoccupied thread.
  # The total number of threads you see may be lower than the configured value as
  # threads are only created when needed due to incoming connections.
  # The value 0 means the number of threads is 50 (as per default settings of CivetWeb)
  # for backwards-compatible behavior.
  threads = 50

  # Additional HTTP headers added to the web server responses.
  # The headers are added to all responses, including those for the API.
  # Note about the default additional headers:
  # - X-DNS-Prefetch-Control: off: Usually browsers proactively perform domain name
  # resolution on links that the user may choose to follow. We disable DNS prefetching
  # here.
  # - Content-Security-Policy: [...] 'unsafe-inline' is both required by Chart.js
  # styling some elements directly, and index.html containing some inlined Javascript
  # code.
  # - X-Frame-Options: DENY: The page can not be displayed in a frame, regardless of the
  # site attempting to do so.
  # - X-Xss-Protection: 0: Disables XSS filtering in browsers that support it. This
  # header is usually enabled by default in browsers, and is not recommended as it can
  # hurt the security of the site.
  # (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection).
  # - X-Content-Type-Options: nosniff: Marker used by the server to indicate that the
  # MIME types advertised in the  Content-Type headers should not be changed and be
  # followed. This allows to opt-out of MIME type sniffing, or, in other words, it is a
  # way to say that the webmasters knew what they were doing. Site security testers
  # usually expect this header to be set.
  # - Referrer-Policy: strict-origin-when-cross-origin: A referrer will be sent for
  # same-site origins, but cross-origin requests will send no referrer information.
  # The latter four headers are set as expected by https://securityheaders.io
  #
  # Possible values are:
  #     array of HTTP headers
  headers = [
    "X-DNS-Prefetch-Control: off",
    "Content-Security-Policy: default-src 'self' 'unsafe-inline';",
    "X-Frame-Options: DENY",
    "X-XSS-Protection: 0",
    "X-Content-Type-Options: nosniff",
    "Referrer-Policy: strict-origin-when-cross-origin",
    "strict_transport_security_max_age: 15552000"
  ] ### CHANGED, default = [ "X-DNS-Prefetch-Control: off", "Content-Security-Policy: default-src 'self' 'unsafe-inline';", "X-Frame-Options: DENY", "X-XSS-Protection: 0", "X-Content-Type-Options: nosniff", "Referrer-Policy: strict-origin-when-cross-origin" ]

  # Should the web server serve all files in webserver.paths.webroot directory? If
  # disabled, only files within the path defined through webserver.paths.webhome and
  # /api will be served.
  serve_all = false

  [webserver.session]
    # Session timeout in seconds. If a session is inactive for more than this time, it will
    # be terminated. Sessions are continuously refreshed by the web interface, preventing
    # sessions from timing out while the web interface is open.
    # This option may also be used to make logins persistent for long times, e.g. 86400
    # seconds (24 hours), 604800 seconds (7 days) or 2592000 seconds (30 days). Note that
    # the total number of concurrent sessions is limited so setting this value too high
    # may result in users being rejected and unable to log in if there are already too
    # many sessions active.
    timeout = 1800

    # Should Pi-hole backup and restore sessions from the database? This is useful if you
    # want to keep your sessions after a restart of the web interface.
    restore = true

  [webserver.tls]
    # Path to the TLS (SSL) certificate file. All directories along the path must be
    # readable and accessible by the user running FTL (typically 'pihole'). This option is
    # only required when at least one of webserver.port is TLS. The file must be in PEM
    # format, and it must have both, private key and certificate (the *.pem file created
    # must contain a 'CERTIFICATE' section as well as a 'RSA PRIVATE KEY' section).
    # The *.pem file can be created using
    #     cp server.crt server.pem
    #     cat server.key >> server.pem
    # if you have these files instead
    #
    # Possible values are:
    #     <valid TLS certificate file (*.pem)>
    cert = "/etc/pihole/tls.pem"

  [webserver.paths]
    # Server root on the host
    #
    # Possible values are:
    #     <valid path>
    webroot = "/var/www/html"

    # Sub-directory of the root containing the web interface
    #
    # Possible values are:
    #     <valid subpath>, both slashes are needed!
    webhome = "/admin/"

    # Prefix where the web interface is served
    # This is useful when you are using a reverse proxy serving the web interface, e.g.,
    # at http://<ip>/pihole/admin/ instead of http://<ip>/admin/. In this example, the
    # prefix would be "/pihole". Note that the prefix has to be stripped away by the
    # reverse proxy, e.g., for traefik:
    # - traefik.http.routers.pihole.rule=PathPrefix(`/pihole`)
    # - traefik.http.middlewares.piholehttp.stripprefix.prefixes=/pihole
    # The prefix should start with a slash. If you don't use a prefix, leave this field
    # empty. Setting this field to an incorrect value may result in the web interface not
    # being accessible.
    # Don't use this setting if you are not using a reverse proxy!
    #
    # Possible values are:
    #     valid URL prefix or empty
    prefix = ""

  [webserver.interface]
    # Should the web interface use the boxed layout?
    boxed = false ### CHANGED, default = true

    # Theme used by the Pi-hole web interface
    #
    # Possible values are:
    #   - "default-auto"
    #       Pi-hole auto
    #   - "default-light"
    #       Pi-hole day
    #   - "default-dark"
    #       Pi-hole midnight
    #   - "default-darker"
    #       Pi-hole deep-midnight
    #   - "high-contrast"
    #       High-contrast light
    #   - "high-contrast-dark"
    #       High-contrast dark
    #   - "lcars"
    #       Star Trek LCARS
    theme = "default-darker" ### CHANGED, default = "default-auto"

  [webserver.api]
    # Number of concurrent sessions allowed for the API. If the number of sessions exceeds
    # this value, no new sessions will be allowed until the number of sessions drops due
    # to session expiration or logout. Note that the number of concurrent sessions is
    # irrelevant if authentication is disabled as no sessions are used in this case.
    max_sessions = 16

    # Should FTL prettify the API output (add extra spaces, newlines and indentation)?
    prettyJSON = false

    # API password hash
    #
    # Possible values are:
    #     <valid Pi-hole password hash>
    pwhash = "$BALLOON-SHA256$v=1$s=1024,t=32$T1ATqLF5ro1CbwQOD75IyQ==$XhA/hKdAp7ba89TpC3k09ZEfb+GArGfZpGDz9sIydgI=" ### CHANGED, default = ""

    # Pi-hole 2FA TOTP secret. When set to something different than "", 2FA authentication
    # will be enforced for the API and the web interface. This setting is write-only, you
    # can not read the secret back.
    #
    # Possible values are:
    #     <valid TOTP secret (20 Bytes in Base32 encoding)>
    totp_secret = ""

    # Pi-hole application password.
    # After you turn on two-factor (2FA) verification and set up an Authenticator app, you
    # may run into issues if you use apps or other services that don't support two-step
    # verification. In this case, you can create and use an app password to sign in. An
    # app password is a long, randomly generated password that can be used instead of your
    # regular password + TOTP token when signing in to the API. The app password can be
    # generated through the API and will be shown only once. You can revoke the app
    # password at any time. If you revoke the app password, be sure to generate a new one
    # and update your app with the new password.
    #
    # Possible values are:
    #     <valid Pi-hole password hash>
    app_pwhash = "$BALLOON-SHA256$v=1$s=1024,t=32$osCs30rf0u8S9Z1OWxyDIw==$4a30xRKAou653XFsNhVhcaZyt0Uqu68O0cGNy8cLfuE=" ### CHANGED, default = ""

    # Should application password API sessions be allowed to modify config settings?
    # Setting this to true allows third-party applications using the application password
    # to modify settings, e.g., the upstream DNS servers, DHCP server settings, or
    # changing passwords. This setting should only be enabled if really needed and only if
    # you trust the applications using the application password.
    app_sudo = false

    # Should FTL create a temporary CLI password? This password is stored in clear in
    # /etc/pihole and can be used by the CLI (pihole ...  commands) to authenticate
    # against the API. Note that the password is only valid for the current session and
    # regenerated on each FTL restart. Sessions initiated with this password cannot modify
    # the Pi-hole configuration (change passwords, etc.) for security reasons but can
    # still use the API to query data and manage lists.
    cli_pw = true

    # Array of clients to be excluded from certain API responses (regex):
    # - Query Log (/api/queries)
    # - Top Clients (/api/stats/top_clients)
    # This setting accepts both IP addresses (IPv4 and IPv6) as well as hostnames.
    # Note that backslashes "\" need to be escaped, i.e. "\\" in this setting
    #
    # Example: [ "^192\\.168\\.2\\.56$", "^fe80::341:[0-9a-f]*$", "^localhost$" ]
    #
    # Possible values are:
    #     array of regular expressions describing clients
    excludeClients = []

    # Array of domains to be excluded from certain API responses (regex):
    # - Query Log (/api/queries)
    # - Top Clients (/api/stats/top_domains)
    # Note that backslashes "\" need to be escaped, i.e. "\\" in this setting
    #
    # Example: [ "(^|\\.)\\.google\\.de$", "\\.pi-hole\\.net$" ]
    #
    # Possible values are:
    #     array of regular expressions describing domains
    excludeDomains = []

    # How much history should be imported from the database and returned by the API
    # [seconds]? (max 24*60*60 = 86400)
    maxHistory = 86400

    # Up to how many clients should be returned in the activity graph endpoint
    # (/api/history/clients)?
    # This setting can be overwritten at run-time using the parameter N. Setting this to 0
    # will always send all clients. Be aware that this may be challenging for the GUI if
    # you have many (think > 1.000 clients) in your network
    maxClients = 10

    # How should the API compute the most active clients? If set to true, the API will
    # return the clients with the most queries globally (within 24 hours). If set to
    # false, the API will return the clients with the most queries per time slot
    # individually.
    client_history_global_max = true

    # Allow destructive API calls (e.g. restart DNS server, flush logs, ...)
    allow_destructive = false ### CHANGED, default = true

    [webserver.api.temp]
      # Which upper temperature limit should be used by Pi-hole? Temperatures above this
      # limit will be shown as "hot". The number specified here is in the unit defined below
      limit = 60.000000

      # Which temperature unit should be used for temperatures processed by FTL?
      #
      # Possible values are:
      #   - "C"
      #       Celsius
      #   - "F"
      #       Fahrenheit
      #   - "K"
      #       Kelvin
      unit = "C"

I have pasted the full webserver section from my file. I agree the headers section makes the most sense; I have left the HSTS string in for now as I applied this through the UI, even though it doesn’t take effect as explained by @rdwebdesign .

Happy to provide any other configuration or output requested, thank you!

I think those civetweb options would need to be translated to their proper HTML equivalents, e.g. "Strict-Transport-Security: max-age=15552000".

1 Like
1 Like

We are getting somewhere: HSTS header is now present after applying the below:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

That solves that particular problem and I will experiment with the remainder of my previous headers later in the week.

This still leaves the question of TLS ciphers, which I do not feel belong under headers but perhaps rather under their own TLS section. If my thinking is correct, I am happy to open a feature request specifically for TLS cipher/version selection.

Progress so far, thank you :grinning_face_with_smiling_eyes:

I think civetweb uses ssl_protocol_version = 4 by default... also apparently Pi-hole already uses this value:

Agreed, scans show the same thing. Also agree with the comment that TLS 1.2 should be the bare minimum in today’s world.

The cipher list is also at default though, which while maintaining wider compatibility does include some weaker ciphers (e.g. CBC is known to have issues). I would be wary of adjusting defaults, but if we can have the option to customise the ciphers in use, that would be awesome.

EDIT: looks like it is default at line 2229:

{"ssl_cipher_list", MG_CONFIG_TYPE_STRING, NULL},