Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 3 additions & 4 deletions dgraph/cmd/alpha/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,7 @@ func queryHandler(w http.ResponseWriter, r *http.Request) {
}

ctx := context.WithValue(r.Context(), query.DebugKey, isDebugMode)
ctx = x.AttachAccessJwt(ctx, r)
ctx = x.AttachRemoteIP(ctx, r)
ctx = x.AttachRequestIdentity(ctx, r)

if queryTimeout != 0 {
var cancel context.CancelFunc
Expand Down Expand Up @@ -467,7 +466,7 @@ func mutationHandler(w http.ResponseWriter, r *http.Request) {
req.Hash = hash
req.CommitNow = commitNow

ctx := x.AttachAccessJwt(context.Background(), r)
ctx := x.AttachRequestIdentity(context.Background(), r)
resp, err := (&edgraph.Server{}).QueryNoGrpc(ctx, req)
if err != nil {
x.SetStatusWithData(w, x.ErrorInvalidRequest, err.Error())
Expand Down Expand Up @@ -530,7 +529,7 @@ func commitHandler(w http.ResponseWriter, r *http.Request) {
return
}

ctx := x.AttachAccessJwt(context.Background(), r)
ctx := x.AttachRequestIdentity(context.Background(), r)
var response map[string]interface{}
if abort {
response, err = handleAbort(ctx, startTs, hash)
Expand Down
45 changes: 41 additions & 4 deletions dgraph/cmd/alpha/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,18 @@ they form a Raft group and provide synchronous replication.
"A comma separated list of IP addresses, IP ranges, CIDR blocks, or hostnames you wish "+
"to whitelist for performing admin actions (i.e., --security "+
`"whitelist=144.142.126.254,127.0.0.1:127.0.0.3,192.168.0.0/16,host.docker.`+
`internal").`).
`internal"). Empty by default, which admits loopback only. This is a network `+
"location check, NOT authentication: every address in the range can run "+
"privileged operations without a credential unless you also set token= or enable "+
"ACL.").
Flag("anonymous",
"[full, data, none] What a caller that presents no verified credential may do. "+
"full (default) leaves authorization to whatever the whitelist, token, and ACL "+
"settings decide, which is the behavior of every earlier release. data allows "+
"queries, mutations, commits, and login while denying every administrative "+
"operation regardless of the whitelist. none additionally denies queries, "+
"mutations, and commits, leaving only login and the health endpoints. data and "+
"none require token= or ACL, otherwise no caller can ever be identified.").
String())

flag.String("limit", worker.LimitDefaults, z.NewSuperFlagHelp(worker.LimitDefaults).
Expand Down Expand Up @@ -310,7 +321,10 @@ func healthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

ctx := x.AttachAccessJwt(context.Background(), r)
// Full identity, not just the access JWT: Health(all) is a capability check,
// and under a closed --security "anonymous=..." posture a caller presenting
// only the --security token would otherwise arrive unidentified.
ctx := x.AttachRequestIdentity(context.Background(), r)
var resp *api.Response
if resp, err = (&edgraph.Server{}).Health(ctx, true); err != nil {
x.SetStatus(w, x.Error, err.Error())
Expand Down Expand Up @@ -355,8 +369,9 @@ func stateHandler(w http.ResponseWriter, r *http.Request) {
x.AddCorsHeaders(w)
w.Header().Set("Content-Type", "application/json")

ctx := context.Background()
ctx = x.AttachAccessJwt(ctx, r)
// Full identity, not just the access JWT: State is a capability check. See the
// note on the /health?all branch above.
ctx := x.AttachRequestIdentity(context.Background(), r)

var aResp *api.Response
if aResp, err = (&edgraph.Server{}).State(ctx); err != nil {
Expand Down Expand Up @@ -652,6 +667,8 @@ func run() {
FromSuperFlag(Alpha.Conf.GetString("badger"))
security := z.NewSuperFlag(Alpha.Conf.GetString("security")).MergeAndCheckDefault(
worker.SecurityDefaults)
anonymous, err := x.ParseAnonymousPosture(security.GetString("anonymous"))
x.Check(err)
conf := audit.GetAuditConf(Alpha.Conf.GetString("audit"))

x.Config.Limit = z.NewSuperFlag(Alpha.Conf.GetString("limit")).MergeAndCheckDefault(
Expand Down Expand Up @@ -720,6 +737,7 @@ func run() {
AbortOlderThan: abortDur,
StartTime: startTime,
Security: security,
Anonymous: anonymous,
TLSClientConfig: tlsClientConf,
TLSServerConfig: tlsServerConf,
AclJwtAlg: keys.AclJwtAlg,
Expand All @@ -729,18 +747,37 @@ func run() {
}
x.WorkerConfig.Parse(Alpha.Conf)

// The built-in authenticator: ACL's access JWT, plus the --security auth token as
// an identity rather than a boolean check. Installed before ConfigureIdentity so a
// deployment can compose with or replace it; see edgraph.PresharedAuthenticator.
x.SetAuthenticator(edgraph.PresharedAuthenticator())

// Install deployment-specific authentication and authorization now: the config
// is parsed, and nothing is serving yet. A misconfiguration here is fatal, which
// is why it runs before any listener rather than lazily on the first request.
ConfigureIdentity()

// The --security "anonymous=..." floor goes on last, so that it wraps whatever
// policy ConfigureIdentity installed rather than being replaced by it. Under the
// shipped anonymous=full this installs nothing.
edgraph.EnforceAnonymousPosture(anonymous)

// Set the directory for temporary buffers.
z.SetTmpDir(x.WorkerConfig.TmpDir)

x.WorkerConfig.EncryptionKey = keys.EncKey

setupCustomTokenizers()
x.Config.PortOffset = Alpha.Conf.GetInt("port_offset")

// Deliberately this late. It has to follow ConfigureIdentity, because a
// deployment authenticator changes who can be identified, and it has to follow
// the port offset, because the warning names the HTTP port.
builtinIdentity := x.AuthenticatorName() == edgraph.PresharedAuthenticator().Name()
for _, msg := range securityWarnings(anonymous, security.GetString("whitelist"), ips,
opts.AuthToken, keys.AclSecretKey != nil, builtinIdentity, httpPort()) {
glog.Warning(msg)
}
x.Config.LimitMutationsNquad = int(x.Config.Limit.GetInt64("mutations-nquad"))
x.Config.LimitQueryEdge = x.Config.Limit.GetUint64("query-edge")
x.Config.BlockClusterWideDrop = x.Config.Limit.GetBool("disallow-drop")
Expand Down
87 changes: 87 additions & 0 deletions dgraph/cmd/alpha/security_posture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

package alpha

import (
"fmt"

"github.com/dgraph-io/dgraph/v25/x"
)

// securityWarnings returns the startup warnings for a --security configuration
// whose parts do not add up, or nil when they do.
//
// Three controls gate Alpha's privileged operations -- the whitelist, the auth
// token, and ACL -- and each one passes when its own feature is unconfigured. The
// protection an operator gets is whatever they configured rather than the union of
// the three, and the two combinations below are the ones where that produces an
// outcome they almost certainly did not intend.
//
// It is pure and returns strings rather than logging, so the combinations can be
// pinned by a table test.
//
// builtinIdentity is false when a deployment installed its own authenticator via
// ConfigureIdentity. Such an authenticator can produce identities from credentials
// this function knows nothing about -- an external JWT, say -- so whether a closed
// posture can be administered is no longer something it can judge.
func securityWarnings(posture x.AnonymousPosture, whitelist string, ips []x.IPRange,
authToken string, aclEnabled, builtinIdentity bool, httpPort int) []string {

hasCredential := aclEnabled || authToken != ""
var out []string

// The shipped default is an empty whitelist, which admits loopback only, so the
// admin plane does not leave the host without an operator widening it. This
// warns at exactly that point: whitelisting answers where a request came from
// and has no credential in it, so a widened range with nothing else configured
// means every address inside it can run privileged operations anonymously.
if posture == x.AnonymousFull && admitsNonLoopback(ips) && !hasCredential {
out = append(out, fmt.Sprintf(
`SECURITY: --security "whitelist=%s" admits non-loopback callers, but neither ACL nor `+
`an admin token is configured. Privileged operations (backup, restore, export, `+
`shutdown, removeNode, moveTablet, assign, draining, config, namespace create and `+
`drop) are reachable from that range WITHOUT ANY CREDENTIAL: anyone who can reach `+
`port %d can read the whole database via backup or export, restore over it, or shut `+
`the cluster down. Set --security "token=..." or enable ACL, and narrow the `+
`whitelist to the addresses that actually administer this cluster. `+
`--security "anonymous=data" additionally denies every administrative operation to `+
`a caller that presents no credential.`, whitelist, httpPort))
}

// A closed posture with nothing that can produce an identity. Every capability
// check will deny, including the operator's own, so the cluster cannot be
// administered at all. This is a misconfiguration rather than a hardening, and
// it is worth saying so loudly at boot instead of letting it surface as a
// permission error during an incident.
if posture.RequiresIdentityForCapability() && !hasCredential && builtinIdentity {
out = append(out, fmt.Sprintf(
`SECURITY: --security "anonymous=%s" requires an identified caller, but neither ACL nor `+
`an admin token is configured, so no request can ever be identified. Every `+
`administrative operation will be denied, including from loopback, and this cluster `+
`cannot be administered. Set --security "token=..." or enable ACL.`, posture))
}

return out
}

// admitsNonLoopback reports whether any whitelist range admits an address outside
// loopback. Loopback is always admitted regardless of the whitelist, so a range
// that covers only loopback -- whitelist=127.0.0.1, or the documented "localhost
// only" example -- does not take the admin plane off the host and is not worth a
// warning.
//
// A range is loopback-only when both of its ends are loopback. That is exact
// rather than approximate: IPv4 loopback is the contiguous block 127.0.0.0/8, and
// IPv6 loopback is the single address ::1, so a range whose ends both fall inside
// one of those cannot contain anything else.
func admitsNonLoopback(ips []x.IPRange) bool {
for _, r := range ips {
if !r.Lower.IsLoopback() || !r.Upper.IsLoopback() {
return true
}
}
return false
}
Loading
Loading