permcheck: imp code, docs

This commit is contained in:
Eugene Burkov 2024-11-28 15:14:06 +03:00
parent 09e4ae1ba1
commit c076c93669
6 changed files with 90 additions and 87 deletions

View File

@ -25,71 +25,64 @@ func check(
querylogDir string, querylogDir string,
confFilePath string, confFilePath string,
) { ) {
checkDir(ctx, l, workDir) dirLoggger, fileLogger := l.With("type", typeDir), l.With("type", typeFile)
checkFile(ctx, l, confFilePath) checkDir(ctx, dirLoggger, workDir)
checkFile(ctx, fileLogger, confFilePath)
// TODO(a.garipov): Put all paths in one place and remove this duplication. // TODO(a.garipov): Put all paths in one place and remove this duplication.
checkDir(ctx, l, dataDir) checkDir(ctx, dirLoggger, dataDir)
checkDir(ctx, l, filepath.Join(dataDir, "filters")) checkDir(ctx, dirLoggger, filepath.Join(dataDir, "filters"))
checkFile(ctx, l, filepath.Join(dataDir, "sessions.db")) checkFile(ctx, fileLogger, filepath.Join(dataDir, "sessions.db"))
checkFile(ctx, l, filepath.Join(dataDir, "leases.json")) checkFile(ctx, fileLogger, filepath.Join(dataDir, "leases.json"))
if dataDir != querylogDir { if dataDir != querylogDir {
checkDir(ctx, l, querylogDir) checkDir(ctx, dirLoggger, querylogDir)
} }
checkFile(ctx, l, filepath.Join(querylogDir, "querylog.json")) checkFile(ctx, fileLogger, filepath.Join(querylogDir, "querylog.json"))
checkFile(ctx, l, filepath.Join(querylogDir, "querylog.json.1")) checkFile(ctx, fileLogger, filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir { if dataDir != statsDir {
checkDir(ctx, l, statsDir) checkDir(ctx, dirLoggger, statsDir)
} }
checkFile(ctx, l, filepath.Join(statsDir, "stats.db")) checkFile(ctx, fileLogger, filepath.Join(statsDir, "stats.db"))
} }
// checkDir checks the permissions of a single directory. The results are // checkDir checks the permissions of a single directory. The results are
// logged at the appropriate level. // logged at the appropriate level.
func checkDir(ctx context.Context, l *slog.Logger, dirPath string) { func checkDir(ctx context.Context, l *slog.Logger, dirPath string) {
checkPath(ctx, l, dirPath, typeDir, aghos.DefaultPermDir) checkPath(ctx, l, dirPath, aghos.DefaultPermDir)
} }
// checkFile checks the permissions of a single file. The results are logged at // checkFile checks the permissions of a single file. The results are logged at
// the appropriate level. // the appropriate level.
func checkFile(ctx context.Context, l *slog.Logger, filePath string) { func checkFile(ctx context.Context, l *slog.Logger, filePath string) {
checkPath(ctx, l, filePath, typeFile, aghos.DefaultPermFile) checkPath(ctx, l, filePath, aghos.DefaultPermFile)
} }
// checkPath checks the permissions of a single filesystem entity. The results // checkPath checks the permissions of a single filesystem entity. The results
// are logged at the appropriate level. // are logged at the appropriate level.
func checkPath(ctx context.Context, l *slog.Logger, entPath, fileType string, want fs.FileMode) { func checkPath(ctx context.Context, l *slog.Logger, fpath string, want fs.FileMode) {
s, err := os.Stat(entPath) l = l.With("path", fpath)
s, err := os.Stat(fpath)
if err != nil { if err != nil {
logFunc := l.ErrorContext lvl := slog.LevelError
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
logFunc = l.DebugContext lvl = slog.LevelDebug
} }
logFunc( l.Log(ctx, lvl, "checking permissions", slogutil.KeyError, err)
ctx,
"checking permissions",
"type", fileType,
"path", entPath,
slogutil.KeyError, err,
)
return return
} }
// TODO(a.garipov): Add a more fine-grained check and result reporting. // TODO(a.garipov): Add a more fine-grained check and result reporting.
perm := s.Mode().Perm() perm := s.Mode().Perm()
if perm != want { if perm == want {
l.WarnContext( return
ctx,
"found unexpected permissions",
"type", fileType,
"path", entPath,
"got", fmt.Sprintf("%#o", perm),
"want", fmt.Sprintf("%#o", want),
)
} }
permOct, wantOct := fmt.Sprintf("%#o", perm), fmt.Sprintf("%#o", want)
l.WarnContext(ctx, "found unexpected permissions", "perm", permOct, "want", wantOct)
} }

