Skip to content
Merged
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
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
46 changes: 42 additions & 4 deletions dgraph/cmd/alpha/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,19 @@ 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, including schema changes and drops, 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 +322,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 +370,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 +668,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 +738,7 @@ func run() {
AbortOlderThan: abortDur,
StartTime: startTime,
Security: security,
Anonymous: anonymous,
TLSClientConfig: tlsClientConf,
TLSServerConfig: tlsServerConf,
AclJwtAlg: keys.AclJwtAlg,
Expand All @@ -729,18 +748,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