diff --git a/lib/fedorabase.pm b/lib/fedorabase.pm
--- a/lib/fedorabase.pm
+++ b/lib/fedorabase.pm
@@ -55,7 +55,8 @@
             return;
         }
         elsif (check_screen $bad, 0) {
-            script_run "exit";
+            # we don't want to 'wait' for this as it won't return
+            script_run "exit", 0;
             sleep 2;
         }
         if ($needuser and check_screen "text_console_login", 0) {
diff --git a/lib/freeipa.pm b/lib/freeipa.pm
new file mode 100644
--- /dev/null
+++ b/lib/freeipa.pm
@@ -0,0 +1,60 @@
+package freeipa;
+
+use strict;
+
+use base 'Exporter';
+use Exporter;
+
+use testapi;
+
+our @EXPORT = qw/add_user start_webui/;
+
+# add a user with given username and surname, always uses the password
+# "correcthorse". Assumes FreeIPA web UI is showing the Users screen.
+sub add_user {
+    my ($user, $surname) = @_;
+    wait_still_screen 1;
+    assert_and_click "freeipa_webui_add_button";
+    assert_screen "freeipa_webui_add_user";
+    wait_still_screen 1;
+    type_string $user;
+    wait_still_screen 1;
+    send_key "tab";
+    # we don't need to be too careful here as the names don't matter
+    type_string "Test";
+    send_key "tab";
+    type_string $surname;
+    send_key "tab";
+    send_key "tab";
+    send_key "tab";
+    send_key "tab";
+    type_string "correcthorse";
+    wait_still_screen 1;
+    send_key "tab";
+    wait_still_screen 1;
+    type_string "correcthorse\n";
+}
+
+# access the FreeIPA web UI and log in as a given user. Assumes
+# Firefox is running.
+sub start_webui {
+    my ($user, $password) = @_;
+    # new tab
+    send_key "ctrl-t";
+    wait_still_screen 2;
+    type_string "https://ipa001.domain.local";
+    # firefox's stupid 'smart' url bar is a pain. wait for things to settle.
+    wait_still_screen 3;
+    send_key "ret";
+    assert_screen "freeipa_webui_login";
+    type_string $user;
+    wait_still_screen 1;
+    send_key "tab";
+    wait_still_screen 1;
+    type_string $password;
+    wait_still_screen 1;
+    send_key "ret";
+    # if we logged in as 'admin' we should land on the admin 'Active
+    # users' screen, otherwise we should land on the user's own page
+    $user eq 'admin' ? assert_screen "freeipa_webui_users" : assert_screen "freeipa_webui_user";
+}
diff --git a/lib/installedtest.pm b/lib/installedtest.pm
--- a/lib/installedtest.pm
+++ b/lib/installedtest.pm
@@ -39,7 +39,8 @@
     }
 
     # Upload /var/log
-    script_run "tar czvf /tmp/var_log.tar.gz /var/log";
+    # lastlog can mess up tar sometimes and it's not much use
+    script_run "tar czvf /tmp/var_log.tar.gz --exclude='lastlog' /var/log";
     upload_logs "/tmp/var_log.tar.gz";
 }
 
@@ -71,6 +72,7 @@
     # open a new tab so we don't race with the default page load
     # (also focuses the location bar for us)
     send_key "ctrl-t";
+    wait_still_screen 2;
     type_string "http://localhost:9090";
     # firefox's stupid 'smart' url bar is a pain. wait for things to settle.
     wait_still_screen 3;
