|
2 | 2 | import json |
3 | 3 | import sys |
4 | 4 | import argparse |
| 5 | +import os |
| 6 | +import stat |
5 | 7 |
|
6 | 8 |
|
7 | 9 | # Expected groups for each user |
|
103 | 105 | # postgresql.service is expected to mount /etc as read-only |
104 | 106 | expected_mount = "/etc ro" |
105 | 107 |
|
| 108 | +# Expected directory permissions for security-critical paths |
| 109 | +# Format: path -> (expected_mode, expected_owner, expected_group, description) |
| 110 | +expected_directory_permissions = { |
| 111 | + "/var/lib/postgresql": ( |
| 112 | + "0755", |
| 113 | + "postgres", |
| 114 | + "postgres", |
| 115 | + "PostgreSQL home - must be traversable for nix-profile symlinks", |
| 116 | + ), |
| 117 | + "/var/lib/postgresql/data": ( |
| 118 | + "0750", |
| 119 | + "postgres", |
| 120 | + "postgres", |
| 121 | + "PostgreSQL data directory symlink - secure, postgres only", |
| 122 | + ), |
| 123 | + "/data/pgdata": ( |
| 124 | + "0750", |
| 125 | + "postgres", |
| 126 | + "postgres", |
| 127 | + "Actual PostgreSQL data directory - secure, postgres only", |
| 128 | + ), |
| 129 | + "/etc/postgresql": ( |
| 130 | + "0775", |
| 131 | + "postgres", |
| 132 | + "postgres", |
| 133 | + "PostgreSQL configuration directory - adminapi writable", |
| 134 | + ), |
| 135 | + "/etc/postgresql-custom": ( |
| 136 | + "0775", |
| 137 | + "postgres", |
| 138 | + "postgres", |
| 139 | + "PostgreSQL custom configuration - adminapi writable", |
| 140 | + ), |
| 141 | + "/etc/ssl/private": ( |
| 142 | + "0750", |
| 143 | + "root", |
| 144 | + "ssl-cert", |
| 145 | + "SSL private keys directory - secure, ssl-cert group only", |
| 146 | + ), |
| 147 | + "/home/postgres": ( |
| 148 | + "0750", |
| 149 | + "postgres", |
| 150 | + "postgres", |
| 151 | + "postgres user home directory - secure, postgres only", |
| 152 | + ), |
| 153 | + "/var/log/postgresql": ( |
| 154 | + "0750", |
| 155 | + "postgres", |
| 156 | + "postgres", |
| 157 | + "PostgreSQL logs directory - secure, postgres only", |
| 158 | + ), |
| 159 | +} |
| 160 | + |
106 | 161 |
|
107 | 162 | # This program depends on osquery being installed on the system |
108 | 163 | # Function to run osquery |
@@ -189,6 +244,75 @@ def check_postgresql_mount(): |
189 | 244 | print("postgresql.service mounts /etc as read-only.") |
190 | 245 |
|
191 | 246 |
|
| 247 | +def check_directory_permissions(): |
| 248 | + """Check that security-critical directories have the correct permissions.""" |
| 249 | + errors = [] |
| 250 | + |
| 251 | + for path, ( |
| 252 | + expected_mode, |
| 253 | + expected_owner, |
| 254 | + expected_group, |
| 255 | + description, |
| 256 | + ) in expected_directory_permissions.items(): |
| 257 | + # Skip if path doesn't exist (might be a symlink or not created yet) |
| 258 | + if not os.path.exists(path): |
| 259 | + print(f"Warning: {path} does not exist, skipping permission check") |
| 260 | + continue |
| 261 | + |
| 262 | + # Get actual permissions |
| 263 | + try: |
| 264 | + stat_info = os.stat(path) |
| 265 | + actual_mode = oct(stat.S_IMODE(stat_info.st_mode))[2:] # Remove '0o' prefix |
| 266 | + |
| 267 | + # Get owner and group names |
| 268 | + import pwd |
| 269 | + import grp |
| 270 | + |
| 271 | + actual_owner = pwd.getpwuid(stat_info.st_uid).pw_name |
| 272 | + actual_group = grp.getgrgid(stat_info.st_gid).gr_name |
| 273 | + |
| 274 | + # Check permissions |
| 275 | + if actual_mode != expected_mode: |
| 276 | + errors.append( |
| 277 | + f"ERROR: {path} has mode {actual_mode}, expected {expected_mode}\n" |
| 278 | + f" Description: {description}\n" |
| 279 | + f" Fix: sudo chmod {expected_mode} {path}" |
| 280 | + ) |
| 281 | + |
| 282 | + # Check ownership |
| 283 | + if actual_owner != expected_owner: |
| 284 | + errors.append( |
| 285 | + f"ERROR: {path} has owner {actual_owner}, expected {expected_owner}\n" |
| 286 | + f" Description: {description}\n" |
| 287 | + f" Fix: sudo chown {expected_owner}:{actual_group} {path}" |
| 288 | + ) |
| 289 | + |
| 290 | + # Check group |
| 291 | + if actual_group != expected_group: |
| 292 | + errors.append( |
| 293 | + f"ERROR: {path} has group {actual_group}, expected {expected_group}\n" |
| 294 | + f" Description: {description}\n" |
| 295 | + f" Fix: sudo chown {actual_owner}:{expected_group} {path}" |
| 296 | + ) |
| 297 | + |
| 298 | + if not errors or not any(path in err for err in errors): |
| 299 | + print(f"✓ {path}: {actual_mode} {actual_owner}:{actual_group} - OK") |
| 300 | + |
| 301 | + except Exception as e: |
| 302 | + errors.append(f"ERROR: Failed to check {path}: {str(e)}") |
| 303 | + |
| 304 | + if errors: |
| 305 | + print("\n" + "=" * 80) |
| 306 | + print("DIRECTORY PERMISSION ERRORS DETECTED:") |
| 307 | + print("=" * 80) |
| 308 | + for error in errors: |
| 309 | + print(error) |
| 310 | + print("=" * 80) |
| 311 | + sys.exit(1) |
| 312 | + |
| 313 | + print("\nAll directory permissions are correct.") |
| 314 | + |
| 315 | + |
192 | 316 | def main(): |
193 | 317 | parser = argparse.ArgumentParser( |
194 | 318 | prog="Supabase Postgres Artifact Permissions Checker", |
@@ -258,6 +382,9 @@ def main(): |
258 | 382 | # Check if postgresql.service is using a read-only mount for /etc |
259 | 383 | check_postgresql_mount() |
260 | 384 |
|
| 385 | + # Check directory permissions for security-critical paths |
| 386 | + check_directory_permissions() |
| 387 | + |
261 | 388 |
|
262 | 389 | if __name__ == "__main__": |
263 | 390 | main() |
0 commit comments