View File

@ -12,6 +12,8 @@ import (
// check is the Windows-specific implementation of [Check]. // check is the Windows-specific implementation of [Check].
func check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) { func check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
l = l.With("type", typeDir, "path", workDir)
dacl, owner, err := getSecurityInfo(workDir) dacl, owner, err := getSecurityInfo(workDir)
if err != nil { if err != nil {
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err) l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
@ -20,12 +22,14 @@ func check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
} }
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) { if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
l.WarnContext(ctx, "working directory owner is not in administrators group") l.WarnContext(ctx, "owner is not in administrators group")
} }
err = rangeACEs(dacl, func(hdr windows.ACE_HEADER, mask windows.ACCESS_MASK, sid *windows.SID) (cont bool) { err = rangeACEs(dacl, func(
l.DebugContext(ctx, "checking entry", "sid", sid, "mask", mask) hdr windows.ACE_HEADER,
mask windows.ACCESS_MASK,
sid *windows.SID,
) (cont bool) {
warn := false warn := false
switch { switch {
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE: case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:

View File

@ -45,63 +45,63 @@ func migrate(
querylogDir string, querylogDir string,
confFilePath string, confFilePath string,
) { ) {
chmodDir(ctx, l, workDir) dirLoggger, fileLogger := l.With("type", typeDir), l.With("type", typeFile)
chmodFile(ctx, l, confFilePath) chmodDir(ctx, dirLoggger, workDir)
chmodFile(ctx, fileLogger, confFilePath)
// TODO(a.garipov): Put all paths in one place and remove this duplication. // TODO(a.garipov): Put all paths in one place and remove this duplication.
chmodDir(ctx, l, dataDir) chmodDir(ctx, dirLoggger, dataDir)
chmodDir(ctx, l, filepath.Join(dataDir, "filters")) chmodDir(ctx, dirLoggger, filepath.Join(dataDir, "filters"))
chmodFile(ctx, l, filepath.Join(dataDir, "sessions.db")) chmodFile(ctx, fileLogger, filepath.Join(dataDir, "sessions.db"))
chmodFile(ctx, l, filepath.Join(dataDir, "leases.json")) chmodFile(ctx, fileLogger, filepath.Join(dataDir, "leases.json"))
if dataDir != querylogDir { if dataDir != querylogDir {
chmodDir(ctx, l, querylogDir) chmodDir(ctx, dirLoggger, querylogDir)
} }
chmodFile(ctx, l, filepath.Join(querylogDir, "querylog.json")) chmodFile(ctx, fileLogger, filepath.Join(querylogDir, "querylog.json"))
chmodFile(ctx, l, filepath.Join(querylogDir, "querylog.json.1")) chmodFile(ctx, fileLogger, filepath.Join(querylogDir, "querylog.json.1"))
if dataDir != statsDir { if dataDir != statsDir {
chmodDir(ctx, l, statsDir) chmodDir(ctx, dirLoggger, statsDir)
} }
chmodFile(ctx, l, filepath.Join(statsDir, "stats.db")) chmodFile(ctx, fileLogger, filepath.Join(statsDir, "stats.db"))
} }
// chmodDir changes the permissions of a single directory. The results are // chmodDir changes the permissions of a single directory. The results are
// logged at the appropriate level. // logged at the appropriate level.
func chmodDir(ctx context.Context, l *slog.Logger, dirPath string) { func chmodDir(ctx context.Context, l *slog.Logger, dirPath string) {
chmodPath(ctx, l, dirPath, typeDir, aghos.DefaultPermDir) chmodPath(ctx, l, dirPath, aghos.DefaultPermDir)
} }
// chmodFile changes the permissions of a single file. The results are logged // chmodFile changes the permissions of a single file. The results are logged
// at the appropriate level. // at the appropriate level.
func chmodFile(ctx context.Context, l *slog.Logger, filePath string) { func chmodFile(ctx context.Context, l *slog.Logger, filePath string) {
chmodPath(ctx, l, filePath, typeFile, aghos.DefaultPermFile) chmodPath(ctx, l, filePath, aghos.DefaultPermFile)
} }
// chmodPath changes the permissions of a single filesystem entity. The results // chmodPath changes the permissions of a single filesystem entity. The results
// are logged at the appropriate level. // are logged at the appropriate level.
func chmodPath(ctx context.Context, l *slog.Logger, entPath, fileType string, fm fs.FileMode) { func chmodPath(ctx context.Context, l *slog.Logger, fpath string, fm fs.FileMode) {
switch err := os.Chmod(entPath, fm); { var lvl slog.Level
var msg string
args := []any{"path", fpath}
switch err := os.Chmod(fpath, fm); {
case err == nil: case err == nil:
l.InfoContext(ctx, "changed permissions", "type", fileType, "path", entPath) lvl = slog.LevelInfo
msg = "changed permissions"
case errors.Is(err, os.ErrNotExist): case errors.Is(err, os.ErrNotExist):
l.DebugContext( lvl = slog.LevelDebug
ctx, msg = "checking permissions"
"changing permissions", args = append(args, slogutil.KeyError, err)
"type", fileType,
"path", entPath,
slogutil.KeyError, err,
)
default: default:
l.ErrorContext( lvl = slog.LevelError
ctx, msg = "can not change permissions; this can leave your system vulnerable, see " +
"can not change permissions; this can leave your system vulnerable, see "+ "https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns"
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns", args = append(args, "target_perm", fmt.Sprintf("%#o", fm), slogutil.KeyError, err)
"type", fileType,
"path", entPath,
"target_perm", fmt.Sprintf("%#o", fm),
slogutil.KeyError, err,
)
} }
l.Log(ctx, lvl, msg, args...)
} }

View File

@ -12,6 +12,8 @@ import (
// needsMigration is the Windows-specific implementation of [NeedsMigration]. // needsMigration is the Windows-specific implementation of [NeedsMigration].
func needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok bool) { func needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok bool) {
l = l.With("type", typeDir, "path", workDir)
dacl, owner, err := getSecurityInfo(workDir) dacl, owner, err := getSecurityInfo(workDir)
if err != nil { if err != nil {
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err) l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
@ -54,9 +56,11 @@ func needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok
// migrate is the Windows-specific implementation of [Migrate]. // migrate is the Windows-specific implementation of [Migrate].
func migrate(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) { func migrate(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
dirLogger := l.With("type", typeDir, "path", workDir)
dacl, owner, err := getSecurityInfo(workDir) dacl, owner, err := getSecurityInfo(workDir)
if err != nil { if err != nil {
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err) dirLogger.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
return return
} }
@ -65,9 +69,10 @@ func migrate(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
var admins *windows.SID var admins *windows.SID
admins, err = windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) admins, err = windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil { if err != nil {
// This log message is not related to the directory.
l.ErrorContext(ctx, "creating administrators sid", slogutil.KeyError, err) l.ErrorContext(ctx, "creating administrators sid", slogutil.KeyError, err)
} else { } else {
l.InfoContext(ctx, "migrating working directory owner", "sid", admins) dirLogger.InfoContext(ctx, "migrating owner", "sid", admins)
owner = admins owner = admins
} }
} }
@ -82,26 +87,26 @@ func migrate(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
switch { switch {
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE: case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
// Add non-allowed access control entries as is. // Add non-allowed access control entries as is.
l.InfoContext(ctx, "migrating deny control entry", "sid", sid) dirLogger.InfoContext(ctx, "migrating deny control entry", "sid", sid)
accessEntries = append(accessEntries, newDenyExplicitAccess(sid, mask)) accessEntries = append(accessEntries, newDenyExplicitAccess(sid, mask))
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid): case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
// Skip non-administrator ACEs. // Skip non-administrator ACEs.
l.InfoContext(ctx, "removing access control entry", "sid", sid) dirLogger.InfoContext(ctx, "removing access control entry", "sid", sid)
default: default:
l.InfoContext(ctx, "migrating access control entry", "sid", sid, "mask", mask) dirLogger.InfoContext(ctx, "migrating access control entry", "sid", sid, "mask", mask)
accessEntries = append(accessEntries, newFullExplicitAccess(sid)) accessEntries = append(accessEntries, newFullExplicitAccess(sid))
} }
return true return true
}) })
if err != nil { if err != nil {
l.ErrorContext(ctx, "ranging trough access control entries", slogutil.KeyError, err) dirLogger.ErrorContext(ctx, "filtering access control entries", slogutil.KeyError, err)
return return
} }
err = setSecurityInfo(workDir, owner, accessEntries) err = setSecurityInfo(workDir, owner, accessEntries)
if err != nil { if err != nil {
l.ErrorContext(ctx, "setting security info", slogutil.KeyError, err) dirLogger.ErrorContext(ctx, "setting security info", slogutil.KeyError, err)
} }
} }