diff --git a/needles/freeipa/webui_actions.json b/needles/freeipa/webui_actions.json
new file mode 100644
--- /dev/null
+++ b/needles/freeipa/webui_actions.json
@@ -0,0 +1,15 @@
+{
+  "properties": [],
+  "area": [
+    {
+      "xpos": 231,
+      "ypos": 288,
+      "width": 47,
+      "height": 15,
+      "type": "match"
+    }
+  ],
+  "tags": [
+    "freeipa_webui_actions"
+  ]
+}
\ No newline at end of file
diff --git a/needles/freeipa/webui_actions.png b/needles/freeipa/webui_actions.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/needles/freeipa/webui_hbac.json b/needles/freeipa/webui_hbac.json
new file mode 100644
--- /dev/null
+++ b/needles/freeipa/webui_hbac.json
@@ -0,0 +1,15 @@
+{
+  "tags": [
+    "freeipa_webui_hbac"
+  ],
+  "area": [
+    {
+      "xpos": 19,
+      "ypos": 200,
+      "width": 127,
+      "height": 24,
+      "type": "match"
+    }
+  ],
+  "properties": []
+}
\ No newline at end of file
diff --git a/needles/freeipa/webui_hbac.png b/needles/freeipa/webui_hbac.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/needles/freeipa/webui_logout.json b/needles/freeipa/webui_logout.json
new file mode 100644
--- /dev/null
+++ b/needles/freeipa/webui_logout.json
@@ -0,0 +1,15 @@
+{
+  "area": [
+    {
+      "xpos": 754,
+      "ypos": 202,
+      "width": 13,
+      "height": 12,
+      "type": "match"
+    }
+  ],
+  "properties": [],
+  "tags": [
+    "freeipa_webui_logout"
+  ]
+}
\ No newline at end of file
diff --git a/needles/freeipa/webui_logout.png b/needles/freeipa/webui_logout.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/needles/freeipa/webui_reset_password_button.json b/needles/freeipa/webui_reset_password_button.json
new file mode 100644
--- /dev/null
+++ b/needles/freeipa/webui_reset_password_button.json
@@ -0,0 +1,15 @@
+{
+  "properties": [],
+  "area": [
+    {
+      "xpos": 575,
+      "ypos": 383,
+      "width": 92,
+      "height": 13,
+      "type": "match"
+    }
+  ],
+  "tags": [
+    "freeipa_webui_reset_password_button"
+  ]
+}
\ No newline at end of file
diff --git a/needles/freeipa/webui_reset_password_button.png b/needles/freeipa/webui_reset_password_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/needles/freeipa/webui_reset_password_link.json b/needles/freeipa/webui_reset_password_link.json
new file mode 100644
--- /dev/null
+++ b/needles/freeipa/webui_reset_password_link.json
@@ -0,0 +1,15 @@
+{
+  "properties": [],
+  "area": [
+    {
+      "xpos": 141,
+      "ypos": 322,
+      "width": 89,
+      "height": 13,
+      "type": "match"
+    }
+  ],
+  "tags": [
+    "freeipa_webui_reset_password_link"
+  ]
+}
\ No newline at end of file
diff --git a/needles/freeipa/webui_reset_password_link.png b/needles/freeipa/webui_reset_password_link.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/needles/freeipa/webui_user.json b/needles/freeipa/webui_user.json
new file mode 100644
--- /dev/null
+++ b/needles/freeipa/webui_user.json
@@ -0,0 +1,15 @@
+{
+  "tags": [
+    "freeipa_webui_user"
+  ],
+  "properties": [],
+  "area": [
+    {
+      "xpos": 41,
+      "ypos": 194,
+      "width": 56,
+      "height": 21,
+      "type": "match"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/needles/freeipa/webui_user.png b/needles/freeipa/webui_user.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/needles/freeipa/webui_user_menu.json b/needles/freeipa/webui_user_menu.json
new file mode 100644
--- /dev/null
+++ b/needles/freeipa/webui_user_menu.json
@@ -0,0 +1,22 @@
+{
+  "properties": [],
+  "area": [
+    {
+      "xpos": 885,
+      "ypos": 83,
+      "width": 10,
+      "height": 11,
+      "type": "match"
+    },
+    {
+      "xpos": 826,
+      "ypos": 84,
+      "width": 11,
+      "height": 10,
+      "type": "match"
+    }
+  ],
+  "tags": [
+    "freeipa_webui_user_menu"
+  ]
+}
\ No newline at end of file
diff --git a/needles/freeipa/webui_user_menu.png b/needles/freeipa/webui_user_menu.png
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/templates b/templates
--- a/templates
+++ b/templates
@@ -1898,7 +1898,7 @@
                     {
                       name => "realmd_join_cockpit",
                       settings => [
-                        { key => "POSTINSTALL", value => "realmd_join_cockpit freeipa_webui freeipa_client" },
+                        { key => "POSTINSTALL", value => "realmd_join_cockpit freeipa_webui freeipa_password_change freeipa_client" },
                         { key => "USER_LOGIN", value => "false" },
                         { key => "ROOT_PASSWORD", value => "weakpassword" },
                         { key => "START_AFTER_TEST", value => "server_cockpit_default" },
diff --git a/tests/freeipa_password_change_postinstall.pm b/tests/freeipa_password_change_postinstall.pm
new file mode 100644
--- /dev/null
+++ b/tests/freeipa_password_change_postinstall.pm
@@ -0,0 +1,59 @@
+use base "installedtest";
+use strict;
+use testapi;
+use freeipa;
+
+sub run {
+    my $self = shift;
+    $self->console_login(user=>'root');
+    # clear browser data so we don't go back to the 'admin' login
+    assert_script_run 'rm -rf /root/.mozilla';
+    type_string "startx /usr/bin/firefox\n";
+    assert_screen "firefox";
+    start_webui("test1", "batterystaple");
+    assert_and_click "freeipa_webui_actions";
+    assert_and_click "freeipa_webui_reset_password_link";
+    wait_still_screen 3;
+    type_string "batterystaple";
+    wait_still_screen 1;
+    send_key "tab";
+    send_key "tab";
+    wait_still_screen 1;
+    type_string "loremipsum";
+    wait_still_screen 1;
+    send_key "tab";
+    wait_still_screen 1;
+    type_string "loremipsum";
+    wait_still_screen 1;
+    assert_and_click "freeipa_webui_reset_password_button";
+    wait_still_screen 2;
+    # log out
+    assert_and_click "freeipa_webui_user_menu";
+    assert_and_click "freeipa_webui_logout";
+    wait_still_screen 3;
+    # close browser, back to console
+    send_key "ctrl-q";
+    # we don't get back to a prompt instantly and keystrokes while X
+    # is still shutting down are swallowed, so wait_still_screen before
+    # finishing (and handing off to freeipa_client_postinstall)
+    wait_still_screen 5;
+    # check we can kinit with changed password
+    assert_script_run 'printf "loremipsum" | kinit test1';
+    # change password via CLI (back to batterystaple, as that's what
+    # freeipa_client test expects)
+    assert_script_run 'dnf -y install freeipa-admintools';
+    assert_script_run 'printf "batterystaple\nbatterystaple" | ipa user-mod test1 --password';
+    # check we can kinit again
+    assert_script_run 'printf "batterystaple" | kinit test1';
+    # we just stay here - freeipa_client will pick right up
+}
+
+sub test_flags {
+    # without anything - rollback to 'lastgood' snapshot if failed
+    # 'fatal' - whole test suite is in danger if this fails
+    # 'milestone' - after this test succeeds, update 'lastgood'
+    # 'important' - if this fails, set the overall state to 'fail'
+    return {};
+}
+
+1;
diff --git a/tests/freeipa_webui_postinstall.pm b/tests/freeipa_webui_postinstall.pm
--- a/tests/freeipa_webui_postinstall.pm
+++ b/tests/freeipa_webui_postinstall.pm
@@ -1,58 +1,21 @@
 use base "installedtest";
 use strict;
 use testapi;
-
-sub add_user {
-    my ($user, $surname) = @_;
-    wait_still_screen 1;
-    assert_and_click "freeipa_webui_add_button";
-    assert_screen "freeipa_webui_add_user";
-    wait_still_screen 1;
-    type_string $user;
-    wait_still_screen 1;
-    send_key "tab";
-    # we don't need to be too careful here as the names don't matter
-    type_string "Test";
-    send_key "tab";
-    type_string $surname;
-    send_key "tab";
-    send_key "tab";
-    send_key "tab";
-    send_key "tab";
-    type_string "correcthorse";
-    wait_still_screen 1;
-    send_key "tab";
-    wait_still_screen 1;
-    type_string "correcthorse\n";
-}
+use freeipa;
 
 sub run {
-    my $self=shift;
+    my $self = shift;
     # we're restarting firefox (instead of using the same one from
     # freeipa_client_postinstall) so Firefox's trusted CA store
     # refreshes and it trusts the web server cert
     type_string "startx /usr/bin/firefox\n";
     assert_screen "firefox";
-    # new tab
-    send_key "ctrl-t";
-    wait_still_screen 2;
-    type_string "https://ipa001.domain.local";
-    # firefox's stupid 'smart' url bar is a pain. wait for things to settle.
-    wait_still_screen 3;
-    send_key "ret";
-    assert_screen "freeipa_webui_login";
-    type_string "admin";
-    wait_still_screen 1;
-    send_key "tab";
-    wait_still_screen 1;
-    type_string "monkeys123";
-    wait_still_screen 1;
-    send_key "ret";
-    assert_screen "freeipa_webui_users";
+    start_webui("admin", "monkeys123");
     add_user("test3", "Three");
     add_user("test4", "Four");
     assert_screen "freeipa_webui_users_added";
     assert_and_click "freeipa_webui_policy";
+    assert_screen "freeipa_webui_hbac";
     assert_and_click "freeipa_webui_add_button";
     assert_screen "freeipa_webui_add_policy";
     type_string "allow-test3";
@@ -109,16 +72,16 @@
     assert_screen "console_password_required";
     type_string "batterystaple\n";
     assert_screen "login_permission_denied";
-    # back to tty1 to let generic freeipa_client test pick up from here
+    # back to tty1
     send_key "ctrl-alt-f1";
 }
 
 sub test_flags {
     # without anything - rollback to 'lastgood' snapshot if failed
     # 'fatal' - whole test suite is in danger if this fails
     # 'milestone' - after this test succeeds, update 'lastgood'
     # 'important' - if this fails, set the overall state to 'fail'
-    return {};
+    return { milestone => 1 };
 }
 
 1;
diff --git a/tests/role_deploy_domain_controller.pm b/tests/role_deploy_domain_controller.pm
--- a/tests/role_deploy_domain_controller.pm
+++ b/tests/role_deploy_domain_controller.pm
@@ -85,6 +85,8 @@
     assert_script_run 'ipa hbacrule-add-user testrule --users=test1';
     # disable the default 'everyone everywhere' rule
     assert_script_run 'ipa hbacrule-disable allow_all';
+    # allow immediate password changes (as we need to test this)
+    assert_script_run 'ipa pwpolicy-mod --minlife=0';
     # kinit as each user and set a new password
     assert_script_run 'printf "correcthorse\nbatterystaple\nbatterystaple" | kinit test1@DOMAIN.LOCAL';
     assert_script_run 'printf "correcthorse\nbatterystaple\nbatterystaple" | kinit test2@DOMAIN.LOCAL';

