Skip to content

Commit

Permalink
Merge pull request #23 from factorhouse/hsts-header-support
Browse files Browse the repository at this point in the history
HSTS Header Support
  • Loading branch information
d-t-w authored Dec 9, 2024
2 parents 89eb979 + 949fa8a commit 65a7280
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 81 deletions.
23 changes: 18 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ name: Slipway Test

on: [push]

env: # runner has 7g of ram
JVM_OPTS: -Xmx6G

jobs:

clojure:
build:

runs-on: ubuntu-latest

strategy:
Expand All @@ -31,7 +35,7 @@ jobs:
java-version: '11'

- name: Install clojure tools
uses: DeLaGuardo/setup-clojure@12.3
uses: DeLaGuardo/setup-clojure@13.0
with:
lein: 'latest'
github-token: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -57,13 +61,22 @@ jobs:
run: lein uberjar

- name: NVD
working-directory: ./${{ matrix.project }}
run: ../scripts/dependency-checker.sh
uses: dependency-check/Dependency-Check_Action@main
env:
# actions/setup-java changes JAVA_HOME so it needs to be reset to match the depcheck image
JAVA_HOME: /opt/jdk
with:
project: ${{ matrix.project }}
path: ${{ matrix.project }}/target
format: 'HTML'
out: ${{ matrix.project }}/reports
args: >
--suppression ${{ matrix.project }}/dependency-check-suppressions.xml
- name: Persist NVD
if: always()
uses: actions/upload-artifact@v4
with:
name: nvd-${{ matrix.project }}-${{ github.sha }}
path: ./${{ matrix.project }}/dependency-check/report/*
path: ${{ matrix.project }}/reports/*
retention-days: 1
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
# Change Log
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/)

## [1.1.18] - 2024-12-09

Introduce ability configure HSTS (HTTP Strict Transport Security) with new slipway.connector.https settings:

* :sts-max-age
* :sts-include-subdomains?

For more informmation, see: https://github.com/factorhouse/kpow/issues/35

Also made these http/https settings configurable (default false, previously hard-coded to false):

* :send-server-version?
* :send-date-header?

## [1.1.17] - 2024-09-05

Bump to latest Jetty version (11.0.24 or equivalent)
Expand Down
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Jetty is sophisticated as it addresses a complex domain with flexibility and con

Slipway holds close to Jetty idioms for configuration rather than presenting a simplified DSL.

Slipway takes a map of namespaced configuration.
Slipway takes a single map of namespaced configuration. Namespaces correspond to Jetty domain models, and can be considered as separate maps and then merged.

### :slipway

Expand Down Expand Up @@ -310,25 +310,27 @@ Configuration of Jetty auth options.
See examples below for configuration guides to JAAS and HASH authentication.

```clojure
#:slipway.security{:realm "the Jetty authentication realm"
:hash-user-file "the path to a Jetty Hash User File"
:login-service "a Jetty LoginService identifier, 'jaas' and 'hash' supported by default"
:identity-service "a concrete Jetty IdentityService"
:authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)"
#:slipway.security{:realm "the Jetty authentication realm"
:hash-user-file "the path to a Jetty Hash User File"
:login-service "a Jetty LoginService identifier, 'jaas' and 'hash' supported by default"
:identity-service "a concrete Jetty IdentityService"
:authenticator "a concrete Jetty Authenticator (e.g. FormAuthenticator or BasicAuthenticator)"
```

### :slipway.connector.http

Configuration of an HTTP server connector.

```clojure
#:slipway.connector.http{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces."
:port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80"
:idle-timeout "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms"
:http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs"
:proxy-protocol? "if true, add the ProxyConnectionFactory. See Jetty Proxy Protocol docs"
:http-config "a concrete HttpConfiguration object to replace the default config entirely"
:configurator "a fn taking the final connector as argument, allowing further configuration"}
#:slipway.connector.http{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces."
:port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80"
:idle-timeout "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms"
:http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs"
:proxy-protocol? "if true, add the ProxyConnectionFactory. See Jetty Proxy Protocol docs"
:http-config "a concrete HttpConfiguration object to replace the default config entirely"
:configurator "a fn taking the final connector as argument, allowing further configuration"
:send-server-version? "if true, send the Server header in responses"
:send-date-header? "if true, send the Date header in responses"}
````

### :slipway.connector.https
Expand Down Expand Up @@ -358,8 +360,12 @@ Configuration of an HTTPS server connector.
:security-provider "the security provider name"
:client-auth "either :need or :want to set the corresponding need/wantClientAuth field"
:ssl-context "a concrete pre-configured SslContext"
:sni-required? "true if a SNI certificate is required, default false"
:sni-host-check? "true if the SNI Host name must match, default false"}
:sni-required? "if true SNI is required, else requests will be rejected with 400 response, default false"
:sni-host-check? "if true the SNI Host name must match when there is an SNI certificate, default false"
:sts-max-age "set the Strict-Transport-Security max age in seconds, default -1"
:sts-include-subdomains? "true if a include subdomain property is sent with any Strict-Transport-Security header"
:send-server-version? "if true, send the Server header in responses"
:send-date-header? "if true, send the Date header in responses"}
```

### :slipway.handler.gzip
Expand Down
2 changes: 2 additions & 0 deletions common-jetty1x/src/slipway/websockets.clj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

(extend-protocol common.ws/WebSocketSend

#_:clj-kondo/ignore
(Class/forName "[B")
(-send!
([ba ws]
Expand Down Expand Up @@ -67,6 +68,7 @@

(extend-protocol common.ws/WebSocketPing

#_:clj-kondo/ignore
(Class/forName "[B")
(-ping! [ba ws] (common.ws/-ping! (ByteBuffer/wrap ba) ws)))

Expand Down
6 changes: 4 additions & 2 deletions common/src/slipway.clj
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
:security-provider "the security provider name"
:client-auth "either :need or :want to set the corresponding need/wantClientAuth field"
:ssl-context "a concrete pre-configured SslContext"
:sni-required? "true if a SNI certificate is required, default false"
:sni-host-check? "true if the SNI Host name must match, default false"}
:sni-required? "true if SNI is required, else requests will be rejected with 400 response, default false"
:sni-host-check? "true if the SNI Host name must match when there is an SNI certificate, default false"
:sts-max-age "set the Strict-Transport-Security max age in seconds, default -1"
:sts-include-subdomains? "true if a include subdomain property is sent with any Strict-Transport-Security header"}

#:slipway.connector.http{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces."
:port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80"
Expand Down
24 changes: 14 additions & 10 deletions common/src/slipway/connector/http.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@
HttpConnectionFactory ProxyConnectionFactory Server ServerConnector)))

(defn default-config ^HttpConfiguration
[{::keys [http-forwarded?]}]
[{::keys [http-forwarded? send-server-version? send-date-header?]
:or {send-server-version? false
send-date-header? false}}]
(let [config (doto (HttpConfiguration.)
(.setSendServerVersion false)
(.setSendDateHeader false))]
(.setSendServerVersion send-server-version?)
(.setSendDateHeader send-date-header?))]
(when http-forwarded? (.addCustomizer config (ForwardedRequestCustomizer.)))
config))

(comment
#:slipway.connector.http{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces"
:port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80"
:idle-timeout "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms"
:http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs"
:proxy-protocol? "if true, add the ProxyConnectionFactor. See Jetty Proxy Protocol docs"
:http-config "a concrete HttpConfiguration object to replace the default config entirely"
:configurator "a fn taking the final connector as argument, allowing further configuration"})
#:slipway.connector.http{:host "the network interface this connector binds to as an IP address or a hostname. If null or 0.0.0.0, then bind to all interfaces. Default null/all interfaces"
:port "port this connector listens on. If set to 0 a random port is assigned which may be obtained with getLocalPort(), default 80"
:idle-timeout "max idle time for a connection, roughly translates to the Socket.setSoTimeout. Default 200000 ms"
:http-forwarded? "if true, add the ForwardRequestCustomizer. See Jetty Forward HTTP docs"
:proxy-protocol? "if true, add the ProxyConnectionFactor. See Jetty Proxy Protocol docs"
:http-config "a concrete HttpConfiguration object to replace the default config entirely"
:configurator "a fn taking the final connector as argument, allowing further configuration"
:send-server-version? "if true, send the Server header in responses"
:send-date-header? "if true, send the Date header in responses"})

(defmethod server/connector ::connector
[^Server server {::keys [host port idle-timeout proxy-protocol? http-forwarded? configurator http-config]
Expand Down
28 changes: 21 additions & 7 deletions common/src/slipway/connector/https.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@
(org.eclipse.jetty.util.ssl SslContextFactory$Server)))

(defn default-config ^HttpConfiguration
[{::keys [port http-forwarded? sni-required? sni-host-check?] :or {sni-required? false sni-host-check? false}}]
(log/infof "sni required? %s, sni host check? %s" sni-required? sni-host-check?)
[{::keys [port http-forwarded? sni-required? sni-host-check? sts-max-age sts-include-subdomains? send-server-version?
send-date-header?]
:or {sni-required? false
sni-host-check? false
sts-max-age -1
sts-include-subdomains? false
send-server-version? false
send-date-header? false}}]
(log/infof "sni required? %s, sni host check? %s, sts-max-age %s, sts-include-subdomains? %s"
sni-required? sni-host-check? sts-max-age sts-include-subdomains?)
(let [config (doto (HttpConfiguration.)
(.setSecurePort port)
(.setSendServerVersion false)
(.setSendDateHeader false)
(.setSendServerVersion send-server-version?)
(.setSendDateHeader send-date-header?)
(.addCustomizer (doto (SecureRequestCustomizer.)
(.setSniRequired sni-required?)
(.setSniHostCheck sni-host-check?))))]
(.setSniHostCheck sni-host-check?)
(.setStsMaxAge sts-max-age)
(.setStsIncludeSubDomains sts-include-subdomains?))))]
(when http-forwarded? (.addCustomizer config (ForwardedRequestCustomizer.)))
config))

Expand Down Expand Up @@ -98,8 +108,12 @@
:security-provider "the security provider name"
:client-auth "either :need or :want to set the corresponding need/wantClientAuth field"
:ssl-context "a concrete pre-configured SslContext"
:sni-required? "true if a SNI certificate is required, default false"
:sni-host-check? "true if the SNI Host name must match, default false"})
:sni-required? "if true SNI is required, else requests will be rejected with 400 response, default false"
:sni-host-check? "if true the SNI Host name must match when there is an SNI certificate, default false"
:sts-max-age "set the Strict-Transport-Security max age in seconds, default -1"
:sts-include-subdomains? "true if a include subdomain property is sent with any Strict-Transport-Security header"
:send-server-version? "if true, send the Server header in responses"
:send-date-header? "if true, send the Date header in responses"})

(defmethod server/connector ::connector
[^Server server {::keys [host port idle-timeout proxy-protocol? http-config configurator]
Expand Down
4 changes: 2 additions & 2 deletions common/src/slipway/security.clj
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
(if config
(when (slurp config)
(doto (JAASLoginService. realm) (.setConfiguration (Configuration/getConfiguration))))
(throw (ex-info (str "start with -Djava.security.auth.login.config=/some/path/to/jaas.config to use Jetty/JAAS auth provider") {})))))
(throw (ex-info "start with -Djava.security.auth.login.config=/some/path/to/jaas.config to use Jetty/JAAS auth provider" {})))))

(defmethod login-service "hash"
[{::keys [realm hash-user-file]}]
(log/infof "initializing HashLoginService - realm: %s, realm file: %s" realm hash-user-file)
(if hash-user-file
(when (slurp hash-user-file)
(HashLoginService. realm hash-user-file))
(throw (ex-info (str "set the path to your hash user realm properties file") {}))))
(throw (ex-info "set the path to your hash user realm properties file" {}))))

(defn user
[^Request base-request]
Expand Down
17 changes: 17 additions & 0 deletions common/test/slipway/example.clj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
:truststore-password "password"
:truststore-type "PKCS12"})

(def hsts #::https{:sts-max-age 31536000
:sts-include-subdomains? true})

(def hsts-no-subdomains #::https{:sts-max-age 31536000})

(def hsts-no-max-age #::https{:sts-include-subdomains? true})

(def form-authenticator (FormAuthenticator. "/login" "/login-retry" false))

(def options
Expand All @@ -38,6 +45,16 @@
:https #::server{:connectors [https-connector]
:error-handler app/server-error-handler}

:hsts #::server{:connectors [(merge https-connector hsts)]
:error-handler app/server-error-handler}

:hsts-no-subdomains #::server{:connectors [(merge https-connector hsts-no-subdomains)]
:error-handler app/server-error-handler}

;; this is an error condition / incorrect configuration - subdomains requires max-age set
:hsts-no-max-age #::server{:connectors [(merge https-connector hsts-no-max-age)]
:error-handler app/server-error-handler}

:http+https #::server{:connectors [http-connector https-connector]
:error-handler app/server-error-handler}

Expand Down
Loading

0 comments on commit 65a7280

Please sign in to comment.