<?xml version="1.0" encoding="UTF-8" ?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" version="2.0"><channel><title>Mike Palmiotto | CrunchyData Blog</title>
<atom:link href="https://www.crunchydata.com/blog/author/mike-palmiotto/rss.xml" rel="self" type="application/rss+xml" />
<link>https://www.crunchydata.com/blog/author/mike-palmiotto</link>
<image><url>https://www.crunchydata.com/build/_assets/mike-palmiotto.png-FUNZQRCO.webp</url>
<title>Mike Palmiotto | CrunchyData Blog</title>
<link>https://www.crunchydata.com/blog/author/mike-palmiotto</link>
<width>2241</width>
<height>2819</height></image>
<description>PostgreSQL experts from Crunchy Data share advice, performance tips, and guides on successfully running PostgreSQL and Kubernetes solutions</description>
<language>en-us</language>
<pubDate>Mon, 14 Feb 2022 04:00:00 EST</pubDate>
<dc:date>2022-02-14T09:00:00.000Z</dc:date>
<dc:language>en-us</dc:language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<item><title><![CDATA[ Safer Application Users in Postgres ]]></title>
<link>https://www.crunchydata.com/blog/safer-application-users-in-postgres</link>
<description><![CDATA[ Risk management for Postgres. A guide to changing application user permissions so they can't delete your production database. ]]></description>
<content:encoded><![CDATA[ <blockquote><p>We deleted our database.</blockquote><p>Two years ago on a Friday afternoon around 4pm I had a customer open a support ticket. The customer thought they were running their test suite against a dev environment. In reality they were running on production. One of the early steps in many test suites is to ensure a clean state:<ol><li><code>DROP</code> all tables or <code>DELETE</code> schemas<li><code>CREATE</code> from scratch</ol><p>With <a href=/blog/database-terminology-explained-postgres-high-availability-and-disaster-recovery>disaster recovery</a> and <a href=https://docs.crunchybridge.com/how-to/point-in-time-recovery/>point-in-time recovery</a> in place, we could roll the database back to any exact moment in the past. So we got the timestamp, and they ran the command and recovered their several TB database to exactly the moment before. A stressful Friday afternoon, but no data loss.<p>You might be thinking of the various ways you can prevent this. Set your shell color to red when connected to production. Don't allow public internet access to production. Only allow CI-driven deployment. Here is one more option for you that is great for production risk mitigation: don't allow your production application users to delete data in prod.<h2 id=prevent-application-user-from-deleting-data-in-production><a href=#prevent-application-user-from-deleting-data-in-production>Prevent Application User from Deleting Data in Production</a></h2><p>To prevent an application from deleting data in production, we need to mitigate this risk and restrict the application user from the following operations:<ul><li><code>DROP TABLE</code><li><code>TRUNCATE TABLE</code></ul><p>The approach requires a mixture of best practices and proper configuration. To start, let's define the actors!<h3 id=administrator-user><a href=#administrator-user>Administrator User</a></h3><p>Administrator users are responsible for the creation of database schemas and relations (<dfn>Data Definition Language</dfn>, or <a href=https://www.postgresql.org/docs/current/ddl.html><abbr>DDL</abbr></a>).<p>Let's create an administrator user for the sake of this example:<pre><code class=language-pgsql>CREATE USER admin with PASSWORD 'correcthorsebatterystaple' SUPERUSER;
CREATE ROLE

\du admin
           List of roles
 Role name | Attributes | Member of
-----------+------------+-----------
 admin     | Superuser  | {}
</code></pre><h3 id=application-user><a href=#application-user>Application User</a></h3><p>Application users are generally restricted to performing operations on predefined database relations and schemas (<dfn>Data Manipulation Language</dfn>, or <a href=https://www.postgresql.org/docs/current/dml.html><abbr>DML</abbr></a>).<p><code>DROP</code> and <code>TRUNCATE</code> privileges would not be granted to an application user.<p>Production applications should only need privileges to add and update data. A typical production application grows by:<ul><li>Adding new columns to tables<li>Adding new rows<li>Updating records</ul><p>If your application follows the design pattern above, you might not want to give app users the ability to <code>DROP</code>, <code>TRUNCATE</code>, or <code>DELETE</code> from tables.<p>In the following example, we will use the application user named 'myappuser', so let's create them:<pre><code class=language-pgsql>CREATE USER myappuser WITH PASSWORD 'verygoodpasswordstring';
CREATE ROLE
</code></pre><h3 id=create-tables-as-admin><a href=#create-tables-as-admin>Create Tables as Admin</a></h3><p>Now that we have our actors defined, let's set the stage.<p>We should only create production tables as the administrator user. By default, relation creators are relation owners. Only owners and superusers can perform actions such as <code>DROP TABLE</code>. This protects against accidental deletion of data in production tables by application users. Application users cannot drop tables they do not own.<p>Let's make sure we're the appropriate admin before making our production sandbox:<pre><code class=language-pgsql>SELECT current_user;
 current_user
--------------
 admin
(1 row)
</code></pre><p>Go ahead and create a production <code>SCHEMA</code> and <code>GRANT</code> the appropriate permissions:<pre><code class=language-pgsql>CREATE SCHEMA prod;
CREATE SCHEMA

GRANT USAGE ON SCHEMA prod TO myappuser;
GRANT
</code></pre><p>Now we can create a table for our production data and start testing out some concepts:<pre><code class=language-pgsql>CREATE TABLE prod.userdata (col1 integer, col2 text, col3 text);
CREATE TABLE
</code></pre><p>If we log back in as <code>myappuser</code>, we shouldn't be able to drop the table:<pre><code class=language-pgsql>\c postgres myappuser
Password for user myappuser:
You are now connected to database "postgres" as user "myappuser".
postgres=> DROP TABLE prod.userdata;
ERROR:  must be owner of table userdata
</code></pre><h3 id=least-privilege><a href=#least-privilege>Least Privilege</a></h3><p>We've shown how to block <code>DROP TABLE</code> for application users. To prevent deletion of tuples inside a relation, we need to do a bit more work. The application user should only have access to exactly what it needs.<p>To do this, we <code>GRANT</code> only the privileges that the application user needs, as outlined above:<pre><code class=language-pgsql>postgres=> \c postgres admin
Password for user admin:
You are now connected to database "postgres" as user "admin".

GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA prod TO myappuser;
GRANT
</code></pre><p>Or if you already have some application user created you can <code>REVOKE</code> the unwanted production privileges:<pre><code class=language-pgsql>REVOKE DELETE, TRUNCATE ON ALL TABLES IN SCHEMA prod FROM myappuser;
REVOKE
</code></pre><p>Now our application user cannot delete data:<pre><code class=language-pgsql>\c postgres myappuser
Password for user myappuser:
You are now connected to database "postgres" as user "myappuser".
postgres=> DELETE FROM prod.userdata *;
ERROR:  permission denied for table userdata
postgres=> TRUNCATE TABLE prod.userdata;
ERROR:  permission denied for table userdata
</code></pre><p>Great! We've narrowed down our privileges, but how do we know whether or not we're missing something?<h3 id=check-access><a href=#check-access>Check Access</a></h3><p>When working with roles and permissions, it is always good to do an access check. We have a nice extension I recommend <a href=https://github.com/CrunchyData/crunchy_check_access>crunchy_check_access</a> for walking the full tree of access and permissions.<p>Log in as the admin user and take a look at the privileges we've granted to the application user:<pre><code class=language-pgsql>SELECT base_role,objtype,schemaname,objname,privname FROM all_access() WHERE base_role = 'myappuser' AND schemaname = 'prod';
 base_role | objtype | schemaname | objname  | privname
-----------+---------+------------+----------+----------
 myappuser | schema  | prod       | prod     | USAGE
 myappuser | table   | prod       | userdata | SELECT
 myappuser | table   | prod       | userdata | INSERT
 myappuser | table   | prod       | userdata | UPDATE
(4 rows)
</code></pre><p>It's as simple as that!<h2 id=let-your-application-user-delete-records><a href=#let-your-application-user-delete-records>Let Your Application User Delete Records</a></h2><p>So we've revoked privileges and protected against "accidental" deletion errors in the database, but it is very likely that your application still needs to delete records. Let's look at safer alternative designs for deleting application data.<p>A common pattern in applications is to mark tuples as deleted, rather than deleting them.<p>We can alter the table above to add a timestamp column, named <code>deleted</code>, which has two benefits:<ol><li>Data is never actually deleted, so the issues outlined above are not of concern.<li>We now have a snapshot of records at each moment in time for quick and painless application-level rollback of state.</ol><h3 id=adding-a-deleted-column><a href=#adding-a-deleted-column>Adding a <code>deleted</code> Column</a></h3><p>Assuming we have the production table created already, we can add a <code>deleted</code> column like so:<pre><code class=language-pgsql>ALTER TABLE prod.userdata ADD COLUMN deleted timestamp;
ALTER TABLE
</code></pre><p>NOTE: The <code>ADD COLUMN</code> syntax noted above is an expensive operation, as it holds an Exclusive Lock on the table.<p>Normal table inserts and update operations can still take the same form:<pre><code class=language-pgsql>INSERT INTO prod.userdata VALUES (generate_series(1,10), md5(random()::text), md5(random()::text)) ;
INSERT 0 10
</code></pre><p>We now have the option of updating a row to mark it deleted. Let's say our app wants to delete all records <code>where col1 &#60 3</code>:<pre><code class=language-pgsql>postgres=> UPDATE prod.userdata SET deleted = now() WHERE col1 &#60 3;
UPDATE 2
</code></pre><p>If we want to see all remaining records:<pre><code class=language-pgsql>SELECT * from prod.userdata WHERE deleted IS NULL;
 col1 |               col2               |               col3               | deleted
------+----------------------------------+----------------------------------+---------
    3 | 828748efff06ce5b6f0f8e8931429bd3 | e50fe6654ee497de8ad75746849fba0f |
    4 | 4241511ee0a8f7f76976f0bab43b47f0 | d08e31ba79f972a2983301832ec67b94 |
    5 | 93de032bc9157362593a0259a8558514 | 6cd1639323a0c1a96fb3e781283e19d3 |
    6 | af1e1d81ef68dbd5ac14a0ae55195e2a | a4e500cf2c3ecd24c0a745c42b5af939 |
    7 | bcd0c74ca0d416b3f1b3e7ffda375615 | 361ed5d6bff759df7c138daf4b4b0e1b |
    8 | 35856a2d5b0e5b3e1d3ea4e09f0f88fe | a6d0977908e08626bad8278e965e9315 |
    9 | 43de7e949e9777969248b9b1d751d44e | 196390d618931a8dd3d5473cc23869fa |
   10 | 3fc5661e900a25b96b708f3c22cf1d59 | 2f29a28b25e1a1e25fc10b45fc22bc91 |
(8 rows)
</code></pre><p>We can also filter by timestamp. Say we delete more records, say any of the non-deleted columns, <code>WHERE col1 &#60 6</code>:<pre><code class=language-pgsql>UPDATE prod.userdata SET deleted = now() WHERE deleted IS NULL AND col1 &#60 6;
UPDATE 3

SELECT * from prod.userdata;
 col1 |               col2               |               col3               |          deleted
------+----------------------------------+----------------------------------+----------------------------
    6 | af1e1d81ef68dbd5ac14a0ae55195e2a | a4e500cf2c3ecd24c0a745c42b5af939 |
    7 | bcd0c74ca0d416b3f1b3e7ffda375615 | 361ed5d6bff759df7c138daf4b4b0e1b |
    8 | 35856a2d5b0e5b3e1d3ea4e09f0f88fe | a6d0977908e08626bad8278e965e9315 |
    9 | 43de7e949e9777969248b9b1d751d44e | 196390d618931a8dd3d5473cc23869fa |
   10 | 3fc5661e900a25b96b708f3c22cf1d59 | 2f29a28b25e1a1e25fc10b45fc22bc91 |
    1 | b4fb51aff93bf865c6bc8c5f32b306cf | 49d37b3934e2c44f20ddd87019bc525e | 2022-02-03 16:30:49.445571
    2 | e53507d91f39905f6bcd193636b13c3d | 66066e4c78a3eb701086391052c19b56 | 2022-02-03 16:30:49.445571
    3 | 828748efff06ce5b6f0f8e8931429bd3 | e50fe6654ee497de8ad75746849fba0f | 2022-02-03 16:34:19.953742
    4 | 4241511ee0a8f7f76976f0bab43b47f0 | d08e31ba79f972a2983301832ec67b94 | 2022-02-03 16:34:19.953742
    5 | 93de032bc9157362593a0259a8558514 | 6cd1639323a0c1a96fb3e781283e19d3 | 2022-02-03 16:34:19.953742
(10 rows)
</code></pre><p>We can now restore state using the timestamp from the last delete:<pre><code class=language-pgsql>SELECT * from prod.userdata WHERE deleted IS NULL OR deleted >= timestamp '2022-02-03 16:34:19.953742';
 col1 |               col2               |               col3               |          deleted
------+----------------------------------+----------------------------------+----------------------------
    6 | af1e1d81ef68dbd5ac14a0ae55195e2a | a4e500cf2c3ecd24c0a745c42b5af939 |
    7 | bcd0c74ca0d416b3f1b3e7ffda375615 | 361ed5d6bff759df7c138daf4b4b0e1b |
    8 | 35856a2d5b0e5b3e1d3ea4e09f0f88fe | a6d0977908e08626bad8278e965e9315 |
    9 | 43de7e949e9777969248b9b1d751d44e | 196390d618931a8dd3d5473cc23869fa |
   10 | 3fc5661e900a25b96b708f3c22cf1d59 | 2f29a28b25e1a1e25fc10b45fc22bc91 |
    3 | 828748efff06ce5b6f0f8e8931429bd3 | e50fe6654ee497de8ad75746849fba0f | 2022-02-03 16:34:19.953742
    4 | 4241511ee0a8f7f76976f0bab43b47f0 | d08e31ba79f972a2983301832ec67b94 | 2022-02-03 16:34:19.953742
    5 | 93de032bc9157362593a0259a8558514 | 6cd1639323a0c1a96fb3e781283e19d3 | 2022-02-03 16:34:19.953742
(8 rows)
</code></pre><h2 id=safer-application-users-summary><a href=#safer-application-users-summary>Safer Application Users Summary</a></h2><p>We've shown how to mitigate the risk of accidental deletion of production data, by:<ol><li>Ensuring administrator users are object owners<li>Application users only have privileges for add/update operations<li>Safer deletion of data is possible by using a <code>deleted</code> timestamp column</ol><p>Now we can rest easy knowing our production data is safe from those pesky test scripts!<ul><li>For more information on limiting database user privileges, check out the blog post on <a href=https://blog.crunchydata.com/blog/creating-a-read-only-postgres-user>Creating a Read-Only Postgres User</a>.<li>PostgreSQL's privilege landscape is complicated. There is often more to Least Privilege than meets the eye. For a deeper dive on the complexities, check out the <a href=https://blog.crunchydata.com/blog/postgresql-defaults-and-impact-on-security-part-1>PostgreSQL Defaults and Impact on Security</a> blog series.<li>If you're interested in protecting user data, take a look at the Enhanced RBAC and Superuser Lockdown features of <a href=https://www.crunchydata.com/products/hardened-postgres>Crunchy Hardened PostgreSQL</a>.</ul> ]]></content:encoded>
<category><![CDATA[ Security ]]></category>
<author><![CDATA[ Mike.Palmiotto@crunchydata.com (Mike Palmiotto) ]]></author>
<dc:creator><![CDATA[ Mike Palmiotto ]]></dc:creator>
<guid isPermalink="false">https://blog.crunchydata.com/blog/safer-application-users-in-postgres</guid>
<pubDate>Mon, 14 Feb 2022 04:00:00 EST</pubDate>
<dc:date>2022-02-14T09:00:00.000Z</dc:date>
<atom:updated>2022-02-14T09:00:00.000Z</atom:updated></item></channel></rss>