diff --git a/api/authentication.go b/api/authentication.go
index b101551..97f3786 100644
--- a/api/authentication.go
+++ b/api/authentication.go
@@ -133,16 +133,13 @@ func SetAuthenticator(fn AuthenticatorFunc) error {
 	return nil
 }
 
-func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler) *AuthToken {
+func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler, readMethod bool) *AuthToken {
 	tracer := log.Tracer(r.Context())
 
-	// Check if request is read only.
-	readRequest := isReadMethod(r.Method)
-
 	// Get required permission for target handler.
 	requiredPermission := PermitSelf
 	if authdHandler, ok := targetHandler.(AuthenticatedHandler); ok {
-		if readRequest {
+		if readMethod {
 			requiredPermission = authdHandler.ReadPermission(r)
 		} else {
 			requiredPermission = authdHandler.WritePermission(r)
@@ -200,7 +197,7 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
 
 	// Get effective permission for request.
 	var requestPermission Permission
-	if readRequest {
+	if readMethod {
 		requestPermission = token.Read
 	} else {
 		requestPermission = token.Write
@@ -221,7 +218,10 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
 	if requestPermission < requiredPermission {
 		// If the token is strictly public, return an authentication request.
 		if token.Read == PermitAnyone && token.Write == PermitAnyone {
-			w.Header().Set("WWW-Authenticate", "Bearer realm=Portmaster API")
+			w.Header().Set(
+				"WWW-Authenticate",
+				`Bearer realm="Portmaster API" domain="/"`,
+			)
 			http.Error(w, "Authorization required.", http.StatusUnauthorized)
 			return nil
 		}
@@ -477,12 +477,24 @@ func deleteSession(sessionKey string) {
 	delete(sessions, sessionKey)
 }
 
-func isReadMethod(method string) bool {
+func getEffectiveMethod(r *http.Request) (eMethod string, readMethod bool, ok bool) {
+	method := r.Method
+
+	// Get CORS request method if OPTIONS request.
+	if r.Method == http.MethodOptions {
+		method = r.Header.Get("Access-Control-Request-Method")
+		if method == "" {
+			return "", false, false
+		}
+	}
+
 	switch method {
-	case http.MethodGet, http.MethodHead, http.MethodOptions:
-		return true
+	case http.MethodGet, http.MethodHead:
+		return http.MethodGet, true, true
+	case http.MethodPost, http.MethodPut, http.MethodDelete:
+		return method, false, true
 	default:
-		return false
+		return "", false, false
 	}
 }
 
diff --git a/api/authentication_test.go b/api/authentication_test.go
index 0eafbdc..0de7a6b 100644
--- a/api/authentication_test.go
+++ b/api/authentication_test.go
@@ -102,7 +102,7 @@ func TestPermissions(t *testing.T) { //nolint:gocognit
 			} {
 
 				// Set request permission for test requests.
-				reading := isReadMethod(method)
+				reading := method == http.MethodGet
 				if reading {
 					testToken.Read = requestPerm
 					testToken.Write = NotSupported
diff --git a/api/endpoints.go b/api/endpoints.go
index 65106ae..14e6e60 100644
--- a/api/endpoints.go
+++ b/api/endpoints.go
@@ -371,9 +371,20 @@ func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// TODO: Return errors instead of warnings, also update the field docs.
-	if isReadMethod(r.Method) {
-		if r.Method != e.ReadMethod {
+	// Return OPTIONS request before starting to handle normal requests.
+	if r.Method == http.MethodOptions {
+		w.WriteHeader(http.StatusNoContent)
+		return
+	}
+
+	eMethod, readMethod, ok := getEffectiveMethod(r)
+	if !ok {
+		http.Error(w, "unsupported method for the actions API", http.StatusMethodNotAllowed)
+		return
+	}
+
+	if readMethod {
+		if eMethod != e.ReadMethod {
 			log.Tracer(r.Context()).Warningf(
 				"api: method %q does not match required read method %q%s",
 				" - this will be an error and abort the request in the future",
@@ -382,7 +393,7 @@ func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			)
 		}
 	} else {
-		if r.Method != e.WriteMethod {
+		if eMethod != e.WriteMethod {
 			log.Tracer(r.Context()).Warningf(
 				"api: method %q does not match required write method %q%s",
 				" - this will be an error and abort the request in the future",
@@ -392,10 +403,9 @@ func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	switch r.Method {
-	case http.MethodHead:
-		w.WriteHeader(http.StatusOK)
-		return
+	switch eMethod {
+	case http.MethodGet, http.MethodDelete:
+		// Nothing to do for these.
 	case http.MethodPost, http.MethodPut:
 		// Read body data.
 		inputData, ok := readBody(w, r)
@@ -403,12 +413,8 @@ func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 		apiRequest.InputData = inputData
-	case http.MethodGet:
-		// Nothing special to do here.
-	case http.MethodOptions:
-		w.WriteHeader(http.StatusNoContent)
-		return
 	default:
+		// Defensive.
 		http.Error(w, "unsupported method for the actions API", http.StatusMethodNotAllowed)
 		return
 	}
@@ -466,8 +472,8 @@ func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// Check if there is no response data.
-	if len(responseData) == 0 {
+	// Return no content if there is none, or if request is HEAD.
+	if len(responseData) == 0 || r.Method == http.MethodHead {
 		w.WriteHeader(http.StatusNoContent)
 		return
 	}
diff --git a/api/endpoints_meta.go b/api/endpoints_meta.go
index 0cf4189..74019fa 100644
--- a/api/endpoints_meta.go
+++ b/api/endpoints_meta.go
@@ -93,7 +93,10 @@ func authBearer(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// Respond with desired authentication header.
-	w.Header().Set("WWW-Authenticate", "Bearer realm=Portmaster API")
+	w.Header().Set(
+		"WWW-Authenticate",
+		`Bearer realm="Portmaster API" domain="/"`,
+	)
 	http.Error(w, "Authorization required.", http.StatusUnauthorized)
 }
 
@@ -106,7 +109,10 @@ func authBasic(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// Respond with desired authentication header.
-	w.Header().Set("WWW-Authenticate", "Basic realm=Portmaster API")
+	w.Header().Set(
+		"WWW-Authenticate",
+		`Basic realm="Portmaster API" domain="/"`,
+	)
 	http.Error(w, "Authorization required.", http.StatusUnauthorized)
 }
 
@@ -127,7 +133,7 @@ func authReset(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Clear-Site-Data", "*")
 
 	// Set HTTP Auth Realm without requesting authorization.
-	w.Header().Set("WWW-Authenticate", "None realm=Portmaster API")
+	w.Header().Set("WWW-Authenticate", `None realm="Portmaster API"`)
 
 	// Reply with 401 Unauthorized in order to clear HTTP Basic Auth data.
 	http.Error(w, "Session deleted.", http.StatusUnauthorized)
diff --git a/api/router.go b/api/router.go
index 463986d..85d6caa 100644
--- a/api/router.go
+++ b/api/router.go
@@ -125,14 +125,31 @@ func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
 		apiRequest.Route = match.Route
 		apiRequest.URLVars = match.Vars
 	}
+	switch {
+	case match.MatchErr == nil:
+		// All good.
+	case errors.Is(match.MatchErr, mux.ErrMethodMismatch):
+		http.Error(lrw, "Method not allowed.", http.StatusMethodNotAllowed)
+		return nil
+	default:
+		http.Error(lrw, "Not found.", http.StatusNotFound)
+		return nil
+	}
 
 	// Be sure that URLVars always is a map.
 	if apiRequest.URLVars == nil {
 		apiRequest.URLVars = make(map[string]string)
 	}
 
+	// Check method.
+	_, readMethod, ok := getEffectiveMethod(r)
+	if !ok {
+		http.Error(lrw, "Method not allowed.", http.StatusMethodNotAllowed)
+		return nil
+	}
+
 	// Check authentication.
-	apiRequest.AuthToken = authenticateRequest(lrw, r, handler)
+	apiRequest.AuthToken = authenticateRequest(lrw, r, handler, readMethod)
 	if apiRequest.AuthToken == nil {
 		// Authenticator already replied.
 		return nil
@@ -164,18 +181,21 @@ func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
 	} else if origin := r.Header.Get("Origin"); origin != "" {
 		// Allow cross origin requests from localhost in dev mode.
 		if u, err := url.Parse(origin); err == nil &&
-			utils.StringInSlice(allowedDevCORSOrigins, u.Host) {
+			utils.StringInSlice(allowedDevCORSOrigins, u.Hostname()) {
 			w.Header().Set("Access-Control-Allow-Origin", origin)
+			w.Header().Set("Access-Control-Allow-Methods", "*")
+			w.Header().Set("Access-Control-Allow-Headers", "*")
+			w.Header().Set("Access-Control-Allow-Credentials", "true")
+			w.Header().Set("Access-Control-Expose-Headers", "*")
+			w.Header().Set("Access-Control-Max-Age", "60")
+			w.Header().Add("Vary", "Origin")
 		}
 	}
 
 	// Handle request.
-	switch {
-	case handler != nil:
+	if handler != nil {
 		handler.ServeHTTP(lrw, r)
-	case errors.Is(match.MatchErr, mux.ErrMethodMismatch):
-		http.Error(lrw, "Method not allowed.", http.StatusMethodNotAllowed)
-	default: // handler == nil or other error
+	} else {
 		http.Error(lrw, "Not found.", http.StatusNotFound)
 	}
 
diff --git a/metrics/config.go b/metrics/config.go
index dfcfc02..91b4c95 100644
--- a/metrics/config.go
+++ b/metrics/config.go
@@ -47,7 +47,7 @@ func prepConfig() error {
 	instanceOption = config.Concurrent.GetAsString(CfgOptionInstanceKey, instanceFlag)
 
 	err = config.Register(&config.Option{
-		Name:            "Push Metrics",
+		Name:            "Push Prometheus Metrics",
 		Key:             CfgOptionPushKey,
 		Description:     "Push metrics to this URL in the prometheus format.",
 		OptType:         config.OptTypeString,