View File

@ -8,8 +8,6 @@ import (
) )
// File type constants for logging. // File type constants for logging.
//
// TODO(e.burkov): !! Use in Windows logging.
const ( const (
typeDir = "directory" typeDir = "directory"
typeFile = "file" typeFile = "file"

View File

@ -16,6 +16,8 @@ const securityInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMA
windows.PROTECTED_DACL_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION |
windows.UNPROTECTED_DACL_SECURITY_INFORMATION windows.UNPROTECTED_DACL_SECURITY_INFORMATION
// objectType is the type of the object for directories in context of security
// API.
const objectType = windows.SE_FILE_OBJECT const objectType = windows.SE_FILE_OBJECT
// fileDeleteChildRight is the mask bit for the right to delete a child object. // fileDeleteChildRight is the mask bit for the right to delete a child object.
@ -41,8 +43,8 @@ const fullControlMask windows.ACCESS_MASK = windows.FILE_LIST_DIRECTORY |
windows.SYNCHRONIZE windows.SYNCHRONIZE
// aceFunc is a function that handles access control entries in the // aceFunc is a function that handles access control entries in the
// discretionary access control list. It should return true to continue ranging // discretionary access control list. It should return true to continue
// or false to stop. // iterating over the entries, or false to stop.
type aceFunc = func( type aceFunc = func(
hdr windows.ACE_HEADER, hdr windows.ACE_HEADER,
mask windows.ACCESS_MASK, mask windows.ACCESS_MASK,
@ -50,7 +52,7 @@ type aceFunc = func(
) (cont bool) ) (cont bool)
// rangeACEs ranges over the access control entries in the discretionary access // rangeACEs ranges over the access control entries in the discretionary access
// control list of the specified security descriptor. // control list of the specified security descriptor and calls f for each one.
func rangeACEs(dacl *windows.ACL, f aceFunc) (err error) { func rangeACEs(dacl *windows.ACL, f aceFunc) (err error) {
var errs []error var errs []error
for i := range uint32(dacl.AceCount) { for i := range uint32(dacl.AceCount) {
@ -119,15 +121,14 @@ func getSecurityInfo(fname string) (dacl *windows.ACL, owner *windows.SID, err e
// newFullExplicitAccess creates a new explicit access entry with full control // newFullExplicitAccess creates a new explicit access entry with full control
// permissions. // permissions.
func newFullExplicitAccess(sid *windows.SID) (expAcc windows.EXPLICIT_ACCESS) { func newFullExplicitAccess(sid *windows.SID) (accEnt windows.EXPLICIT_ACCESS) {
// TODO(e.burkov): !! lookup account type
return windows.EXPLICIT_ACCESS{ return windows.EXPLICIT_ACCESS{
AccessPermissions: fullControlMask, AccessPermissions: fullControlMask,
AccessMode: windows.GRANT_ACCESS, AccessMode: windows.GRANT_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT, Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{ Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID, TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_GROUP, TrusteeType: windows.TRUSTEE_IS_UNKNOWN,
TrusteeValue: windows.TrusteeValueFromSID(sid), TrusteeValue: windows.TrusteeValueFromSID(sid),
}, },
} }
@ -135,15 +136,17 @@ func newFullExplicitAccess(sid *windows.SID) (expAcc windows.EXPLICIT_ACCESS) {
// newDenyExplicitAccess creates a new explicit access entry with specified deny // newDenyExplicitAccess creates a new explicit access entry with specified deny
// permissions. // permissions.
func newDenyExplicitAccess(sid *windows.SID, mask windows.ACCESS_MASK) (expAcc windows.EXPLICIT_ACCESS) { func newDenyExplicitAccess(
// TODO(e.burkov): !! lookup account type sid *windows.SID,
mask windows.ACCESS_MASK,
) (accEnt windows.EXPLICIT_ACCESS) {
return windows.EXPLICIT_ACCESS{ return windows.EXPLICIT_ACCESS{
AccessPermissions: mask, AccessPermissions: mask,
AccessMode: windows.DENY_ACCESS, AccessMode: windows.DENY_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT, Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{ Trustee: windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID, TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_GROUP, TrusteeType: windows.TRUSTEE_IS_UNKNOWN,
TrusteeValue: windows.TrusteeValueFromSID(sid), TrusteeValue: windows.TrusteeValueFromSID(sid),
}, },
} }